diff --git a/.agents/skills/compute-worker/SKILL.md b/.agents/skills/compute-worker/SKILL.md new file mode 100644 index 00000000..4eddce30 --- /dev/null +++ b/.agents/skills/compute-worker/SKILL.md @@ -0,0 +1,157 @@ +--- +name: compute-worker +description: Platform-aware compute worker and service for constructive-functions. Discovers functions from the database, tracks invocations, dispatches via HTTP. Use when working on the compute-worker, compute-service, function discovery, or invocation tracking. +--- + +# Compute Worker & Service + +The compute-worker is a platform-aware replacement for the legacy knative-job-worker. Instead of discovering functions from a static manifest or env vars, it queries `constructive_infra_public.platform_function_definitions` and tracks every invocation in `platform_function_invocations`. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ compute-service (orchestrator) │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ HTTP callback │ │ ComputeWorker│ │ Scheduler │ │ +│ │ server (:8080)│ │ (polls jobs) │ │ (cron jobs) │ │ +│ └──────────────┘ └──────┬───────┘ └──────────────┘ │ +│ │ │ +│ ┌──────────────┼──────────────┐ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ FunctionDiscovery InvocationTracker compute_request │ +│ (TTL-cached DB (INSERT/UPDATE (HTTP POST to │ +│ lookups) invocations) function URL) │ +└─────────────────────────────────────────────────────────┘ + │ │ │ + ▼ ▼ ▼ + platform_function platform_function Function HTTP + _definitions _invocations endpoint + (read) (write) (send-email:8081) +``` + +## Packages + +### job/compute-worker (`@constructive-io/compute-worker`) + +Core worker class and supporting modules: + +| File | Purpose | +|------|---------| +| `src/index.ts` | `ComputeWorker` class — lifecycle, job polling, dispatch | +| `src/discovery.ts` | `FunctionDiscovery` — lazy TTL-cached DB lookups | +| `src/invocation.ts` | `InvocationTracker` — create/complete/fail invocation records | +| `src/req.ts` | `compute_request()` — HTTP POST dispatch with X-* headers | +| `src/cache.ts` | `TtlCache` — generic TTL cache | +| `src/types.ts` | TypeScript interfaces | + +### job/compute-service (`@constructive-io/compute-service`) + +Orchestrator that starts the callback server, ComputeWorker, and Scheduler: + +| File | Purpose | +|------|---------| +| `src/index.ts` | `ComputeService` class + `bootCompute()` entry point | +| `src/run.ts` | CLI entry point (`node dist/run.js`) | +| `src/registry.ts` | Function registry loader (for optional in-process function servers) | +| `src/types.ts` | TypeScript interfaces | + +## Key differences from legacy worker + +| Feature | knative-job-worker | compute-worker | +|---------|-------------------|----------------| +| Function discovery | Static manifest / `JOBS_SUPPORTED` env | DB query (TTL-cached) | +| Invocation tracking | None | `platform_function_invocations` table | +| Task filtering | `JOBS_SUPPORTED` allowlist | Accepts any registered task | +| URL resolution | Gateway URL + dev map | `service_url` from DB → dev map → gateway fallback | +| Infra requirement | Only needs `app_jobs` schema | Needs `app_jobs` + `constructive_infra_public` | + +## Function discovery flow + +``` +Job arrives (task_identifier = "send-email") + │ + ▼ +FunctionDiscovery.resolve("send-email") + │ + ├─ Cache hit? → return cached definition + │ + └─ Cache miss? → SQL query: + SELECT * FROM constructive_infra_public.platform_function_definitions + WHERE task_identifier = 'send-email' + │ + └─ Cache result (TTL default: 60s) + │ + ▼ + PlatformFunctionDefinition { + id, name, task_identifier, service_url, + is_invocable, max_attempts, priority, ... + } +``` + +## Invocation lifecycle + +``` +1. create() → INSERT INTO platform_function_invocations + (status='running', started_at=now()) + → returns invocation_id + +2a. complete() → UPDATE SET status='completed', + completed_at=now(), duration_ms=X + +2b. fail() → UPDATE SET status='failed', + completed_at=now(), duration_ms=X, error='...' +``` + +## Running locally + +```bash +# Tier 1: pgpm-local +pgpm docker start --image docker.io/constructiveio/postgres-plus:18 +eval "$(pgpm env)" +make setup-platform # deploy infra + seed functions +make dev-compute # start compute-service + functions + +# Tier 2: compose-local +make dev # docker compose up +make dev-compute # start compute-service + functions +``` + +## Testing a job manually + +```bash +# Insert a test job +eval "$(pgpm env)" +psql -d constructive-functions-db1 -c " + INSERT INTO app_jobs.jobs (task_identifier, payload) + VALUES ('send-email', '{\"to\":\"test@example.com\",\"subject\":\"test\",\"html\":\"

hi

\"}'::json) +" + +# Check invocation records +psql -d constructive-functions-db1 -c " + SELECT id, task_identifier, status, duration_ms, error + FROM constructive_infra_public.platform_function_invocations + ORDER BY started_at DESC LIMIT 5 +" +``` + +## Environment variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `COMPUTE_JOBS_ENABLED` | `true` | Enable/disable the compute worker | +| `COMPUTE_CALLBACK_URL` | — | URL functions POST to on completion | +| `COMPUTE_GATEWAY_URL` | — | Fallback gateway URL for functions without `service_url` | +| `JOBS_SCHEMA` | `app_jobs` | PostgreSQL schema for the jobs table | +| `INTERNAL_JOBS_CALLBACK_PORT` | `8080` | Port for the HTTP callback server | +| `INTERNAL_GATEWAY_DEVELOPMENT_MAP` | — | JSON map of task→URL for local dev | + +## Database requirements + +The compute-service checks two things at boot: +1. `app_jobs.jobs` table exists (deployed by `@pgpm/database-jobs`, a dependency of `constructive-infra`) +2. `constructive_infra_public.platform_function_definitions` table exists (deployed by `constructive-infra`) + +Both are deployed together via `make setup-platform` or the `platform-setup` Docker Compose service. diff --git a/.agents/skills/dev-tiers/SKILL.md b/.agents/skills/dev-tiers/SKILL.md new file mode 100644 index 00000000..78bcc5ad --- /dev/null +++ b/.agents/skills/dev-tiers/SKILL.md @@ -0,0 +1,171 @@ +--- +name: dev-tiers +description: Three-tier local development environment for constructive-functions. Covers pgpm-local (Tier 1), compose-local (Tier 2), and k8s-local (Tier 3). Use when setting up the dev environment, starting services, or debugging infrastructure. +--- + +# Development Tiers + +constructive-functions supports three tiers of local development, each adding more infrastructure while keeping the same function code. Choose based on what you need. + +## Tier 1 — pgpm-local + +**What it is:** Postgres only via `pgpm docker`. Functions + services run as bare Node.js processes on the host. Fastest edit-run cycle. + +**When to use:** Day-to-day function development, quick iteration, debugging a single function. + +**Setup:** +```bash +# Start Postgres +pgpm docker start --image docker.io/constructiveio/postgres-plus:18 +eval "$(pgpm env)" + +# Deploy infra schema + seed function definitions +make setup-platform + +# Generate function packages (if not done) +pnpm generate && pnpm install && pnpm build + +# Start functions + compute-service (platform-aware) +make dev-compute + +# Or start functions + legacy job-service +make dev-fn +``` + +**What runs where:** +| Component | Where | +|-----------|-------| +| PostgreSQL | Docker container (via pgpm) | +| MinIO (optional) | Docker container (via pgpm) | +| Functions (send-email, etc.) | Local Node.js process | +| compute-service / job-service | Local Node.js process | +| GraphQL server | Not running (unless started manually) | + +**Database:** `constructive-functions-db1` (configurable via `DB_NAME` env var) + +**Ports:** +| Service | Port | +|---------|------| +| PostgreSQL | 5432 | +| compute-service / job-service | 8080 | +| send-email | 8081 | +| send-verification-link | 8082 | + +--- + +## Tier 2 — compose-local + +**What it is:** Docker Compose runs infrastructure (Postgres, db-setup, GraphQL server, mailpit, platform-setup). Functions still run as local Node.js processes. + +**When to use:** Testing with full infrastructure, working with email, needing GraphQL API, integration testing. + +**Setup:** +```bash +# Create .env from example +cp .env.example .env + +# Start infrastructure +make dev # docker compose up -d + +# Wait for db-setup + platform-setup to complete +docker compose logs -f db-setup platform-setup + +# Start functions + compute-service +make dev-compute + +# Or start functions + legacy job-service +make dev-fn +``` + +**What runs where:** +| Component | Where | +|-----------|-------| +| PostgreSQL | Docker container | +| db-setup (deploys constructive, metaschema) | Docker container (runs once) | +| platform-setup (deploys constructive-infra, seeds functions) | Docker container (runs once) | +| GraphQL server | Docker container (port 3002) | +| Mailpit (email testing) | Docker container (SMTP 1025, UI 8025) | +| Functions | Local Node.js process | +| compute-service / job-service | Local Node.js process | + +**Database:** `constructive` (full constructive stack) + +**Ports:** +| Service | Port | +|---------|------| +| PostgreSQL | 5432 | +| GraphQL server | 3002 | +| Mailpit SMTP | 1025 | +| Mailpit UI | 8025 | +| compute-service / job-service | 8080 | +| send-email | 8081 | +| send-verification-link | 8082 | + +--- + +## Tier 3 — k8s-local + +**What it is:** Everything runs in a local Kubernetes cluster via Skaffold. Two sub-profiles: `local-simple` (plain Deployments) and `local` (Knative Serving). + +**When to use:** Testing K8s manifests, verifying production-like behavior, testing Knative scaling, pre-deployment validation. + +**Setup (local-simple — no Knative):** +```bash +# One-time: install K8s tooling +make setup-dev + +# Start everything +make skaffold-dev +``` + +**Setup (local — Knative):** +```bash +# One-time: install Knative operators +cd k8s && make operators-knative-only + +# Start everything +make skaffold-dev-knative +``` + +**What runs where:** +| Component | Where | +|-----------|-------| +| Everything | Kubernetes pods | + +**Key manifests (local-simple overlay):** +- `k8s/overlays/local-simple/postgres-local.yaml` — PostgreSQL StatefulSet +- `k8s/overlays/local-simple/constructive-db-job.yaml` — DB setup Job +- `k8s/overlays/local-simple/job-service.yaml` — Legacy job service Deployment +- `k8s/overlays/local-simple/compute-service.yaml` — Platform-aware compute service Deployment +- `k8s/overlays/local-simple/constructive-server.yaml` — GraphQL server + +--- + +## Switching between tiers + +The tiers are independent. Tear down one before starting another to avoid port conflicts: + +```bash +# Stop Tier 2 +make dev-down + +# Stop Tier 3 +# Ctrl+C in the skaffold terminal + +# Stop Tier 1 +pgpm docker stop +``` + +## Environment variables + +Common env vars across all tiers: + +| Variable | Tier 1 Default | Tier 2 Default | Description | +|----------|---------------|---------------|-------------| +| `PGHOST` | localhost | localhost | PostgreSQL host | +| `PGPORT` | 5432 | 5432 | PostgreSQL port | +| `PGUSER` | postgres | postgres | PostgreSQL user | +| `PGPASSWORD` | (from pgpm env) | (from .env) | PostgreSQL password | +| `PGDATABASE` | constructive-functions-db1 | constructive | Database name | +| `JOBS_SCHEMA` | app_jobs | app_jobs | Jobs table schema | +| `COMPUTE_JOBS_ENABLED` | true | true | Enable compute worker | diff --git a/.agents/skills/fbp/SKILL.md b/.agents/skills/fbp/SKILL.md new file mode 100644 index 00000000..c021e436 --- /dev/null +++ b/.agents/skills/fbp/SKILL.md @@ -0,0 +1,97 @@ +--- +name: fbp +description: Flow-Based Programming integration for the Constructive Functions platform. Maps FBP NodeDefinitions to platform_function_definitions and handler.json, enabling visual flow graphs where each function is a node with typed input/output ports. +--- + +# FBP Integration Skill + +## When to Apply + +Use this skill when: +- Mapping platform functions to FBP node definitions +- Building or modifying the Flow Graph UI +- Connecting function outputs to function inputs via edges +- Working with the `@fbp/spec` or `@fbp/types` packages in this repo + +## Core Mapping + +Each `platform_function_definition` row (from `constructive_infra_public.platform_function_definitions`) maps to an FBP `NodeDefinition`: + +| Platform Field | FBP Field | Notes | +|---|---|---| +| `name` | `NodeDefinition.name` | e.g. `send-email` | +| `task_identifier` | `NodeDefinition.context` | e.g. `email:send_email` — acts as the dispatch key | +| `description` | `NodeDefinition.description` | Human-readable | +| `required_secrets` | `NodeDefinition.props[]` | Each secret becomes a `PropDef` with `required` flag | +| `required_configs` | `NodeDefinition.props[]` | Each config becomes a `PropDef` | +| `scope` | `NodeDefinition.category` | Groups nodes in the palette | + +### Port Model + +Functions have a single implicit input port (`payload`) and a single implicit output port (`result`): + +```typescript +// Every function node has these ports by default +inputs: [{ name: 'payload', type: 'json' }] +outputs: [{ name: 'result', type: 'json' }] +``` + +An edge from `functionA.result` to `functionB.payload` means: "the output of job A is piped as the input payload of job B." + +### handler.json → NodeDefinition + +The `handler.json` manifest enriches the definition: + +| handler.json Field | FBP Field | +|---|---| +| `name` | `NodeDefinition.name` | +| `version` | stored in `meta` | +| `type` | template type (not FBP type) | +| `port` | stored in `meta` (runtime detail) | +| `taskIdentifier` | `NodeDefinition.context` | +| `description` | `NodeDefinition.description` | +| `dependencies` | not mapped (build-time only) | + +## FBP Spec Quick Reference + +### Graph Structure + +```typescript +interface Graph { + name?: string; + nodes: Node[]; // Function instances + edges: Edge[]; // Data flow connections + definitions?: NodeDefinition[]; // Available function types +} +``` + +### Edge (connection between functions) + +```typescript +interface Edge { + src: { node: string; port: string }; // e.g. { node: 'send-email', port: 'result' } + dst: { node: string; port: string }; // e.g. { node: 'log-result', port: 'payload' } +} +``` + +### Node (function instance in a flow) + +```typescript +interface Node { + name: string; // Instance name (unique within flow) + type: string; // References a NodeDefinition name + meta?: { x?: number; y?: number; description?: string }; + props?: Array<{ name: string; value?: any }>; +} +``` + +## Persistence + +Flows are stored as JSON objects conforming to the `Graph` interface. Initial implementation uses `localStorage`; production will use a database table. + +## Source Packages + +- `@fbp/spec` — Storage types + manipulation API (from `constructive-io/fbp`) +- `@fbp/types` — Core TypeScript type definitions +- `@fbp/graph-editor` — React visual editor component +- `@fbp/evaluator` — Graph execution engine diff --git a/.env.example b/.env.example index 05eb2b0d..abb77ac5 100644 --- a/.env.example +++ b/.env.example @@ -8,14 +8,35 @@ POSTGRES_PASSWORD=changeme-set-a-strong-password # pgAdmin login password (only used by k8s overlays that deploy pgAdmin). PGADMIN_DEFAULT_PASSWORD=changeme-set-a-strong-password -# MinIO root credentials (S3-compatible local storage). -MINIO_ROOT_USER=changeme-set-a-username -MINIO_ROOT_PASSWORD=changeme-set-a-strong-password +# MinIO / S3-compatible object storage (used by constructive-storage module). +# Local dev uses MinIO; production uses real S3 or compatible. +MINIO_ROOT_USER=minioadmin +MINIO_ROOT_PASSWORD=minioadmin +MINIO_ENDPOINT=http://localhost:9000 +AWS_ACCESS_KEY=minioadmin +AWS_SECRET_KEY=minioadmin +AWS_REGION=us-east-1 -# Mailgun (only required if EMAIL_SEND_USE_SMTP is false in dev). +# --- Email: Mailgun (production) --- +# Only required if EMAIL_SEND_USE_SMTP is false. MAILGUN_API_KEY= MAILGUN_KEY= MAILGUN_DOMAIN= +MAILGUN_FROM=no-reply@mg.constructive.io +MAILGUN_REPLY=info@mg.constructive.io + +# --- Email: SMTP / Mailpit (local dev) --- +# Set EMAIL_SEND_USE_SMTP=true to use Mailpit instead of Mailgun. +# Then view emails at http://localhost:8025 +EMAIL_SEND_USE_SMTP=true +SMTP_HOST=localhost +SMTP_PORT=1025 +SMTP_FROM=test@localhost + +# --- Function dry-run toggles --- +# Set to false to actually send emails (requires SMTP or Mailgun config above). +SEND_EMAIL_DRY_RUN=false +SEND_VERIFICATION_LINK_DRY_RUN=false # AWS Route53 access key ID for cert-manager DNS01 challenges (per-environment). ROUTE53_ACCESS_KEY_ID= diff --git a/.github/workflows/pgpm-test.yaml b/.github/workflows/pgpm-test.yaml new file mode 100644 index 00000000..fe9c709e --- /dev/null +++ b/.github/workflows/pgpm-test.yaml @@ -0,0 +1,67 @@ +name: pgpm integration tests + +on: + push: + branches: [main] + paths: + - 'pgpm/**' + - 'pgpm.json' + - '.github/workflows/pgpm-test.yaml' + pull_request: + branches: [main] + paths: + - 'pgpm/**' + - 'pgpm.json' + - '.github/workflows/pgpm-test.yaml' + workflow_dispatch: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + pgpm-test: + name: pgpm deploy test + runs-on: ubuntu-latest + + env: + PGHOST: localhost + PGPORT: 5432 + PGUSER: postgres + PGPASSWORD: password + + services: + pg_db: + image: constructiveio/postgres-plus:18 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: '22' + cache: 'pnpm' + + - name: Install pgpm CLI + run: npm install -g pgpm + + - name: Seed pg users + run: pgpm admin-users bootstrap --yes + + - name: Run pgpm full-cycle test (deploy/verify/revert/deploy) + run: pgpm test-packages --full-cycle --exclude constructive-infra-services diff --git a/Makefile b/Makefile index 95b32482..6204695e 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,10 @@ -.PHONY: install build clean lint generate dev dev-fn dev-down dev-logs docker-build skaffold-dev skaffold-dev-knative +.PHONY: install build clean lint generate register \ + up down status verify-platform check-env setup-platform \ + up\:email-job down\:email-job \ + up\:www \ + dev dev-fn dev-compute dev-down dev-logs setup-dev setup-check \ + secrets\:sync \ + skaffold-dev skaffold-dev-knative docker-build install: node --experimental-strip-types scripts/generate.ts @@ -16,9 +22,69 @@ lint: generate: pnpm run generate -# --- Local development --- +register: + node --experimental-strip-types scripts/register-functions.ts + +# ═══════════════════════════════════════════════════════════════════════════════ +# Lifecycle — up / down +# ═══════════════════════════════════════════════════════════════════════════════ +# +# Full procedural setup and teardown. Idempotent — safe to run repeatedly. +# +# make up # postgres + deploy infra + seed + verify +# make up DB_NAME=mydb # same, custom DB name +# make up:email-job # add mailpit + compute-service (SMTP mode) +# make down:email-job # stop mailpit + compute-service +# make down # stop everything (postgres, compose, etc.) +# DROP=1 make down DB_NAME=mydb # also drop the database + +up: + ./scripts/up.sh $(DB_NAME) + +down: + ./scripts/down.sh $(DB_NAME) + +up\:email-job: + ./scripts/email-job-up.sh $(DB_NAME) + +down\:email-job: + ./scripts/email-job-down.sh + +up\:www: + ./scripts/www-up.sh $(DB_NAME) + +# ═══════════════════════════════════════════════════════════════════════════════ +# Tier 1 — pgpm-local +# ═══════════════════════════════════════════════════════════════════════════════ +# Postgres only (via pgpm docker). Functions + services run as local Node +# processes. Fastest edit-run cycle. +# +# Quick start: +# make up # full setup +# make up:email-job # start mailpit + compute-service + +setup-platform: + ./scripts/setup-platform-db.sh + +status: + @./scripts/status.sh + +verify-platform: + @./scripts/verify-platform.sh $(DB_NAME) + +check-env: + @./scripts/load-platform-env.sh $(ENV_FILE) $(DB_NAME) + +# ═══════════════════════════════════════════════════════════════════════════════ +# Tier 2 — compose-local +# ═══════════════════════════════════════════════════════════════════════════════ # Infrastructure (postgres, db-setup, graphql-server, mailpit) runs in Docker. # Functions run as local Node processes for fast edit-run cycles. +# +# Quick start: +# make dev # docker compose up -d +# make dev-fn # start existing job-service + functions +# make dev-compute # or start compute-service + functions dev: docker compose up -d @@ -26,6 +92,9 @@ dev: dev-fn: node --experimental-strip-types scripts/dev.ts +dev-compute: + node --experimental-strip-types scripts/dev-compute.ts + dev-down: docker compose down @@ -39,7 +108,18 @@ setup-dev: setup-check: ./scripts/setup-dev.sh --check -# --- Skaffold k8s development --- +secrets\:sync: + ./scripts/secrets-sync.sh $(ENV_FILE) $(DB_NAME) + +# ═══════════════════════════════════════════════════════════════════════════════ +# Tier 3 — k8s-local +# ═══════════════════════════════════════════════════════════════════════════════ +# Everything runs in a local Kubernetes cluster via Skaffold. +# +# Quick start: +# make skaffold-dev # plain k8s (no Knative needed) +# make skaffold-dev-knative # full Knative setup + # Plain k8s (Deployments + Services, no Knative operators needed) skaffold-dev: skaffold dev -p local-simple diff --git a/docker-compose.yml b/docker-compose.yml index 0717362c..cb306972 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,7 +56,37 @@ services: pgpm deploy --yes --database "$$PGDATABASE" --package metaschema pgpm deploy --yes --database "$$PGDATABASE" --package pgpm-database-jobs - echo "Done" + echo "Done (core packages)" + + # Deploy the local constructive-infra package and seed function definitions. + # This runs after db-setup and mounts the local pgpm/ and scripts/ dirs. + platform-setup: + image: ghcr.io/constructive-io/constructive:latest + depends_on: + db-setup: + condition: service_completed_successfully + environment: + PGHOST: postgres + PGPORT: "5432" + PGUSER: postgres + PGPASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set (see .env.example)} + PGDATABASE: constructive + volumes: + - ./pgpm:/workspace/pgpm:ro + - ./extensions:/workspace/extensions:ro + entrypoint: ["bash", "-c"] + command: + - | + cd /workspace/pgpm + for mod in constructive-infra constructive-store constructive-objects constructive-fbp constructive-storage; do + if [ -d "$$mod" ]; then + echo "Deploying $$mod..." + pgpm deploy --yes --database "$$PGDATABASE" --package "$$mod" + fi + done + echo "Deploying constructive-infra-seed..." + pgpm deploy --yes --database "$$PGDATABASE" --package constructive-infra-seed + echo "Platform setup done" graphql-server: image: ghcr.io/constructive-io/constructive:latest @@ -87,6 +117,23 @@ services: ports: - "3002:3000" + minio: + image: minio/minio:latest + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + ports: + - "9000:9000" # S3 API + - "9001:9001" # Console UI + volumes: + - minio-data:/data + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 5s + timeout: 5s + retries: 5 + mailpit: image: axllent/mailpit:latest ports: @@ -95,3 +142,4 @@ services: volumes: pgdata: + minio-data: diff --git a/docs/spec/fbp-data-model.md b/docs/spec/fbp-data-model.md new file mode 100644 index 00000000..d57ec1cb --- /dev/null +++ b/docs/spec/fbp-data-model.md @@ -0,0 +1,458 @@ +# FBP Data Model & State Management + +## 1. The Canonical Graph Format + +Both `@fbp/types` (TypeScript) and `constructive-db` (SQL `import_graph_json` / `read_function_graph`) use **the same JSON structure**. Here's a complete annotated example: + +```jsonc +{ + "name": "mixed-native-def", // graph name (human-readable key) + "context": "function", // namespace scope (always "function" for platform functions) + + // Optional: reusable node definitions (digital assets) — sub-graphs that can be + // referenced by type name from any node. Think of them as "macros" or "components". + "definitions": [ + { + "name": "double", // referenced via node.type = "double" + "context": "function", + "category": "math", + "inputs": [{ "name": "x", "type": "number" }], + "outputs": [{ "name": "result", "type": "number" }], + "graph": { // embedded sub-graph (recursive) + "name": "double-internal", + "context": "function", + "nodes": [ + { "name": "input_x", "type": "graphInput", "props": [{ "name": "portName", "value": "x" }] }, + { "name": "mul", "type": "double_impl" }, + { "name": "output_result", "type": "graphOutput", "props": [{ "name": "portName", "value": "result" }] } + ], + "edges": [ + { "src": { "node": "input_x", "port": "value" }, "dst": { "node": "mul", "port": "x" } }, + { "src": { "node": "mul", "port": "result" }, "dst": { "node": "output_result", "port": "value" } } + ] + } + } + ], + + // Top-level graph + "nodes": [ + // Boundary nodes — typed entry/exit points + { + "name": "input_x", + "type": "graphInput", // magic type: becomes an external input port + "props": [ + { "name": "portName", "value": "x" }, // external port name + { "name": "dataType", "value": "number" } // optional type hint + ] + }, + { + "name": "input_y", + "type": "graphInput", + "props": [ + { "name": "portName", "value": "y" }, + { "name": "dataType", "value": "number" } + ] + }, + { + "name": "prop_scale", + "type": "graphProp", // magic type: configurable parameter (not a data port) + "props": [ + { "name": "propName", "value": "scale" }, + { "name": "dataType", "value": "number" }, + { "name": "default", "value": 1.5 } + ] + }, + + // Compute nodes + { "name": "my_double", "type": "double" }, // resolved from definitions[] + { "name": "adder", "type": "add" }, // resolved from registered node types + + // Output boundary + { + "name": "output_result", + "type": "graphOutput", + "props": [ + { "name": "portName", "value": "result" }, + { "name": "dataType", "value": "number" } + ] + } + ], + + "edges": [ + { "src": { "node": "input_x", "port": "value" }, "dst": { "node": "my_double", "port": "x" } }, + { "src": { "node": "my_double", "port": "result" }, "dst": { "node": "adder", "port": "a" } }, + { "src": { "node": "input_y", "port": "value" }, "dst": { "node": "adder", "port": "b" } }, + { "src": { "node": "adder", "port": "result" }, "dst": { "node": "output_result", "port": "value" } } + ] +} +``` + +### Boundary Node Conventions + +| Type | Purpose | Key Props | Port | +|---------------|--------------------------------|-------------------------------|--------------| +| `graphInput` | External input → internal port | `portName`, `dataType` | output `value` | +| `graphOutput` | Internal port → external output| `portName`, `dataType` | input `value` | +| `graphProp` | Configurable parameter | `propName`, `dataType`, `default` | output `value` | + +### Subnet Nodes (Inline) + +A node can contain `nodes[]` + `edges[]` directly (inline subnet): + +```jsonc +{ + "name": "processor", + "type": "subnet", // or any type — presence of nodes[] makes it a subnet + "nodes": [ + { "name": "input_val", "type": "graphInput", "props": [{ "name": "portName", "value": "val" }] }, + { "name": "double", "type": "multiply", "props": [{ "name": "b", "value": 2 }] }, + { "name": "output_result", "type": "graphOutput", "props": [{ "name": "portName", "value": "result" }] } + ], + "edges": [ + { "src": { "node": "input_val", "port": "value" }, "dst": { "node": "double", "port": "a" } }, + { "src": { "node": "double", "port": "result" }, "dst": { "node": "output_result", "port": "value" } } + ] +} +``` + +External edges wire to the subnet's boundary ports: +```json +{ "src": { "node": "input_x", "port": "value" }, "dst": { "node": "processor", "port": "val" } } +``` + +--- + +## 2. Type System Alignment + +### @fbp/types (TypeScript) + +```typescript +interface Graph { + name: string; + context?: string; + definitions?: NodeDefinition[]; + inputs?: Port[]; // graph-level input declarations (optional) + outputs?: Port[]; // graph-level output declarations (optional) + props?: PropDefinition[]; // graph-level prop declarations (optional) + nodes: Node[]; + edges: Edge[]; + groups?: Group[]; + meta?: Metadata; // { x, y, description, ... } +} + +interface Node { + name: string; // unique within scope + type: string; // references a NodeDefinition or boundary type + context?: string; + meta?: Metadata; // position { x, y } + props?: Prop[]; // instance config + inputs?: Port[]; // override definition ports (optional) + outputs?: Port[]; + nodes?: Node[]; // inline subnet children + edges?: Edge[]; // inline subnet edges +} + +interface Edge { + src: { node: string; port: string }; + dst: { node: string; port: string }; +} + +interface NodeDefinition { + context: string; + name: string; + category?: string; + inputs?: Port[]; + outputs?: Port[]; + props?: PropDefinition[]; + graph?: Graph; // embedded sub-graph (digital asset) + icon?: string; +} +``` + +### constructive-db (SQL/Merkle) + +The DB stores the same shape via `import_graph_json(scope_id, name, graph_json::jsonb)` and returns it via `read_function_graph(graph_id)`: + +``` +read_function_graph returns: +{ + name, context, + nodes: [{ name, type, meta?, props? }], + edges: [{ src: {node, port}, dst: {node, port} }], + definitions: [{ name, context, category, inputs, outputs, graph: {...} }] +} +``` + +**Key:** The JSON format is identical. No transform needed between client and DB. + +--- + +## 3. Merkle Tree Storage + +The DB doesn't store graphs as flat JSON blobs. It decomposes them into a content-addressed Merkle tree: + +``` + ┌─ root tree_id ─┐ + │ │ + function/ │ + ├── graphs/ │ + │ └── my-graph/ │ + │ ├── nodes/ │ + │ │ ├── input_a │ ← each is an object { type, props, meta } + │ │ ├── add1 │ + │ │ └── output │ + │ └── edges/ │ + │ ├── 0 │ ← { src: {node, port}, dst: {node, port} } + │ └── 1 │ + └── definitions/ │ + └── double │ ← { name, context, inputs, outputs, graph: {...} } +``` + +- Every node in the tree is content-addressed (UUID v5 from `sha256(data)`) +- Parent hashes change when children change → Merkle proof of integrity +- Deduplication is free (identical data → same hash → same row) +- Git-like commits: `graph_store` → `graph_ref` (branch "main") → `graph_commit` → `tree_id` + +### Operations + +| Function | Description | +|----------|-------------| +| `import_graph_json(scope_id, name, json)` | Bulk load from canonical JSON → Merkle tree | +| `read_function_graph(graph_id)` | Reconstitute Merkle tree → canonical JSON | +| `serialize_graph(graph_id)` | Export raw tree (path→data pairs) | +| `deserialize_graph(database_id, name, snapshot)` | Restore from raw tree snapshot | +| `add_node_and_save(graph_id, ...)` | Insert node + auto-commit | +| `add_edge_and_save(graph_id, ...)` | Insert edge + auto-commit | +| `add_node(scope_id, root_hash, ...)` | Insert node (no commit, returns new root) | +| `add_edge(scope_id, root_hash, ...)` | Insert edge (no commit, returns new root) | +| `save_graph(graph_id, root_hash, msg)` | Commit a root hash | +| `validate_function_graph(graph_id)` | Cycle detection, dangling edges, missing output | +| `copy_graph(graph_id)` | Clone a graph (new store) | +| `import_definitions(target, source_scope, source_commit, context_path)` | Copy definitions between graphs | + +--- + +## 4. Execution Model + +### Lifecycle + +``` +start_execution(graph_id, input_payload, output_node) + │ + ├── Resolve graphInput nodes → pre-populate node_outputs with input values + ├── Resolve graphProp nodes → pre-populate with defaults or overrides + ├── Create execution row (status='running') + └── tick_execution(exec_id) + │ + ├── For each node not yet computed: + │ ├── Check all incoming edges — are source nodes in node_outputs? + │ │ No → skip (not ready) + │ │ Yes → gather inputs from source output objects + │ │ + │ ├── If type == 'graphOutput': + │ │ Store inputs as output, mark node complete + │ │ If this is the designated output_node → mark execution 'completed' + │ │ + │ ├── If type has a definition with embedded graph: + │ │ import_graph_json(def.graph) → start_execution(sub_graph, inputs, + │ │ parent_execution_id=exec_id, parent_node_name=node_name) + │ │ When sub-execution completes → complete_node(parent, node, outputs) + │ │ + │ └── Otherwise (native/external node): + │ INSERT INTO app_jobs.jobs with task_identifier = 'fbp:eval:{context}:{type}' + │ payload = { execution_id, node_name, node_type, inputs } + │ + └── Safety guards: + tick_count >= max_ticks → fail TICK_LIMIT_EXCEEDED + now() >= timeout_at → fail EXECUTION_TIMEOUT + pending_jobs >= max → fail JOB_LIMIT_EXCEEDED +``` + +### Job Payload (what the worker receives) + +```json +{ + "execution_id": "uuid", + "node_name": "add1", + "node_type": "add", + "inputs": { "a": 5, "b": 3 } +} +``` + +Task identifier: `fbp:eval:function:add` + +### Completing a Node + +After a worker processes a job, it calls: +```sql +SELECT complete_node(execution_id, node_name, '{"result": 8}'::jsonb) +``` + +This: +1. Content-addresses the output data (sha256 → execution_outputs table) +2. Adds `node_name → output_id` to `execution.node_outputs` +3. Calls `tick_execution` again → may enqueue more jobs + +### Execution State + +```sql +function_graph_executions: + id, graph_id, database_id, + output_node, output_port, + status: 'running' | 'completed' | 'failed', + input_payload: jsonb, + output_payload: jsonb, -- final result + node_outputs: jsonb, -- { node_name: output_object_id, ... } + execution_plan: jsonb, + current_wave: int, + tick_count: int, + max_ticks: int, -- default 100 + max_pending_jobs: int, -- default 50 + timeout_at: timestamptz, -- default now() + 5 min + parent_execution_id: uuid, -- for sub-graph executions + parent_node_name: text, + error_code, error_message +``` + +--- + +## 5. Two Execution Engines — When to Use Which + +| | @fbp/evaluator (client) | constructive-db graph_module (server) | +|---|---|---| +| **Where** | Browser / Node.js | PostgreSQL | +| **How** | Lazy pull from output node | Wave-based push from inputs | +| **Async** | Awaits each node impl | Enqueues jobs → workers call complete_node | +| **Subnets** | Recursive evaluate() | Spawns sub-execution with parent tracking | +| **Definitions** | Runtime lookup in definitions[] | Merkle tree lookup at `{context}/definitions/{type}` | +| **State** | In-memory cache (Map) | `function_graph_executions.node_outputs` (content-addressed) | +| **Best for** | Preview, prototyping, pure-function flows | Production, side-effects, long-running, auditable | + +### Client Evaluation (Phase 1 — current) + +```typescript +import { evaluate } from '@fbp/evaluator'; + +const result = await evaluate(graph, { + definitions: [...builtinDefs, ...platformFnDefs], + outputNode: 'output_result', + outputPort: 'value', + inputs: { x: 7 }, + props: { scale: 2 } +}); +``` + +- Definitions need an `impl` function (not serializable — runtime only) +- Pure functions work perfectly; side-effect functions use stubs + +### Server Evaluation (Phase 2) + +```sql +-- Store graph +SELECT import_graph_json(database_id, 'my-flow', graph_json); +-- Execute +SELECT start_execution(graph_id, '{"x": 7}'::jsonb); +-- Workers process jobs, call complete_node +-- Final result in function_graph_executions.output_payload +``` + +--- + +## 6. State Management Design for the UI + +### Layers + +``` +┌─────────────────────────────────────────┐ +│ @fbp/graph-editor (or React Flow) │ Rendering + interaction +│ State: reducer-managed Graph object │ +├─────────────────────────────────────────┤ +│ Graph store (React state + persistence) │ Source of truth +│ - localStorage (Phase 1) │ +│ - import_graph_json / serialize │ +│ (Phase 2, via API) │ +├─────────────────────────────────────────┤ +│ Evaluation engine │ +│ - @fbp/evaluator (Phase 1, client) │ +│ - start_execution (Phase 2, server) │ +├─────────────────────────────────────────┤ +│ Definition registry │ +│ - Built-in: math, core, ui, net defs │ +│ - Platform: GET /api/functions → defs │ +│ - Graph-local: graph.definitions[] │ +│ - Shared library (Phase 2): │ +│ import_definitions between graphs │ +└─────────────────────────────────────────┘ +``` + +### The Graph object IS the state + +The canonical `Graph` JSON from `@fbp/types` is both the in-memory state and the serialization format. No transform needed: + +``` +UI edit → dispatch reducer action → new Graph → save to localStorage +Load → read localStorage → Graph → pass to +``` + +Phase 2 adds: +``` +Save → POST /api/graphs { graph_json } → import_graph_json(...) +Load → GET /api/graphs/:id → read_function_graph(id) → Graph +``` + +### Definition Resolution + +Platform functions → NodeDefinition[] mapping: + +```typescript +function platformFnToDefinition(fn: PlatformFunction): NodeDefinition { + return { + context: fn.scope || 'platform', + name: fn.task_identifier || fn.name, + category: 'functions', + description: fn.description, + // Phase 1: generic ports + inputs: [{ name: 'payload', type: 'json' }], + outputs: [{ name: 'result', type: 'json' }], + // Phase 2: typed ports from handler.json + // inputs: fn.handler_schema?.inputs ?? [...], + // outputs: fn.handler_schema?.outputs ?? [...], + }; +} +``` + +For execution, add `impl`: +```typescript +const defWithImpl: NodeDefinitionWithImpl = { + ...definition, + impl: async (inputs) => { + // Phase 1: stub + return { result: inputs.payload ?? inputs }; + // Phase 2: POST /api/jobs → poll for result + } +}; +``` + +--- + +## 7. Summary: What Matches, What's Missing + +### Already aligned (no work needed) +- Graph JSON format: `@fbp/types.Graph` ≡ `import_graph_json` input ≡ `read_function_graph` output +- Boundary nodes: `graphInput`, `graphOutput`, `graphProp` — same semantics everywhere +- Edge format: `{ src: { node, port }, dst: { node, port } }` — identical +- Definitions: `NodeDefinition` with embedded `graph` — same shape +- Subnet: `Node.nodes[]` + `Node.edges[]` — same pattern + +### Phase 1 (current — client-side) +- [x] Graph state = `@fbp/types.Graph` in React state +- [x] Persistence = localStorage +- [x] Evaluation = `@fbp/evaluator` with stub impls +- [x] Definitions = built-in + platform functions (generic ports) + +### Phase 2 (server-side) +- [ ] Persistence = `import_graph_json` / `read_function_graph` via API +- [ ] Evaluation = `start_execution` / `tick_execution` / `complete_node` +- [ ] Typed ports from `handler.json` → `inputs[]/outputs[]` on NodeDefinition +- [ ] Definition library (shared across graphs via `import_definitions`) +- [ ] Execution status tracking in UI (poll `function_graph_executions`) diff --git a/docs/spec/fbp-deep-plan.md b/docs/spec/fbp-deep-plan.md new file mode 100644 index 00000000..ae692c32 --- /dev/null +++ b/docs/spec/fbp-deep-plan.md @@ -0,0 +1,646 @@ +# FBP Integration Deep Plan + +Full feature port from `@fbp/graph-editor` into `constructive-functions`, backed by `constructive-db` merkle storage + graph execution engine. + +--- + +## Table of Contents + +1. [Current State](#1-current-state) +2. [Target Architecture](#2-target-architecture) +3. [Feature Matrix](#3-feature-matrix) +4. [Schema & Port Strategy](#4-schema--port-strategy) +5. [DB Integration: Merkle Store + Graph Module](#5-db-integration-merkle-store--graph-module) +6. [UI Component Architecture](#6-ui-component-architecture) +7. [State Management](#7-state-management) +8. [Execution Model](#8-execution-model) +9. [Implementation Phases](#9-implementation-phases) +10. [Open Questions](#10-open-questions) + +--- + +## 1. Current State + +### What exists today (PR #53, merged) + +- **FlowsPanel.tsx**: React Flow (`@xyflow/react`) canvas with: + - Custom `FunctionNode` component (name, description, scope badge, secrets/configs count) + - Drag-and-drop from sidebar palette + - Animated Bezier edges between generic `payload` → `result` ports + - localStorage persistence keyed by `'constructive-flows'` + - Minimap, flow selector sidebar +- **FunctionsPanel.tsx**: Inline "Trigger" button with raw JSON payload editor +- **No subnets, no selections, no hotkeys, no context/cwd, no boundary nodes** + +### What exists in @fbp/graph-editor + +SVG-based editor with full Houdini-inspired graph editing (see [Feature Matrix](#3-feature-matrix)). + +### What exists in constructive-db + +`merkle_store_module` + `graph_module` generators producing tables + 20+ SQL functions for content-addressed graph storage and wave-based execution. + +--- + +## 2. Target Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ Browser │ +│ ┌───────────────────────────────────────────────────┐ │ +│ │ @fbp/graph-editor (SVG Canvas) │ │ +│ │ ├─ GraphCanvas (pan/zoom, box select, connect) │ │ +│ │ ├─ GraphNode (ports, drag, multi-select) │ │ +│ │ ├─ GraphEdge (Bezier, selection) │ │ +│ │ ├─ Toolbar (palette, hotkeys) │ │ +│ │ ├─ PropertiesPanel (props, evaluate, rename) │ │ +│ │ └─ StatusBar (counts, breadcrumb, cwd) │ │ +│ └───────────────┬───────────────────────────────────┘ │ +│ │ onChange(graph: Graph) │ +│ ┌───────────────▼───────────────────────────────────┐ │ +│ │ GraphBridge (adapter layer) │ │ +│ │ ├─ Syncs @fbp/types Graph ↔ REST API │ +│ │ ├─ Maps NodeDefinitions from /api/functions │ │ +│ │ └─ Debounced save via API │ │ +│ └───────────────┬───────────────────────────────────┘ │ +└──────────────────┼──────────────────────────────────────┘ + │ HTTP/JSON +┌──────────────────▼──────────────────────────────────────┐ +│ Express Server (www/server/index.ts) │ +│ ┌───────────────────────────────────────────────────┐ │ +│ │ Graph API Routes │ │ +│ │ POST /api/graphs → create_function_graph│ │ +│ │ GET /api/graphs/:id → serialize_graph │ │ +│ │ PUT /api/graphs/:id → import_graph_json │ │ +│ │ POST /api/graphs/:id/exec → start_execution │ │ +│ │ GET /api/graphs/:id/exec → list executions │ │ +│ │ GET /api/definitions → import_definitions │ │ +│ └───────────────┬───────────────────────────────────┘ │ +└──────────────────┼──────────────────────────────────────┘ + │ SQL +┌──────────────────▼──────────────────────────────────────┐ +│ PostgreSQL (provisioned by make up) │ +│ ┌────────────────────┐ ┌────────────────────────────┐ │ +│ │ merkle_store_module│ │ graph_module │ │ +│ │ object, store, │ │ function_graphs, │ │ +│ │ commit, ref │ │ function_graph_executions,│ │ +│ │ │ │ function_graph_exec_outputs│ │ +│ └────────────────────┘ └────────────────────────────┘ │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ platform_function_definitions (existing) │ │ +│ │ app_jobs.jobs (existing) │ │ +│ └────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +**Key decision: adopt `@fbp/graph-editor` as a dependency, don't rewrite it.** + +The editor already implements the full feature set (subnets, collapse, selections, hotkeys, context). The constructive-functions UI wraps it with a bridge that: +1. Loads `NodeDefinition[]` from the platform function registry +2. Persists the `Graph` object to the DB via graph_module SQL functions +3. Triggers execution via `start_execution` + +--- + +## 3. Feature Matrix + +### Canvas & Navigation + +| Feature | @fbp/graph-editor | Current FlowsPanel | Plan | +|---------|-------------------|---------------------|------| +| SVG canvas with dot grid | Yes | No (React Flow) | Adopt editor | +| Pan: Alt+Drag / Middle Mouse / Space+Drag | Yes | React Flow default | Adopt editor | +| Zoom: Ctrl/Cmd+Scroll | Yes | React Flow default | Adopt editor | +| Per-scope state (view + selection per cwd) | Yes (`stateByPath: Map`) | No | Adopt editor | +| Breadcrumb navigation (cwd path) | Yes (StatusBar) | No | Adopt editor | + +### Selection System + +| Feature | @fbp/graph-editor | Current FlowsPanel | Plan | +|---------|-------------------|---------------------|------| +| Click to select node | Yes | Yes (React Flow) | Adopt editor | +| Shift+Click add/remove selection | Yes | Partial | Adopt editor | +| Shift+Drag box select (marquee) | Yes | No | Adopt editor | +| Space+Drag to move marquee | Yes (`MOVE_BOX_SELECT`) | No | Adopt editor | +| Cmd/Ctrl+A select all | Yes | No | Adopt editor | +| Escape clear selection | Yes | No | Adopt editor | +| Edge selection (click) | Yes (`selectEdges`) | No | Adopt editor | +| Multi-node drag | Yes | Yes (React Flow) | Adopt editor | + +### Node & Edge Operations + +| Feature | @fbp/graph-editor | Current FlowsPanel | Plan | +|---------|-------------------|---------------------|------| +| Delete selected (Del/Backspace) | Yes | No | Adopt editor | +| Duplicate selection (Cmd/Ctrl+D) | Yes | No | Adopt editor | +| Copy selection (Cmd/Ctrl+C) | Yes | No | Adopt editor | +| Paste (Cmd/Ctrl+V) | Yes | No | Adopt editor | +| Rename node (inline edit) | Yes (`RENAME_NODE`) | No | Adopt editor | +| Drag from port to connect | Yes | Yes (React Flow) | Adopt editor | +| Temp edge line while connecting | Yes (`TempEdge`) | Yes | Adopt editor | +| Port type validation on connect | No (any→any) | No | Phase 2 | +| Auto-layout selection (L key) | Yes (`LAYOUT_SELECTION`) | No | Adopt editor | + +### Subnets + +| Feature | @fbp/graph-editor | Current FlowsPanel | Plan | +|---------|-------------------|---------------------|------| +| Dive into subnet (Enter) | Yes (`DIVE_INTO`) | No | Adopt editor | +| Go up from subnet (U) | Yes (`GO_UP`) | No | Adopt editor | +| Collapse selection to subnet (Shift+C) | Yes (`COLLAPSE_SELECTION`) | No | Adopt editor | +| Boundary nodes: graphInput | Yes | No | Adopt editor | +| Boundary nodes: graphOutput | Yes | No | Adopt editor | +| Boundary nodes: graphProp | Yes | No | Adopt editor | +| Derive ports from boundary nodes | Yes (`deriveBoundaryPorts`) | No | Adopt editor | +| Subnet node renders child ports | Yes (`GraphNode`) | No | Adopt editor | + +### Properties & Evaluation + +| Feature | @fbp/graph-editor | Current FlowsPanel | Plan | +|---------|-------------------|---------------------|------| +| Properties panel (auto-generated) | Yes | No | Adopt editor | +| Inline evaluate button | Yes (calls `evaluateFn`) | No | Phase 2 | +| Code editor for string props | Yes (`CodeEditor`) | No | Adopt editor | +| Channel reference detection | Yes (`ch("...")`, `$VAR`) | No | Phase 2 | + +### Palette & Toolbar + +| Feature | @fbp/graph-editor | Current FlowsPanel | Plan | +|---------|-------------------|---------------------|------| +| Node palette (grouped by category) | Yes | Yes (sidebar) | Adopt editor | +| Drag from palette to canvas | Yes (dataTransfer) | Yes | Adopt editor | +| Click to add from palette | Yes | No | Adopt editor | +| Boundary node palette items | Yes (graph/input, output, prop) | No | Adopt editor | +| Keyboard shortcut help panel | Yes (collapsible) | No | Adopt editor | + +### Complete Hotkey Map + +| Key | Action | Reducer Action | +|-----|--------|----------------| +| `Delete` / `Backspace` | Delete selected nodes/edges | `DELETE_NODES` + `DELETE_EDGES` | +| `Cmd/Ctrl+D` | Duplicate selection | `DUPLICATE_SELECTION` | +| `Cmd/Ctrl+C` | Copy selection | `COPY_SELECTION` | +| `Cmd/Ctrl+V` | Paste clipboard | `PASTE_SELECTION` | +| `Cmd/Ctrl+A` | Select all | `SELECT_ALL` | +| `Escape` | Clear selection / cancel connect | `CLEAR_SELECTION` / `CANCEL_CONNECTING` | +| `Enter` | Dive into selected subnet | `DIVE_INTO` | +| `U` | Go up from subnet | `GO_UP` | +| `Shift+C` | Collapse selection to subnet | `COLLAPSE_SELECTION` | +| `L` | Auto-layout selection | `LAYOUT_SELECTION` | +| `Shift+Drag` | Box select (marquee) | `START/UPDATE/END_BOX_SELECT` | +| `Space+Drag` | Move marquee while selecting | `MOVE_BOX_SELECT` | +| `Alt+Drag` / `Middle Mouse` | Pan canvas | `SET_VIEW` | +| `Space+Click+Drag` | Pan canvas | `SET_VIEW` | +| `Ctrl/Cmd+Scroll` | Zoom | `SET_VIEW` | + +--- + +## 4. Schema & Port Strategy + +### Current: Generic Ports + +Every function has implicit ports: +``` +input: [{ name: "payload", type: "json" }] +output: [{ name: "result", type: "json" }] +``` + +### Proposed: Typed Ports via handler.json Extension + +Extend `handler.json` with explicit port definitions: + +```json +{ + "name": "send-email", + "version": "1.0.0", + "type": "node-graphql", + "inputs": [ + { "name": "to", "type": "string", "schema": { "format": "email" } }, + { "name": "subject", "type": "string" }, + { "name": "html", "type": "string" }, + { "name": "from", "type": "string", "schema": { "format": "email" }, "default": "noreply@example.com" } + ], + "outputs": [ + { "name": "result", "type": "json", "schema": { "properties": { "complete": { "type": "boolean" } } } } + ] +} +``` + +### Port ↔ @fbp/types Mapping + +```typescript +// handler.json port → @fbp/types PortDef +interface PortDef { + name: string; + type?: string; // "string", "number", "json", "boolean" + schema?: Record; // JSON Schema for validation + multi?: boolean; // accept multiple incoming edges + description?: string; +} + +// handler.json → NodeDefinition +function handlerToDefinition(handler: HandlerJson): NodeDefinition { + return { + context: 'function', // runtime context + name: handler.name, // "send-email" + category: handler.scope || 'platform', // grouping + inputs: handler.inputs || [{ name: 'payload', type: 'json' }], + outputs: handler.outputs || [{ name: 'result', type: 'json' }], + props: [ + ...handler.required_secrets?.map(s => ({ name: s.name, type: 'secret' })) || [], + ...handler.required_configs?.map(c => ({ name: c.name, type: 'config' })) || [] + ] + }; +} +``` + +### Migration Path + +1. Functions without `inputs`/`outputs` in handler.json keep the generic `payload`/`result` ports +2. Functions with explicit ports get per-field port rendering in the editor +3. Port `schema` fields enable edge-level type validation (Phase 2) +4. The `platform_function_definitions` table gets a new `port_schema jsonb` column (or the port data stays in handler.json and is served via the API) + +### Trigger UI Impact + +With typed ports, the Trigger form renders typed fields instead of a raw JSON textarea: + +``` +┌─────────────────────────────┐ +│ Trigger: send-email │ +│ │ +│ to: [email input ] │ +│ subject: [text input ] │ +│ html: [multiline ] │ +│ from: [noreply@... ] │ +│ │ +│ [Execute] │ +└─────────────────────────────┘ +``` + +--- + +## 5. DB Integration: Merkle Store + Graph Module + +### 5a. Provisioning + +The `graph_module` generator in constructive-db needs to be invoked during the constructive-functions database provisioning (`make up`). This produces: + +**Tables:** + +| Table | Schema | Purpose | +|-------|--------|---------| +| `{prefix}object` | merkle | Content-addressed tree nodes (`id` = UUID v5 hash of `kids[] + data`) | +| `{prefix}store` | merkle | Named stores (one per graph repo) | +| `{prefix}commit` | merkle | Commit history (store → tree snapshot) | +| `{prefix}ref` | merkle | Branch heads (mutable pointers) | +| `function_graphs` | public | Graph registry (name, store_id FK, context, is_valid, validation_errors) | +| `function_graph_executions` | private | Execution state (graph_id, status, input/output_payload, execution_plan, current_wave) | +| `function_graph_execution_outputs` | private | Content-addressed execution output storage | + +### 5b. SQL Function → API Endpoint Mapping + +| SQL Function | Visibility | API Endpoint | Purpose | +|--------------|------------|-------------|---------| +| `create_function_graph(scope_id, name, context, description)` | public | `POST /api/graphs` | Create empty graph with merkle store | +| `import_graph_json(scope_id, name, graph_json, context)` | public | `PUT /api/graphs/:id` | Import full @fbp/spec JSON into merkle tree | +| `serialize_graph(graph_id)` → jsonb | private | `GET /api/graphs/:id` | Read graph as @fbp/spec JSON | +| `add_node(scope_id, root_hash, node_name, node_type, context, graph_name, props, meta)` | public | _plumbing_ | Add single node to merkle tree | +| `add_edge(scope_id, root_hash, src_node, src_port, dst_node, dst_port, context, graph_name)` | public | _plumbing_ | Add single edge to merkle tree | +| `add_node_and_save(graph_id, ...)` | public | _porcelain_ | Add node + commit | +| `add_edge_and_save(graph_id, ...)` | public | _porcelain_ | Add edge + commit | +| `save_graph(graph_id, root_hash, message)` | public | `POST /api/graphs/:id/save` | Commit current state | +| `validate_function_graph(graph_id)` | public | `POST /api/graphs/:id/validate` | Structural validation | +| `read_function_graph(graph_id)` | public | _internal_ | Read graph row | +| `copy_graph(scope_id, graph_id, name)` | public | `POST /api/graphs/:id/copy` | Deep copy via serialize+deserialize | +| `start_execution(graph_id, input_payload, output_node, output_port, ...)` | public | `POST /api/graphs/:id/exec` | Begin execution | +| `tick_execution(execution_id)` | private | _internal_ | Advance execution wave | +| `complete_node(execution_id, node_name, output_data)` | private | `POST /api/exec/:id/complete` | Mark node done, trigger tick | +| `import_definitions(graph_id, source_scope_id, source_commit_id, contexts[])` | public | `POST /api/graphs/:id/definitions` | Pin NodeDefinitions | + +### 5c. Graph JSON Round-Trip + +The graph_module's `import_graph_json` / `serialize_graph` use the exact same JSON shape as `@fbp/types.Graph`: + +```typescript +// Client saves: +const graph: Graph = editor.getGraph(); +await fetch(`/api/graphs/${graphId}`, { + method: 'PUT', + body: JSON.stringify(graph) // → import_graph_json +}); + +// Client loads: +const res = await fetch(`/api/graphs/${graphId}`); +const graph: Graph = await res.json(); // ← serialize_graph +editor.setGraph(graph); +``` + +No transformation needed — the @fbp/spec JSON format IS the storage format. + +### 5d. NodeDefinition Sync + +Platform functions → NodeDefinitions flow: + +``` +platform_function_definitions (existing table) + │ + │ GET /api/functions (existing endpoint) + ▼ +handlerToDefinition() transform (in browser or server) + │ + │ Becomes NodeDefinition[] for editor + ▼ +@fbp/graph-editor receives definitions prop + │ + │ Also pinned to graph via import_definitions + ▼ +function_graphs.definitions_commit_id (merkle snapshot) +``` + +This means definitions are: +1. **Live** in the editor (loaded from API each session) +2. **Pinned** on each graph (via `definitions_commit_id`) for deterministic execution + +--- + +## 6. UI Component Architecture + +### Strategy: Wrap @fbp/graph-editor, Don't Rewrite + +The `@fbp/graph-editor` package exports a `` component: + +```tsx + +``` + +Internally it composes: +- `GraphCanvas` — SVG canvas, pan/zoom, box select, connection, drag-and-drop +- `GraphNode` — Node rendering, ports, multi-select drag +- `GraphEdge` / `TempEdge` — Bezier edges +- `Toolbar` — Palette + hotkey help +- `PropertiesPanel` — Props editing, evaluate, rename +- `StatusBar` — Node/edge counts, cwd breadcrumb, zoom % +- `GraphContext` — Reducer with 30+ actions + +### What to Build in constructive-functions + +``` +www/src/components/ + FlowsPanel.tsx # REPLACE current React Flow implementation + ├─ # @fbp/graph-editor (adopted as dependency) + ├─ FlowSelector # List/create/delete graphs (backed by API) + ├─ ExecutionPanel # Run flow, show execution status/results + └─ DefinitionLoader # Fetch platform functions → NodeDefinition[] +``` + +**FlowsPanel.tsx** becomes a thin wrapper: + +```tsx +function FlowsPanel() { + const [graph, setGraph] = useState(emptyGraph); + const [definitions, setDefinitions] = useState([]); + const [graphId, setGraphId] = useState(null); + + // Load definitions from platform functions + useEffect(() => { + api.getFunctions().then(fns => setDefinitions(fns.map(handlerToDefinition))); + }, []); + + // Load graph from DB + useEffect(() => { + if (graphId) api.getGraph(graphId).then(setGraph); + }, [graphId]); + + // Save graph to DB (debounced) + const handleChange = useDebouncedCallback((g: Graph) => { + if (graphId) api.saveGraph(graphId, g); + setGraph(g); + }, 1000); + + return ( +
+ + + +
+ ); +} +``` + +### Theming + +The editor uses Tailwind classes (`slate-*` color scheme). constructive-functions uses `zinc-950` dark theme. Options: + +1. **Configure Tailwind content paths** to include `@fbp/graph-editor`: + ```js + content: ['./node_modules/@fbp/graph-editor/**/*.{js,tsx}'] + ``` +2. **CSS custom properties** for colors if the editor supports theming +3. **Acceptable delta** — slate vs zinc are close enough in dark mode; can unify later + +--- + +## 7. State Management + +### Current: localStorage + +```typescript +localStorage.setItem('constructive-flows', JSON.stringify(flows)); +``` + +### Target: DB-Backed via Graph API + +``` +┌────────────┐ onChange ┌─────────────┐ PUT /api/graphs/:id ┌──────────────┐ +│ GraphEditor ├────────────────▶│ GraphBridge ├──────────────────────────▶│ import_graph_ │ +│ (in-memory │ │ (debounce │ │ json (merkle) │ +│ reducer) │◀────────────────┤ + optimistic│◀──────────────────────────┤ │ +│ │ setGraph │ updates) │ GET /api/graphs/:id │ │ +└────────────┘ └─────────────┘ └──────────────┘ +``` + +**GraphBridge responsibilities:** +- On mount: `GET /api/graphs/:id` → `serialize_graph` → pass to editor +- On change: debounced `PUT /api/graphs/:id` → `import_graph_json` (full snapshot) +- Optimistic updates: editor state is always ahead of DB, reconcile on error +- No offline fallback needed (server is local via `make up`) + +### Per-Scope State (cwd) + +The editor's `stateByPath: Map` already handles per-scope view/selection state internally. This is ephemeral (session-only) — not persisted to DB. The graph _data_ (nodes, edges) is persisted; the view state (pan, zoom, selection) is not. + +This matches the Houdini model: the file (graph) is saved, but your viewport is session-local. + +--- + +## 8. Execution Model + +### Overview: Wave-Based Scheduling + +The `graph_module` implements a wave-based execution engine: + +``` +start_execution(graph_id, input_payload) + │ + │ 1. Build execution_plan from graph topology + │ 2. Identify wave-0 nodes (no dependencies, or boundary inputs) + │ 3. Call tick_execution + │ + ▼ +tick_execution(execution_id) ◀──┐ + │ │ + │ For each ready node in current wave: │ + │ • Collect input data from upstream │ + │ • Dispatch job to app_jobs.jobs │ + │ │ + ▼ │ +Job Worker picks up job │ + │ │ + │ Function handler runs │ + │ │ + ▼ │ +complete_node(execution_id, node_name, output_data) + │ │ + │ 1. Store output in execution_outputs │ + │ 2. Check if downstream nodes ready │ + │ 3. If yes → tick_execution ────────────┘ + │ 4. If all done → mark execution complete + │ + ▼ +Execution status: 'completed' | 'failed' | 'timed_out' +``` + +### Execution Parameters + +```sql +start_execution( + graph_id uuid, + input_payload jsonb DEFAULT '{}'::jsonb, -- feeds graphInput boundary nodes + output_node text DEFAULT 'output_result', + output_port text DEFAULT 'value', + max_ticks int DEFAULT 100, -- safety limit + max_pending_jobs int DEFAULT 50, -- concurrency cap + timeout_interval interval DEFAULT '5 minutes', + parent_execution_id uuid DEFAULT NULL, -- for subnet execution chaining + parent_node_name text DEFAULT NULL +); +``` + +### Subnet Execution + +When `tick_execution` encounters a subnet node: +1. It calls `start_execution` recursively with `parent_execution_id` + `parent_node_name` +2. The child execution runs independently +3. When the child completes, `complete_node` is called on the parent execution +4. The parent then proceeds to the next wave + +### UI Integration + +``` +┌──────────────────────────────────────┐ +│ Execution Panel │ +│ │ +│ Graph: "email-pipeline" │ +│ Status: ● running (wave 2/4) │ +│ │ +│ Nodes: │ +│ ✓ fetch-template [completed] │ +│ ✓ format-html [completed] │ +│ ● send-email [running] │ +│ ○ log-result [pending] │ +│ │ +│ Input: { to: "...", template: "..." }│ +│ Output: (pending) │ +│ │ +│ [Execute] [Cancel] │ +└──────────────────────────────────────┘ +``` + +The execution panel polls `GET /api/graphs/:id/exec/:execId` for status updates, visualizing which nodes have completed, which are running, and which are pending. + +--- + +## 9. Implementation Phases + +### Phase 1: Editor Adoption + DB Wiring + +**Goal:** Replace React Flow FlowsPanel with @fbp/graph-editor, persist to DB. + +**Tasks:** +1. Add `@fbp/graph-editor`, `@fbp/types`, `@fbp/spec` as www/ dependencies +2. Provision `merkle_store_module` + `graph_module` in constructive-functions DB blueprint +3. Add Express API routes: `POST/GET/PUT /api/graphs`, `GET /api/graphs/:id` +4. Build `DefinitionLoader`: `GET /api/functions` → `NodeDefinition[]` via `handlerToDefinition()` +5. Replace `FlowsPanel.tsx` with `` wrapper + `FlowSelector` + `GraphBridge` +6. Configure Tailwind to include editor styles +7. Remove `@xyflow/react` dependency + +**Delivers:** Full editor with subnets, collapse (Shift+C), selections, hotkeys, context/cwd, boundary nodes, palette, properties panel — all persisted to merkle store. + +### Phase 2: Typed Ports + Execution + +**Goal:** Per-field ports on functions, flow execution. + +**Tasks:** +1. Extend `handler.json` schema with `inputs[]` / `outputs[]` +2. Update `handlerToDefinition()` to map typed ports +3. Add port compatibility validation on edge creation (schema-based) +4. Add Express routes: `POST /api/graphs/:id/exec`, `GET /api/graphs/:id/exec/:execId` +5. Build `ExecutionPanel` component (status, node progress, input/output display) +6. Wire "Run Flow" button → `start_execution` SQL function +7. Update Trigger UI to render typed fields from port schemas +8. Implement `complete_node` webhook endpoint for job worker callback + +**Delivers:** Typed port rendering, schema validation on edges, full flow execution with visual progress. + +### Phase 3: Advanced Features + +**Goal:** Channel references, multi-tab processes, graph versioning. + +**Tasks:** +1. Enable channel reference detection (`ch("...")`, `$VAR`) in property values +2. Implement multi-process/multi-tab support (`EditorState.processes: Map`) +3. Add graph version history UI (merkle commit log) +4. Add graph diff view (compare two commits) +5. Implement `import_definitions` for pinning definition snapshots +6. Add `evaluateFn` integration for in-editor preview (lazy evaluation via `@fbp/evaluator`) + +--- + +## 10. Open Questions + +### Q1: Graph Module Provisioning +How is the `graph_module` generator invoked during `make up`? Does the constructive-functions infra schema already have a hook for registering new modules, or do we need to add a migration? + +### Q2: Definition Storage Strategy +Should typed port definitions live in: +- (a) `handler.json` only (served by API, not stored in DB) +- (b) New `port_schema jsonb` column on `platform_function_definitions` +- (c) Both (handler.json is source of truth, synced to DB on registration) + +Option (c) seems most robust — handler.json is the authoring format, DB is the runtime source. + +### Q3: @fbp/graph-editor Package Distribution +Is `@fbp/graph-editor` published to an npm registry, or should constructive-functions reference it as a workspace/git dependency? This affects the dependency declaration in `www/package.json`. + +### Q4: Authentication for Graph API +The Express server currently uses database-scoped JWT claims. Graph API routes need the same auth pattern — should they use the existing `jwt.claims.database_id` mechanism? + +### Q5: Execution Job Dispatch +When `tick_execution` dispatches jobs, it presumably inserts into `app_jobs.jobs`. How does the job worker know to call `complete_node` when the function handler returns? Options: +- (a) Job worker calls `complete_node` SQL function directly after handler returns +- (b) Function handler POSTs to a callback URL +- (c) Job service polls execution status + +### Q6: Offline / Disconnected Editing +Should the editor support offline editing with sync-on-reconnect? The merkle store's content-addressed design enables this (merge by hash), but it adds complexity. Recommend deferring to Phase 3+. diff --git a/docs/spec/fbp-integration.md b/docs/spec/fbp-integration.md new file mode 100644 index 00000000..106dc392 --- /dev/null +++ b/docs/spec/fbp-integration.md @@ -0,0 +1,190 @@ +# FBP Integration with Constructive Functions + +## Overview + +This document describes how [Flow-Based Programming](https://en.wikipedia.org/wiki/Flow-based_programming) (FBP) concepts map onto the Constructive Functions platform, enabling visual flow graphs where each registered platform function becomes a draggable node with typed ports. + +## Background + +### Platform Function Definitions + +Functions are registered in `constructive_infra_public.platform_function_definitions`: + +```sql +CREATE TABLE platform_function_definitions ( + name TEXT PRIMARY KEY, + task_identifier TEXT NOT NULL, -- dispatch key, e.g. 'email:send_email' + service_url TEXT, + is_invocable BOOLEAN DEFAULT true, + is_built_in BOOLEAN DEFAULT false, + scope TEXT, -- e.g. 'platform', 'app' + description TEXT, + required_secrets composite[], -- (name, required) pairs + required_configs composite[], -- (name, required) pairs + namespace_id UUID REFERENCES platform_namespaces(id), + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ +); +``` + +### handler.json Manifest + +Each function's source directory contains a `handler.json`: + +```json +{ + "name": "send-email", + "version": "1.6.4", + "type": "node-graphql", + "port": 8081, + "taskIdentifier": "email:send_email", + "description": "Sends emails directly from job payload", + "dependencies": { ... } +} +``` + +### FBP NodeDefinition (from `@fbp/types`) + +```typescript +interface NodeDefinition { + context: string; // namespace, e.g. "email" + name: string; // e.g. "send-email" + category?: string; // palette group + inputs?: PortDef[]; // input ports + outputs?: PortDef[]; // output ports + props?: PropDef[]; // configuration properties + description?: string; + icon?: string; +} +``` + +## Mapping: platform_function_definitions → NodeDefinition + +### Direct Field Mapping + +| DB Column / handler.json | NodeDefinition Field | Transformation | +|---|---|---| +| `name` | `name` | Direct copy | +| `task_identifier` | `context` | The `task_identifier` doubles as the FBP context (dispatch address) | +| `scope` | `category` | Groups functions in the visual palette | +| `description` | `description` | Direct copy | +| `required_secrets[]` | `props[]` | Each `(secret_name, required)` → `{ name, type: 'secret', required }` | +| `required_configs[]` | `props[]` | Each `(config_name, required)` → `{ name, type: 'config', required }` | + +### Port Model + +Every function has an identical port signature — one JSON input, one JSON output: + +``` +┌────────────────────────┐ +│ send-email │ +│ │ +payload ──►│ handler(params, ctx) │──► result +│ │ +└────────────────────────┘ +``` + +```typescript +const toNodeDefinition = (fn: PlatformFunction): NodeDefinition => ({ + context: fn.task_identifier, + name: fn.name, + category: fn.scope || 'default', + description: fn.description, + inputs: [ + { name: 'payload', type: 'json', description: 'Job payload object' } + ], + outputs: [ + { name: 'result', type: 'json', description: 'Handler return value' } + ], + props: [ + ...fn.required_secrets.map(s => ({ + name: s.name, + type: 'secret', + required: s.required, + description: `Secret: ${s.name}` + })), + ...fn.required_configs.map(c => ({ + name: c.name, + type: 'config', + required: c.required, + description: `Config: ${c.name}` + })) + ] +}); +``` + +### Edge Semantics + +An edge connects one function's output to another's input: + +```typescript +// "Pipe result of send-email into log-result" +{ + src: { node: 'send-email-1', port: 'result' }, + dst: { node: 'log-result-1', port: 'payload' } +} +``` + +At runtime this means: when the job for `send-email-1` completes, its return value is used as the `payload` for a new job dispatched to `log-result-1`. + +### Job Payload Flow + +``` +User triggers flow + │ + ▼ +Job A inserted into app_jobs.jobs + │ task_identifier = fn_a.task_identifier + │ payload = user-provided or upstream result + │ + ▼ +Worker picks up Job A → calls fn_a handler + │ + ▼ +fn_a returns { complete: true, data: {...} } + │ + ▼ +For each outgoing edge from fn_a: + │ Insert new job with: + │ task_identifier = edge.dst function's task_identifier + │ payload = fn_a's result (or mapped subset) + │ + ▼ +Job B inserted → worker picks up → fn_b handler runs → ... +``` + +## Flow Graph (UI Model) + +A flow is persisted as an FBP `Graph`: + +```typescript +interface Flow { + name: string; + nodes: Array<{ + name: string; // instance id, e.g. "send-email-1" + type: string; // function name, e.g. "send-email" + meta?: { x: number; y: number }; + props?: Array<{ name: string; value?: any }>; + }>; + edges: Array<{ + src: { node: string; port: string }; + dst: { node: string; port: string }; + }>; + definitions: NodeDefinition[]; // populated from GET /api/functions +} +``` + +### Persistence Strategy + +| Phase | Storage | Notes | +|---|---|---| +| MVP | `localStorage` | Immediate, no backend changes needed | +| Production | DB table `constructive_infra_public.platform_flows` | Content-addressable via merkle hashing (aligned with `@fbp/spec` design) | + +## Future Extensions + +- **Typed ports**: Parse handler signatures to generate richer port schemas (beyond generic JSON) +- **Conditional routing**: Use FBP channels (`edge.channel`) for error vs. success paths +- **Subnets**: Compose flows of flows using FBP's nested graph model +- **Live execution**: Trigger a flow and watch job status propagate through the graph in real-time +- **Flow evaluation**: Use `@fbp/evaluator` to validate graphs before execution diff --git a/extensions/@pgpm/database-jobs/LICENSE b/extensions/@pgpm/database-jobs/LICENSE new file mode 100644 index 00000000..7b18c918 --- /dev/null +++ b/extensions/@pgpm/database-jobs/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2025 Dan Lynch +Copyright (c) 2025 Constructive + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/@pgpm/database-jobs/Makefile b/extensions/@pgpm/database-jobs/Makefile new file mode 100644 index 00000000..a7d586e4 --- /dev/null +++ b/extensions/@pgpm/database-jobs/Makefile @@ -0,0 +1,6 @@ +EXTENSION = pgpm-database-jobs +DATA = sql/pgpm-database-jobs--0.26.3.sql + +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) diff --git a/extensions/@pgpm/database-jobs/README.md b/extensions/@pgpm/database-jobs/README.md new file mode 100644 index 00000000..1015d10e --- /dev/null +++ b/extensions/@pgpm/database-jobs/README.md @@ -0,0 +1,363 @@ +# @pgpm/database-jobs + +

+ +

+ +

+ + + + + +

+ +Database-specific job handling and queue management. + +## Overview + +`@pgpm/database-jobs` provides a complete PostgreSQL-based background job processing system with persistent queues, scheduled jobs, and worker management. This package implements a robust job queue system entirely within PostgreSQL, enabling reliable background task processing with features like job locking, retries, priorities, and cron-style scheduling. + +## Features + +- **Persistent Job Queue**: Store jobs in PostgreSQL with ACID guarantees +- **Job Scheduling**: Cron-style and rule-based job scheduling +- **Worker Management**: Multiple workers with job locking and expiry +- **Priority Queue**: Process jobs by priority and run time +- **Automatic Retries**: Configurable retry attempts with exponential backoff +- **Job Keys**: Upsert semantics for idempotent job creation +- **Queue Management**: Named queues with independent locking +- **Notifications**: PostgreSQL LISTEN/NOTIFY for real-time job processing + +## Installation + +If you have `pgpm` installed: + +```bash +pgpm install @pgpm/database-jobs +pgpm deploy +``` + +This is a quick way to get started. The sections below provide more detailed installation options. + +### Prerequisites + +```bash +# Install pgpm CLI +npm install -g pgpm + +# Start local Postgres (via Docker) and export env vars +pgpm docker start +eval "$(pgpm env)" +``` + +> **Tip:** Already running Postgres? Skip the Docker step and just export your `PG*` environment variables. + +### **Add to an Existing Package** + +```bash +# 1. Install the package +pgpm install @pgpm/database-jobs + +# 2. Deploy locally +pgpm deploy +``` + +### **Add to a New Project** + +```bash +# 1. Create a workspace +pgpm init workspace + +# 2. Create your first module +cd my-workspace +pgpm init + +# 3. Install a package +cd packages/my-module +pgpm install @pgpm/database-jobs + +# 4. Deploy everything +pgpm deploy --createdb --database mydb1 +``` + +## Core Concepts + +### Jobs Table + +The `app_jobs.jobs` table stores active jobs with the following key fields: +- `id`: Unique job identifier +- `database_id`: Database/tenant identifier +- `task_identifier`: Job type/handler name +- `payload`: JSON data for the job +- `priority`: Lower numbers = higher priority (default: 0) +- `run_at`: When the job should run +- `attempts`: Current attempt count +- `max_attempts`: Maximum retry attempts (default: 25) +- `locked_by`: Worker ID that locked this job +- `locked_at`: When the job was locked +- `key`: Optional unique key for upsert semantics + +### Scheduled Jobs Table + +The `app_jobs.scheduled_jobs` table stores recurring jobs with cron-style or rule-based scheduling. + +### Job Queues Table + +The `app_jobs.job_queues` table tracks queue statistics and locking state. + +## Usage + +### Adding Jobs + +```sql +-- Add a simple job +SELECT app_jobs.add_job( + db_id := '5b720132-17d5-424d-9bcb-ee7b17c13d43'::uuid, + identifier := 'send_email', + payload := '{"to": "user@example.com", "subject": "Hello"}'::json +); + +-- Add a job with priority and delayed execution +SELECT app_jobs.add_job( + db_id := '5b720132-17d5-424d-9bcb-ee7b17c13d43'::uuid, + identifier := 'generate_report', + payload := '{"report_id": 123}'::json, + run_at := now() + interval '1 hour', + priority := 10, + max_attempts := 5 +); + +-- Add a job with a unique key (upsert semantics) +SELECT app_jobs.add_job( + db_id := '5b720132-17d5-424d-9bcb-ee7b17c13d43'::uuid, + identifier := 'daily_summary', + payload := '{"date": "2025-01-15"}'::json, + job_key := 'daily_summary_2025_01_15', + queue_name := 'reports' +); +``` + +### Getting Jobs (Worker Side) + +```sql +-- Worker fetches next available job +SELECT * FROM app_jobs.get_job( + worker_id := 'worker-1', + task_identifiers := ARRAY['send_email', 'generate_report'], + job_expiry := interval '4 hours' +); + +-- Returns NULL if no jobs available +-- Returns job row if job was successfully locked +``` + +### Completing Jobs + +```sql +-- Mark job as complete +SELECT app_jobs.complete_job( + worker_id := 'worker-1', + job_id := 123 +); +``` + +### Failing Jobs + +```sql +-- Mark job as failed (will retry if attempts < max_attempts) +SELECT app_jobs.fail_job( + worker_id := 'worker-1', + job_id := 123, + error_message := 'Connection timeout' +); +``` + +### Scheduled Jobs + +```sql +-- Schedule a job with cron-style timing +INSERT INTO app_jobs.scheduled_jobs ( + database_id, + task_identifier, + payload, + schedule_info +) VALUES ( + '5b720132-17d5-424d-9bcb-ee7b17c13d43'::uuid, + 'cleanup_old_data', + '{"days": 30}'::json, + '{ + "hour": [2], + "minute": [0], + "dayOfWeek": [0, 1, 2, 3, 4, 5, 6] + }'::json +); + +-- Schedule a job with a rule (every minute for 3 minutes) +SELECT app_jobs.add_scheduled_job( + db_id := '5b720132-17d5-424d-9bcb-ee7b17c13d43'::uuid, + identifier := 'heartbeat', + payload := '{}'::json, + schedule_info := json_build_object( + 'start', now() + interval '10 seconds', + 'end', now() + interval '3 minutes', + 'rule', '*/1 * * * *' + ) +); + +-- Run a scheduled job (creates a job in the jobs table) +SELECT * FROM app_jobs.run_scheduled_job(scheduled_job_id := 1); +``` + +## Functions Reference + +### app_jobs.add_job(...) + +Adds a new job to the queue or updates an existing job if a key is provided. + +**Parameters:** +- `db_id` (uuid): Database/tenant identifier +- `identifier` (text): Job type/handler name +- `payload` (json): Job data (default: `{}`) +- `job_key` (text): Optional unique key for upsert (default: NULL) +- `queue_name` (text): Optional queue name (default: random UUID) +- `run_at` (timestamptz): When to run (default: now()) +- `max_attempts` (integer): Maximum retries (default: 25) +- `priority` (integer): Job priority (default: 0) + +**Returns:** `app_jobs.jobs` row + +**Behavior:** +- If `job_key` is provided and exists, updates the job (if not locked) +- If job is locked, removes the key and creates a new job +- Triggers notifications for workers + +### app_jobs.get_job(...) + +Fetches and locks the next available job for a worker. + +**Parameters:** +- `worker_id` (text): Unique worker identifier +- `task_identifiers` (text[]): Optional filter for job types (default: NULL = all) +- `job_expiry` (interval): How long before locked jobs expire (default: 4 hours) + +**Returns:** `app_jobs.jobs` row or NULL + +**Behavior:** +- Selects jobs by priority, run_at, and id +- Locks the job and its queue +- Increments attempt counter +- Uses `FOR UPDATE SKIP LOCKED` for concurrency + +### app_jobs.complete_job(...) + +Marks a job as successfully completed and removes it from the queue. + +**Parameters:** +- `worker_id` (text): Worker that processed the job +- `job_id` (bigint): Job identifier + +**Returns:** `app_jobs.jobs` row + +### app_jobs.fail_job(...) + +Marks a job as failed and schedules retry if attempts remain. + +**Parameters:** +- `worker_id` (text): Worker that processed the job +- `job_id` (bigint): Job identifier +- `error_message` (text): Error description (default: NULL) + +**Returns:** `app_jobs.jobs` row + +**Behavior:** +- Records error message +- Unlocks the job for retry if attempts < max_attempts +- Permanently fails if max_attempts reached + +### app_jobs.add_scheduled_job(...) + +Creates a scheduled job with cron-style or rule-based timing. + +**Parameters:** +- `db_id` (uuid): Database/tenant identifier +- `identifier` (text): Job type/handler name +- `payload` (json): Job data +- `schedule_info` (json): Scheduling configuration +- `job_key` (text): Optional unique key +- `queue_name` (text): Optional queue name +- `max_attempts` (integer): Maximum retries +- `priority` (integer): Job priority + +**Returns:** `app_jobs.scheduled_jobs` row + +### app_jobs.run_scheduled_job(...) + +Executes a scheduled job by creating a job in the jobs table. + +**Parameters:** +- `scheduled_job_id` (bigint): Scheduled job identifier + +**Returns:** `app_jobs.jobs` row + +## Job Processing Pattern + +```sql +-- Worker loop (simplified) +LOOP + -- 1. Get next job + SELECT * FROM app_jobs.get_job('worker-1', ARRAY['my_task']); + + -- 2. Process job + -- ... application logic ... + + -- 3. Mark as complete or failed + IF success THEN + SELECT app_jobs.complete_job('worker-1', job_id); + ELSE + SELECT app_jobs.fail_job('worker-1', job_id, error_msg); + END IF; +END LOOP; +``` + +## Triggers and Automation + +The package includes several triggers for automatic management: + +- **timestamps**: Automatically sets created_at/updated_at +- **notify_worker**: Sends LISTEN/NOTIFY events when jobs are added +- **increase_job_queue_count**: Updates queue statistics on insert +- **decrease_job_queue_count**: Updates queue statistics on delete/update + +## Dependencies + +- PGPM roles (anonymous, authenticated, administrator) +- `@pgpm/verify`: Verification utilities for database objects + +## Testing + +```bash +pnpm test +``` + +The test suite validates: +- Job creation and retrieval +- Scheduled job creation with cron and rule-based timing +- Job key upsert semantics +- Worker locking and concurrency + +## Related Tooling + +* [pgpm](https://github.com/constructive-io/constructive/tree/main/packages/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages. +* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/packages/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation. +* [supabase-test](https://github.com/constructive-io/constructive/tree/main/packages/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready. +* [graphile-test](https://github.com/constructive-io/constructive/tree/main/packages/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts. +* [pgsql-parser](https://github.com/constructive-io/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax. +* [libpg-query-node](https://github.com/constructive-io/libpg-query-node): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees. +* [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. + +## Disclaimer + +AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND. + +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. diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/helpers/json_build_object_apply.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/helpers/json_build_object_apply.sql new file mode 100644 index 00000000..2a835248 --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/helpers/json_build_object_apply.sql @@ -0,0 +1,28 @@ +-- Deploy schemas/app_jobs/helpers/json_build_object_apply to pg +-- requires: schemas/app_jobs/schema + +BEGIN; +CREATE FUNCTION app_jobs.json_build_object_apply (arguments text[]) + RETURNS json + AS $$ +DECLARE + arg text; + _sql text; + _res json; + args text[]; +BEGIN + _sql = 'SELECT json_build_object('; + FOR arg IN + SELECT + unnest(arguments) + LOOP + args = array_append(args, format('''%s''', arg)); + END LOOP; + _sql = _sql || format('%s);', array_to_string(args, ',')); + EXECUTE _sql INTO _res; + RETURN _res; +END; +$$ +LANGUAGE 'plpgsql'; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/add_job.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/add_job.sql new file mode 100644 index 00000000..5cf035d1 --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/add_job.sql @@ -0,0 +1,125 @@ +-- Deploy schemas/app_jobs/procedures/add_job to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table +-- requires: schemas/app_jobs/tables/job_queues/table +-- requires: pgpm-jwt-claims:schemas/jwt_private/procedures/current_database_id +-- requires: pgpm-jwt-claims:schemas/jwt_public/procedures/current_user_id + +BEGIN; +CREATE FUNCTION app_jobs.add_job ( + identifier text, + payload json DEFAULT '{}' ::json, + job_key text DEFAULT NULL, + queue_name text DEFAULT NULL, + run_at timestamptz DEFAULT now(), + max_attempts integer DEFAULT 25, + priority integer DEFAULT 0, + entity_id uuid DEFAULT NULL, + organization_id uuid DEFAULT NULL, + entity_type text DEFAULT NULL +) + RETURNS app_jobs.jobs + AS $$ +DECLARE + v_job app_jobs.jobs; + v_database_id uuid; + v_actor_id uuid; +BEGIN + -- Read context from JWT claims + v_database_id := jwt_private.current_database_id(); + v_actor_id := jwt_public.current_user_id(); + + IF job_key IS NOT NULL THEN + -- Upsert job + INSERT INTO app_jobs.jobs ( + database_id, + actor_id, + entity_id, + organization_id, + entity_type, + task_identifier, + payload, + queue_name, + run_at, + max_attempts, + key, + priority + ) VALUES ( + v_database_id, + v_actor_id, + add_job.entity_id, + add_job.organization_id, + add_job.entity_type, + identifier, + coalesce(payload, '{}'::json), + queue_name, + coalesce(run_at, now()), + coalesce(max_attempts, 25), + job_key, + coalesce(priority, 0) + ) + ON CONFLICT (key) + DO UPDATE SET + task_identifier = EXCLUDED.task_identifier, + payload = EXCLUDED.payload, + queue_name = EXCLUDED.queue_name, + max_attempts = EXCLUDED.max_attempts, + run_at = EXCLUDED.run_at, + priority = EXCLUDED.priority, + -- always reset error/retry state + attempts = 0, last_error = NULL + WHERE + jobs.locked_at IS NULL + RETURNING + * INTO v_job; + + -- If upsert succeeded (insert or update), return early + IF NOT (v_job IS NULL) THEN + RETURN v_job; + END IF; + + -- Upsert failed -> there must be an existing job that is locked. Remove + -- existing key to allow a new one to be inserted, and prevent any + -- subsequent retries by bumping attempts to the max allowed. + UPDATE + app_jobs.jobs + SET + key = NULL, + attempts = jobs.max_attempts + WHERE + key = job_key; + END IF; + + INSERT INTO app_jobs.jobs ( + database_id, + actor_id, + entity_id, + organization_id, + entity_type, + task_identifier, + payload, + queue_name, + run_at, + max_attempts, + priority + ) VALUES ( + v_database_id, + v_actor_id, + add_job.entity_id, + add_job.organization_id, + add_job.entity_type, + identifier, + payload, + queue_name, + run_at, + max_attempts, + priority + ) + RETURNING * INTO v_job; + + RETURN v_job; +END; +$$ +LANGUAGE 'plpgsql' VOLATILE SECURITY DEFINER; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/add_scheduled_job.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/add_scheduled_job.sql new file mode 100644 index 00000000..e9d86777 --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/add_scheduled_job.sql @@ -0,0 +1,112 @@ +-- Deploy schemas/app_jobs/procedures/add_scheduled_job to pg + +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/scheduled_jobs/table +-- requires: pgpm-jwt-claims:schemas/jwt_private/procedures/current_database_id +-- requires: pgpm-jwt-claims:schemas/jwt_public/procedures/current_user_id + +BEGIN; + +CREATE FUNCTION app_jobs.add_scheduled_job( + identifier text, + payload json DEFAULT '{}'::json, + schedule_info json DEFAULT '{}'::json, + job_key text DEFAULT NULL, + queue_name text DEFAULT NULL, + max_attempts integer DEFAULT 25, + priority integer DEFAULT 0, + entity_id uuid DEFAULT NULL +) + RETURNS app_jobs.scheduled_jobs + AS $$ +DECLARE + v_job app_jobs.scheduled_jobs; + v_database_id uuid; + v_actor_id uuid; +BEGIN + v_database_id := jwt_private.current_database_id(); + v_actor_id := jwt_public.current_user_id(); + + IF job_key IS NOT NULL THEN + + -- Upsert job + INSERT INTO app_jobs.scheduled_jobs ( + database_id, + actor_id, + entity_id, + task_identifier, + payload, + queue_name, + schedule_info, + max_attempts, + key, + priority + ) VALUES ( + v_database_id, + v_actor_id, + add_scheduled_job.entity_id, + identifier, + coalesce(payload, '{}'::json), + queue_name, + schedule_info, + coalesce(max_attempts, 25), + job_key, + coalesce(priority, 0) + ) + ON CONFLICT (key) + DO UPDATE SET + task_identifier = EXCLUDED.task_identifier, + payload = EXCLUDED.payload, + queue_name = EXCLUDED.queue_name, + max_attempts = EXCLUDED.max_attempts, + schedule_info = EXCLUDED.schedule_info, + priority = EXCLUDED.priority + WHERE + scheduled_jobs.locked_at IS NULL + RETURNING + * INTO v_job; + + -- If upsert succeeded (insert or update), return early + + IF NOT (v_job IS NULL) THEN + RETURN v_job; + END IF; + + -- Upsert failed -> there must be an existing scheduled job that is locked. Remove + -- and allow a new one to be inserted + + DELETE FROM + app_jobs.scheduled_jobs + WHERE + KEY = job_key; + END IF; + + INSERT INTO app_jobs.scheduled_jobs ( + database_id, + actor_id, + entity_id, + task_identifier, + payload, + queue_name, + schedule_info, + max_attempts, + priority + ) VALUES ( + v_database_id, + v_actor_id, + add_scheduled_job.entity_id, + identifier, + payload, + queue_name, + schedule_info, + max_attempts, + priority + ) RETURNING * INTO v_job; + RETURN v_job; +END; +$$ +LANGUAGE 'plpgsql' +VOLATILE +SECURITY DEFINER; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/complete_job.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/complete_job.sql new file mode 100644 index 00000000..afafdbb2 --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/complete_job.sql @@ -0,0 +1,32 @@ +-- Deploy schemas/app_jobs/procedures/complete_job to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table +-- requires: schemas/app_jobs/tables/job_queues/table + +BEGIN; +CREATE FUNCTION app_jobs.complete_job (worker_id text, job_id bigint) + RETURNS app_jobs.jobs + LANGUAGE plpgsql + AS $$ +DECLARE + v_row app_jobs.jobs; +BEGIN + DELETE FROM app_jobs.jobs + WHERE id = job_id + RETURNING + * INTO v_row; + IF v_row.queue_name IS NOT NULL THEN + UPDATE + app_jobs.job_queues + SET + locked_by = NULL, + locked_at = NULL + WHERE + queue_name = v_row.queue_name + AND locked_by = worker_id; + END IF; + RETURN v_row; +END; +$$; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/complete_jobs.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/complete_jobs.sql new file mode 100644 index 00000000..1b14dffc --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/complete_jobs.sql @@ -0,0 +1,19 @@ +-- Deploy schemas/app_jobs/procedures/complete_jobs to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/job_queues/table +-- requires: schemas/app_jobs/tables/jobs/table + +BEGIN; +CREATE FUNCTION app_jobs.complete_jobs (job_ids bigint[]) + RETURNS SETOF app_jobs.jobs + LANGUAGE sql + AS $$ + DELETE FROM app_jobs.jobs + WHERE id = ANY (job_ids) + AND (locked_by IS NULL + OR locked_at < NOW() - interval '4 hours') + RETURNING + *; +$$; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/do_notify.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/do_notify.sql new file mode 100644 index 00000000..82d92525 --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/do_notify.sql @@ -0,0 +1,16 @@ +-- Deploy schemas/app_jobs/procedures/do_notify to pg +-- requires: schemas/app_jobs/schema + +BEGIN; +CREATE FUNCTION app_jobs.do_notify () + RETURNS TRIGGER + AS $$ +BEGIN + PERFORM + pg_notify(TG_ARGV[0], ''); + RETURN NEW; +END; +$$ +LANGUAGE plpgsql; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/fail_job.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/fail_job.sql new file mode 100644 index 00000000..a5b38c68 --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/fail_job.sql @@ -0,0 +1,41 @@ +-- Deploy schemas/app_jobs/procedures/fail_job to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table +-- requires: schemas/app_jobs/tables/job_queues/table + +BEGIN; +CREATE FUNCTION app_jobs.fail_job (worker_id text, job_id bigint, error_message text) + RETURNS app_jobs.jobs + LANGUAGE plpgsql + STRICT + AS $$ +DECLARE + v_row app_jobs.jobs; +BEGIN + UPDATE + app_jobs.jobs + SET + last_error = error_message, + run_at = greatest (now(), run_at) + (exp(least (attempts, 10))::text || ' seconds')::interval, + locked_by = NULL, + locked_at = NULL + WHERE + id = job_id + AND locked_by = worker_id + RETURNING + * INTO v_row; + IF v_row.queue_name IS NOT NULL THEN + UPDATE + app_jobs.job_queues + SET + locked_by = NULL, + locked_at = NULL + WHERE + queue_name = v_row.queue_name + AND locked_by = worker_id; + END IF; + RETURN v_row; +END; +$$; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/force_unlock_workers.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/force_unlock_workers.sql new file mode 100644 index 00000000..c28392ca --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/force_unlock_workers.sql @@ -0,0 +1,20 @@ +-- Deploy schemas/app_jobs/procedures/force_unlock_workers to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table +-- requires: schemas/app_jobs/tables/job_queues/table + +BEGIN; +CREATE FUNCTION app_jobs.force_unlock_workers (worker_ids text[]) + RETURNS void + LANGUAGE sql + VOLATILE + AS $$ + UPDATE app_jobs.jobs + SET locked_at = NULL, locked_by = NULL + WHERE locked_by = ANY (worker_ids); + + UPDATE app_jobs.job_queues + SET locked_at = NULL, locked_by = NULL + WHERE locked_by = ANY (worker_ids); +$$; +COMMIT; diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/get_job.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/get_job.sql new file mode 100644 index 00000000..e3ac03fb --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/get_job.sql @@ -0,0 +1,68 @@ +-- Deploy schemas/app_jobs/procedures/get_job to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/job_queues/table +-- requires: schemas/app_jobs/tables/jobs/table + +BEGIN; +CREATE FUNCTION app_jobs.get_job ( + worker_id text, + task_identifiers text[] DEFAULT NULL, + job_expiry interval DEFAULT '4 hours' +) + RETURNS app_jobs.jobs + LANGUAGE plpgsql + AS $$ +DECLARE + v_job_id bigint; + v_queue_name text; + v_row app_jobs.jobs; + v_now timestamptz = now(); +BEGIN + IF worker_id IS NULL THEN + RAISE EXCEPTION 'INVALID_WORKER_ID'; + END IF; + + SELECT jobs.queue_name, jobs.id + INTO v_queue_name, v_job_id + FROM app_jobs.jobs + WHERE is_available = true + AND (jobs.locked_at IS NULL + OR jobs.locked_at < (v_now - job_expiry)) + AND (jobs.queue_name IS NULL + OR jobs.queue_name IN ( + SELECT jq.queue_name + FROM app_jobs.job_queues jq + WHERE (jq.locked_at IS NULL + OR jq.locked_at < (v_now - job_expiry)) + FOR UPDATE SKIP LOCKED + )) + AND run_at <= v_now + AND attempts < max_attempts + AND (task_identifiers IS NULL + OR task_identifier = ANY (task_identifiers)) + ORDER BY priority ASC, run_at ASC, id ASC + LIMIT 1 + FOR UPDATE SKIP LOCKED; + + IF v_job_id IS NULL THEN + RETURN NULL; + END IF; + + IF v_queue_name IS NOT NULL THEN + UPDATE app_jobs.job_queues + SET locked_by = worker_id, locked_at = v_now + WHERE job_queues.queue_name = v_queue_name; + END IF; + + UPDATE app_jobs.jobs + SET + attempts = attempts + 1, + locked_by = worker_id, + locked_at = v_now + WHERE id = v_job_id + RETURNING * INTO v_row; + + RETURN v_row; +END; +$$; +COMMIT; diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/get_scheduled_job.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/get_scheduled_job.sql new file mode 100644 index 00000000..b8fa5a66 --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/get_scheduled_job.sql @@ -0,0 +1,61 @@ +-- Deploy schemas/app_jobs/procedures/get_scheduled_job to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/scheduled_jobs/table + +BEGIN; +CREATE FUNCTION app_jobs.get_scheduled_job (worker_id text, task_identifiers text[] DEFAULT NULL) + RETURNS app_jobs.scheduled_jobs + LANGUAGE plpgsql + AS $$ +DECLARE + v_job_id bigint; + v_row app_jobs.scheduled_jobs; +BEGIN + + -- + + IF worker_id IS NULL THEN + RAISE exception 'INVALID_WORKER_ID'; + END IF; + + -- + + SELECT + scheduled_jobs.id INTO v_job_id + FROM + app_jobs.scheduled_jobs + WHERE (scheduled_jobs.locked_at IS NULL) + AND (task_identifiers IS NULL + OR task_identifier = ANY (task_identifiers)) + ORDER BY + priority ASC, + id ASC + LIMIT 1 + FOR UPDATE + SKIP LOCKED; + + -- + + IF v_job_id IS NULL THEN + RETURN NULL; + END IF; + + -- + + UPDATE + app_jobs.scheduled_jobs + SET + locked_by = worker_id, + locked_at = NOW() + WHERE + id = v_job_id + RETURNING + * INTO v_row; + + -- + + RETURN v_row; +END; +$$; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated.sql new file mode 100644 index 00000000..23a3fab0 --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated.sql @@ -0,0 +1,10 @@ +-- Deploy schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated to pg + +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/procedures/add_job + +BEGIN; + +GRANT EXECUTE ON FUNCTION app_jobs.add_job(text, json, text, text, timestamptz, integer, integer, uuid, uuid, text) TO authenticated; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/permanently_fail_jobs.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/permanently_fail_jobs.sql new file mode 100644 index 00000000..3c732806 --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/permanently_fail_jobs.sql @@ -0,0 +1,24 @@ +-- Deploy schemas/app_jobs/procedures/permanently_fail_jobs to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/job_queues/table +-- requires: schemas/app_jobs/tables/jobs/table + +BEGIN; +CREATE FUNCTION app_jobs.permanently_fail_jobs (job_ids bigint[], error_message text DEFAULT NULL) + RETURNS SETOF app_jobs.jobs + LANGUAGE sql + AS $$ + UPDATE + app_jobs.jobs + SET + last_error = coalesce(error_message, 'Manually marked as failed'), + attempts = max_attempts + WHERE + id = ANY (job_ids) + AND (locked_by IS NULL + OR locked_at < NOW() - interval '4 hours') + RETURNING + *; +$$; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/release_jobs.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/release_jobs.sql new file mode 100644 index 00000000..2fd06336 --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/release_jobs.sql @@ -0,0 +1,34 @@ +-- Deploy schemas/app_jobs/procedures/release_jobs to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table +-- requires: schemas/app_jobs/tables/job_queues/table + +BEGIN; +CREATE FUNCTION app_jobs.release_jobs (worker_id text) + RETURNS void + AS $$ +DECLARE +BEGIN + -- clear the job + UPDATE + app_jobs.jobs + SET + locked_at = NULL, + locked_by = NULL, + attempts = GREATEST (attempts - 1, 0) + WHERE + locked_by = worker_id; + -- clear the queue + UPDATE + app_jobs.job_queues + SET + locked_at = NULL, + locked_by = NULL + WHERE + locked_by = worker_id; +END; +$$ +LANGUAGE 'plpgsql' +VOLATILE; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/release_scheduled_jobs.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/release_scheduled_jobs.sql new file mode 100644 index 00000000..ec66b60a --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/release_scheduled_jobs.sql @@ -0,0 +1,26 @@ +-- Deploy schemas/app_jobs/procedures/release_scheduled_jobs to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/scheduled_jobs/table + +BEGIN; +CREATE FUNCTION app_jobs.release_scheduled_jobs (worker_id text, ids bigint[] DEFAULT NULL) + RETURNS void + AS $$ +DECLARE +BEGIN + -- clear the scheduled job + UPDATE + app_jobs.scheduled_jobs s + SET + locked_at = NULL, + locked_by = NULL + WHERE + locked_by = worker_id + AND (ids IS NULL + OR s.id = ANY (ids)); +END; +$$ +LANGUAGE 'plpgsql' +VOLATILE; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/remove_job.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/remove_job.sql new file mode 100644 index 00000000..b6688afe --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/remove_job.sql @@ -0,0 +1,34 @@ +-- Deploy schemas/app_jobs/procedures/remove_job to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table + +BEGIN; +CREATE FUNCTION app_jobs.remove_job (job_key text) + RETURNS app_jobs.jobs + LANGUAGE plpgsql + STRICT + AS $$ +DECLARE + v_job app_jobs.jobs; +BEGIN + DELETE FROM app_jobs.jobs + WHERE key = job_key + AND (locked_at IS NULL + OR locked_at < NOW() - interval '4 hours') + RETURNING * INTO v_job; + + IF NOT (v_job IS NULL) THEN + RETURN v_job; + END IF; + + UPDATE app_jobs.jobs + SET + key = NULL, + attempts = jobs.max_attempts + WHERE key = job_key + RETURNING * INTO v_job; + + RETURN v_job; +END; +$$; +COMMIT; diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/reschedule_jobs.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/reschedule_jobs.sql new file mode 100644 index 00000000..b39d884d --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/reschedule_jobs.sql @@ -0,0 +1,26 @@ +-- Deploy schemas/app_jobs/procedures/reschedule_jobs to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table + +BEGIN; +-- NOTE this should be renamed to reset_jobs to avoid confusion of scheduled jobs +CREATE FUNCTION app_jobs.reschedule_jobs (job_ids bigint[], run_at timestamptz DEFAULT NULL, priority integer DEFAULT NULL, attempts integer DEFAULT NULL, max_attempts integer DEFAULT NULL) + RETURNS SETOF app_jobs.jobs + LANGUAGE sql + AS $$ + UPDATE + app_jobs.jobs + SET + run_at = coalesce(reschedule_jobs.run_at, jobs.run_at), + priority = coalesce(reschedule_jobs.priority, jobs.priority), + attempts = coalesce(reschedule_jobs.attempts, jobs.attempts), + max_attempts = coalesce(reschedule_jobs.max_attempts, jobs.max_attempts) + WHERE + id = ANY (job_ids) + AND (locked_by IS NULL + OR locked_at < NOW() - interval '4 hours') + RETURNING + *; +$$; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/run_scheduled_job.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/run_scheduled_job.sql new file mode 100644 index 00000000..7bdb4399 --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/procedures/run_scheduled_job.sql @@ -0,0 +1,82 @@ +-- Deploy schemas/app_jobs/procedures/run_scheduled_job to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table +-- requires: schemas/app_jobs/tables/scheduled_jobs/table + +BEGIN; +CREATE FUNCTION app_jobs.run_scheduled_job (id bigint, job_expiry interval DEFAULT '1 hours') + RETURNS app_jobs.jobs + AS $$ +DECLARE + j app_jobs.jobs; + last_id bigint; + lkd_by text; +BEGIN + -- check last scheduled + SELECT + last_scheduled_id + FROM + app_jobs.scheduled_jobs s + WHERE + s.id = run_scheduled_job.id INTO last_id; + + -- if it's been scheduled check if it's been run + + IF (last_id IS NOT NULL) THEN + SELECT + locked_by + FROM + app_jobs.jobs js + WHERE + js.id = last_id + AND (js.locked_at IS NULL -- never been run + OR js.locked_at >= (NOW() - job_expiry) + -- still running within a safe interval +) INTO lkd_by; + IF (FOUND) THEN + RAISE EXCEPTION 'ALREADY_SCHEDULED'; + END IF; + END IF; + + -- insert new job + INSERT INTO app_jobs.jobs ( + database_id, + actor_id, + entity_id, + queue_name, + task_identifier, + payload, + priority, + max_attempts, + key + ) SELECT + database_id, + actor_id, + entity_id, + queue_name, + task_identifier, + payload, + priority, + max_attempts, + key + FROM + app_jobs.scheduled_jobs s + WHERE + s.id = run_scheduled_job.id + RETURNING + * INTO j; + -- update the scheduled job + UPDATE + app_jobs.scheduled_jobs s + SET + last_scheduled = NOW(), + last_scheduled_id = j.id + WHERE + s.id = run_scheduled_job.id; + RETURN j; +END; +$$ +LANGUAGE 'plpgsql' +VOLATILE; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/schema.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/schema.sql new file mode 100644 index 00000000..8c3339c3 --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/schema.sql @@ -0,0 +1,8 @@ +-- Deploy schemas/app_jobs/schema to pg +BEGIN; +CREATE SCHEMA IF NOT EXISTS app_jobs; +GRANT USAGE ON SCHEMA app_jobs TO administrator; +GRANT USAGE ON SCHEMA app_jobs TO authenticated; +ALTER DEFAULT PRIVILEGES IN SCHEMA app_jobs GRANT EXECUTE ON FUNCTIONS TO administrator; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql new file mode 100644 index 00000000..6f0fc3be --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql @@ -0,0 +1,12 @@ +-- Deploy schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator to pg + +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/job_queues/table + +BEGIN; + +-- TODO make sure to require any policies on this table! + +GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE app_jobs.job_queues TO administrator; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql new file mode 100644 index 00000000..cc78f18a --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql @@ -0,0 +1,8 @@ +-- Deploy schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/job_queues/table + +BEGIN; +CREATE INDEX job_queues_locked_by_idx ON app_jobs.job_queues (locked_by); +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/job_queues/table.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/job_queues/table.sql new file mode 100644 index 00000000..dd003c34 --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/job_queues/table.sql @@ -0,0 +1,19 @@ +-- Deploy schemas/app_jobs/tables/job_queues/table to pg +-- requires: schemas/app_jobs/schema + +BEGIN; +CREATE TABLE app_jobs.job_queues ( + queue_name text NOT NULL PRIMARY KEY, + job_count int DEFAULT 0 NOT NULL, + locked_at timestamptz, + locked_by text +); + +COMMENT ON TABLE app_jobs.job_queues IS 'Queue metadata: tracks job counts and locking state for each named queue'; +COMMENT ON COLUMN app_jobs.job_queues.queue_name IS 'Unique name identifying this queue'; +COMMENT ON COLUMN app_jobs.job_queues.job_count IS 'Number of pending jobs in this queue'; +COMMENT ON COLUMN app_jobs.job_queues.locked_at IS 'Timestamp when this queue was locked for batch processing'; +COMMENT ON COLUMN app_jobs.job_queues.locked_by IS 'Identifier of the worker that currently holds the queue lock'; + +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql new file mode 100644 index 00000000..11a3ac34 --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql @@ -0,0 +1,12 @@ +-- Deploy schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator to pg + +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table + +BEGIN; + +-- TODO make sure to require any policies on this table! + +GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE app_jobs.jobs TO administrator; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql new file mode 100644 index 00000000..d4168037 --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql @@ -0,0 +1,8 @@ +-- Deploy schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table + +BEGIN; +CREATE INDEX jobs_locked_by_idx ON app_jobs.jobs (locked_by); +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql new file mode 100644 index 00000000..35bf1d6e --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql @@ -0,0 +1,17 @@ +-- Deploy schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table + +BEGIN; + +CREATE INDEX jobs_main_index + ON app_jobs.jobs USING btree (priority, run_at) + INCLUDE (id, queue_name) + WHERE (is_available = true); + +CREATE INDEX jobs_no_queue_index + ON app_jobs.jobs USING btree (priority, run_at) + INCLUDE (id) + WHERE (is_available = true AND queue_name IS NULL); + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/table.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/table.sql new file mode 100644 index 00000000..e562e591 --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/table.sql @@ -0,0 +1,53 @@ +-- Deploy schemas/app_jobs/tables/jobs/table to pg +-- requires: schemas/app_jobs/schema + +BEGIN; +CREATE TABLE app_jobs.jobs ( + id bigserial PRIMARY KEY, + database_id uuid, + actor_id uuid, + entity_id uuid, + organization_id uuid, + entity_type text, + queue_name text DEFAULT NULL, + task_identifier text NOT NULL, + payload json DEFAULT '{}' ::json NOT NULL, + priority integer DEFAULT 0 NOT NULL, + run_at timestamptz DEFAULT now() NOT NULL, + attempts integer DEFAULT 0 NOT NULL, + max_attempts integer DEFAULT 25 NOT NULL, + key text, + last_error text, + locked_at timestamptz, + locked_by text, + is_available boolean GENERATED ALWAYS AS ((locked_at IS NULL) AND (attempts < max_attempts)) STORED NOT NULL, + CHECK (length(key) < 513), + CHECK (length(task_identifier) < 127), + CHECK (max_attempts >= 1), + CHECK (length(queue_name) < 127), + CHECK (length(locked_by) > 3), + UNIQUE (key) +); + +COMMENT ON TABLE app_jobs.jobs IS 'Background job queue: each row is a pending or in-progress task, optionally scoped to a database'; +COMMENT ON COLUMN app_jobs.jobs.id IS 'Auto-incrementing job identifier'; +COMMENT ON COLUMN app_jobs.jobs.database_id IS 'Database this job belongs to (nullable for system-level jobs without tenant context)'; +COMMENT ON COLUMN app_jobs.jobs.actor_id IS 'User who triggered this job, read from JWT claims at enqueue time'; +COMMENT ON COLUMN app_jobs.jobs.entity_id IS 'Entity (org/team) this job is scoped to for billing; NULL means platform-level (resolved via database_id → owner_id)'; +COMMENT ON COLUMN app_jobs.jobs.organization_id IS 'Top-level organization for this entity; resolved at enqueue time via get_organization_id(entity_type, entity_id)'; +COMMENT ON COLUMN app_jobs.jobs.entity_type IS 'Entity type prefix (org, team, app, etc.) for interpreting entity_id'; +COMMENT ON COLUMN app_jobs.jobs.queue_name IS 'Name of the queue this job belongs to; used for worker routing and concurrency control'; +COMMENT ON COLUMN app_jobs.jobs.task_identifier IS 'Identifier for the task type (maps to a worker handler function)'; +COMMENT ON COLUMN app_jobs.jobs.payload IS 'JSON payload of arguments passed to the task handler'; +COMMENT ON COLUMN app_jobs.jobs.priority IS 'Execution priority; lower numbers run first (default 0)'; +COMMENT ON COLUMN app_jobs.jobs.run_at IS 'Earliest time this job should be executed; used for delayed/scheduled execution'; +COMMENT ON COLUMN app_jobs.jobs.attempts IS 'Number of times this job has been attempted so far'; +COMMENT ON COLUMN app_jobs.jobs.max_attempts IS 'Maximum retry attempts before the job is considered permanently failed'; +COMMENT ON COLUMN app_jobs.jobs.key IS 'Optional unique deduplication key; prevents duplicate jobs with the same key'; +COMMENT ON COLUMN app_jobs.jobs.last_error IS 'Error message from the most recent failed attempt'; +COMMENT ON COLUMN app_jobs.jobs.locked_at IS 'Timestamp when a worker locked this job for processing'; +COMMENT ON COLUMN app_jobs.jobs.locked_by IS 'Identifier of the worker that currently holds the lock'; +COMMENT ON COLUMN app_jobs.jobs.is_available IS 'Generated column: true when job is unlocked and has remaining attempts'; + +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql new file mode 100644 index 00000000..c87bf7bc --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql @@ -0,0 +1,45 @@ +-- Deploy schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table + +BEGIN; +CREATE FUNCTION app_jobs.tg_decrease_job_queue_count () + RETURNS TRIGGER + AS $$ +DECLARE + v_new_job_count int; +BEGIN + UPDATE + app_jobs.job_queues + SET + job_count = job_queues.job_count - 1 + WHERE + queue_name = OLD.queue_name + RETURNING + job_count INTO v_new_job_count; + IF v_new_job_count <= 0 THEN + DELETE FROM app_jobs.job_queues + WHERE queue_name = OLD.queue_name + AND job_count <= 0; + END IF; + RETURN OLD; +END; +$$ +LANGUAGE 'plpgsql' +VOLATILE; + +CREATE TRIGGER decrease_job_queue_count_on_delete + AFTER DELETE ON app_jobs.jobs + FOR EACH ROW + WHEN ((OLD.queue_name IS NOT NULL)) + EXECUTE PROCEDURE app_jobs.tg_decrease_job_queue_count (); + +-- only a person would do this... +CREATE TRIGGER decrease_job_queue_count_on_update + AFTER UPDATE OF queue_name ON app_jobs.jobs + FOR EACH ROW + WHEN (((NEW.queue_name IS DISTINCT FROM OLD.queue_name) AND (OLD.queue_name IS NOT NULL))) + EXECUTE PROCEDURE app_jobs.tg_decrease_job_queue_count (); + +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql new file mode 100644 index 00000000..b25b3f1a --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql @@ -0,0 +1,32 @@ +-- Deploy schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table + +BEGIN; +CREATE FUNCTION app_jobs.tg_increase_job_queue_count () + RETURNS TRIGGER + AS $$ +BEGIN + INSERT INTO app_jobs.job_queues (queue_name, job_count) + VALUES (NEW.queue_name, 1) + ON CONFLICT (queue_name) + DO UPDATE SET + job_count = job_queues.job_count + 1; + RETURN NEW; +END; +$$ +LANGUAGE 'plpgsql' +VOLATILE; +CREATE TRIGGER _500_increase_job_queue_count_on_insert + AFTER INSERT ON app_jobs.jobs + FOR EACH ROW + WHEN ((NEW.queue_name IS NOT NULL)) + EXECUTE PROCEDURE app_jobs.tg_increase_job_queue_count (); +-- only a person would do this +CREATE TRIGGER _500_increase_job_queue_count_on_update + AFTER UPDATE OF queue_name ON app_jobs.jobs + FOR EACH ROW + WHEN (((NEW.queue_name IS DISTINCT FROM OLD.queue_name) AND (NEW.queue_name IS NOT NULL))) + EXECUTE PROCEDURE app_jobs.tg_increase_job_queue_count (); +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql new file mode 100644 index 00000000..e847ebbf --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql @@ -0,0 +1,23 @@ +-- Deploy schemas/app_jobs/tables/jobs/triggers/notify_worker to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table +-- requires: schemas/app_jobs/procedures/do_notify +-- requires: schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count + +BEGIN; +CREATE FUNCTION app_jobs.tg_jobs__after_insert () + RETURNS TRIGGER + AS $$ +BEGIN + PERFORM + pg_notify('jobs:insert', ''); + RETURN NULL; +END; +$$ +LANGUAGE plpgsql; + +CREATE TRIGGER _900_after_insert + AFTER INSERT ON app_jobs.jobs + FOR EACH STATEMENT + EXECUTE PROCEDURE app_jobs.tg_jobs__after_insert (); +COMMIT; diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/triggers/timestamps.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/triggers/timestamps.sql new file mode 100644 index 00000000..b2629695 --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/jobs/triggers/timestamps.sql @@ -0,0 +1,20 @@ +-- Deploy schemas/app_jobs/tables/jobs/triggers/timestamps to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table +-- requires: schemas/app_jobs/triggers/tg_update_timestamps + +BEGIN; +ALTER TABLE app_jobs.jobs + ADD COLUMN created_at timestamptz; +ALTER TABLE app_jobs.jobs + ALTER COLUMN created_at SET DEFAULT NOW(); +ALTER TABLE app_jobs.jobs + ADD COLUMN updated_at timestamptz; +ALTER TABLE app_jobs.jobs + ALTER COLUMN updated_at SET DEFAULT NOW(); +CREATE TRIGGER _100_update_jobs_modtime_tg + BEFORE UPDATE OR INSERT ON app_jobs.jobs + FOR EACH ROW + EXECUTE PROCEDURE app_jobs.tg_update_timestamps (); +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql new file mode 100644 index 00000000..914166bd --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql @@ -0,0 +1,12 @@ +-- Deploy schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator to pg + +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/scheduled_jobs/table + +BEGIN; + +-- TODO make sure to require any policies on this table! + +GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE app_jobs.scheduled_jobs TO administrator; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql new file mode 100644 index 00000000..d222737f --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql @@ -0,0 +1,8 @@ +-- Deploy schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/scheduled_jobs/table + +BEGIN; +CREATE INDEX scheduled_jobs_locked_by_idx ON app_jobs.scheduled_jobs (locked_by); +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql new file mode 100644 index 00000000..9bd54879 --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql @@ -0,0 +1,8 @@ +-- Deploy schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/scheduled_jobs/table + +BEGIN; +CREATE INDEX scheduled_jobs_priority_id_idx ON app_jobs.scheduled_jobs (priority, id); +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql new file mode 100644 index 00000000..bbf7820a --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql @@ -0,0 +1,47 @@ +-- Deploy schemas/app_jobs/tables/scheduled_jobs/table to pg +-- requires: schemas/app_jobs/schema + +BEGIN; +CREATE TABLE app_jobs.scheduled_jobs ( + id bigserial PRIMARY KEY, + database_id uuid, + actor_id uuid, + entity_id uuid, + queue_name text DEFAULT NULL, + task_identifier text NOT NULL, + payload json DEFAULT '{}' ::json NOT NULL, + priority integer DEFAULT 0 NOT NULL, + max_attempts integer DEFAULT 25 NOT NULL, + key text, + locked_at timestamptz, + locked_by text, + schedule_info json NOT NULL, + last_scheduled timestamptz, + last_scheduled_id bigint, + CHECK (length(key) < 513), + CHECK (length(task_identifier) < 127), + CHECK (max_attempts >= 1), + CHECK (length(queue_name) < 127), + CHECK (length(locked_by) > 3), + UNIQUE (key) +); + +COMMENT ON TABLE app_jobs.scheduled_jobs IS 'Recurring/cron-style job definitions: each row spawns jobs on a schedule, optionally scoped to a database'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.id IS 'Auto-incrementing scheduled job identifier'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.database_id IS 'Database this scheduled job belongs to (nullable for system-level schedules without tenant context)'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.actor_id IS 'User who created this scheduled job, read from JWT claims at creation time'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.entity_id IS 'Entity (org/team) this scheduled job is scoped to for billing; NULL means platform-level (resolved via database_id → owner_id)'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.queue_name IS 'Name of the queue spawned jobs are placed into'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.task_identifier IS 'Task type identifier for spawned jobs'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.payload IS 'JSON payload passed to each spawned job'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.priority IS 'Priority assigned to spawned jobs (lower = higher priority)'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.max_attempts IS 'Max retry attempts for spawned jobs'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.key IS 'Optional unique deduplication key'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.locked_at IS 'Timestamp when the scheduler locked this record for processing'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.locked_by IS 'Identifier of the scheduler worker holding the lock'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.schedule_info IS 'JSON schedule configuration (e.g. cron expression, interval)'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.last_scheduled IS 'Timestamp when a job was last spawned from this schedule'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.last_scheduled_id IS 'ID of the last job spawned from this schedule'; + +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql new file mode 100644 index 00000000..51e17d4c --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql @@ -0,0 +1,12 @@ +-- Deploy schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/scheduled_jobs/table +-- requires: schemas/app_jobs/procedures/do_notify + +BEGIN; +CREATE TRIGGER _900_notify_scheduled_job + AFTER INSERT ON app_jobs.scheduled_jobs + FOR EACH ROW + EXECUTE PROCEDURE app_jobs.do_notify ('scheduled_jobs:insert'); +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_fields.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_fields.sql new file mode 100644 index 00000000..314ad2ba --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_fields.sql @@ -0,0 +1,50 @@ +-- Deploy schemas/app_jobs/triggers/tg_add_job_with_fields to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/helpers/json_build_object_apply + +BEGIN; +CREATE FUNCTION app_jobs.trigger_job_with_fields () + RETURNS TRIGGER + AS $$ +DECLARE + arg text; + fn text; + i int; + args text[]; +BEGIN + FOR i IN + SELECT + * + FROM + generate_series(1, TG_NARGS) g (i) + LOOP + IF (i = 1) THEN + fn = TG_ARGV[i - 1]; + ELSE + args = array_append(args, TG_ARGV[i - 1]); + IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN + EXECUTE format('SELECT ($1).%s::text', TG_ARGV[i - 1]) + USING NEW INTO arg; + END IF; + IF (TG_OP = 'DELETE') THEN + EXECUTE format('SELECT ($1).%s::text', TG_ARGV[i - 1]) + USING OLD INTO arg; + END IF; + args = array_append(args, arg); + END IF; + END LOOP; + PERFORM + app_jobs.add_job (fn, app_jobs.json_build_object_apply (args)); + IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN + RETURN NEW; + END IF; + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + END IF; +END; +$$ +LANGUAGE plpgsql +VOLATILE +SECURITY DEFINER; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_row.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_row.sql new file mode 100644 index 00000000..6dec82ff --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_row.sql @@ -0,0 +1,26 @@ +-- Deploy schemas/app_jobs/triggers/tg_add_job_with_row to pg +-- requires: schemas/app_jobs/schema + +BEGIN; +CREATE FUNCTION app_jobs.tg_add_job_with_row () + RETURNS TRIGGER + AS $$ +BEGIN + IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN + PERFORM + app_jobs.add_job (TG_ARGV[0], to_json(NEW)); + RETURN NEW; + END IF; + IF (TG_OP = 'DELETE') THEN + PERFORM + app_jobs.add_job (TG_ARGV[0], to_json(OLD)); + RETURN OLD; + END IF; +END; +$$ +LANGUAGE plpgsql +VOLATILE +SECURITY DEFINER; +COMMENT ON FUNCTION app_jobs.tg_add_job_with_row IS E'Useful shortcut to create a job on insert or update. Pass the task name as the trigger argument, and the record data will automatically be available on the JSON payload.'; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql new file mode 100644 index 00000000..64ad8c30 --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql @@ -0,0 +1,27 @@ +-- Deploy schemas/app_jobs/triggers/tg_add_job_with_row_id to pg + +-- requires: schemas/app_jobs/schema + +BEGIN; +CREATE FUNCTION app_jobs.tg_add_job_with_row_id () + RETURNS TRIGGER + AS $$ +BEGIN + IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN + PERFORM + app_jobs.add_job (tg_argv[0], json_build_object('id', NEW.id)); + RETURN NEW; + END IF; + IF (TG_OP = 'DELETE') THEN + PERFORM + app_jobs.add_job (tg_argv[0], json_build_object('id', OLD.id)); + RETURN OLD; + END IF; +END; +$$ +LANGUAGE plpgsql +VOLATILE +SECURITY DEFINER; +COMMENT ON FUNCTION app_jobs.tg_add_job_with_row_id IS E'Useful shortcut to create a job on insert or update. Pass the task name as the trigger argument, and the record id will automatically be available on the JSON payload.'; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/triggers/tg_update_timestamps.sql b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/triggers/tg_update_timestamps.sql new file mode 100644 index 00000000..e74c4abf --- /dev/null +++ b/extensions/@pgpm/database-jobs/deploy/schemas/app_jobs/triggers/tg_update_timestamps.sql @@ -0,0 +1,21 @@ +-- Deploy schemas/app_jobs/triggers/tg_update_timestamps to pg +-- requires: schemas/app_jobs/schema + +BEGIN; +CREATE FUNCTION app_jobs.tg_update_timestamps () + RETURNS TRIGGER + AS $$ +BEGIN + IF TG_OP = 'INSERT' THEN + NEW.created_at = NOW(); + NEW.updated_at = NOW(); + ELSIF TG_OP = 'UPDATE' THEN + NEW.created_at = OLD.created_at; + NEW.updated_at = greatest (now(), OLD.updated_at + interval '1 millisecond'); + END IF; + RETURN NEW; +END; +$$ +LANGUAGE 'plpgsql'; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/package.json b/extensions/@pgpm/database-jobs/package.json new file mode 100644 index 00000000..5a0bcb55 --- /dev/null +++ b/extensions/@pgpm/database-jobs/package.json @@ -0,0 +1,39 @@ +{ + "name": "@pgpm/database-jobs", + "version": "0.26.5", + "description": "Database-specific job handling and queue management", + "author": "Dan Lynch ", + "contributors": [ + "Constructive " + ], + "keywords": [ + "postgresql", + "pgpm", + "jobs", + "queue" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "bundle": "pgpm package", + "test": "jest", + "test:watch": "jest --watch" + }, + "devDependencies": { + "pgpm": "^4.23.2" + }, + "dependencies": { + "@pgpm/jwt-claims": "0.26.0", + "@pgpm/verify": "0.26.0" + }, + "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" + }, + "gitHead": "a496a00d89c37d874f4a7207265b9972b6f05c7d" +} diff --git a/extensions/@pgpm/database-jobs/pgpm-database-jobs.control b/extensions/@pgpm/database-jobs/pgpm-database-jobs.control new file mode 100644 index 00000000..22f6e534 --- /dev/null +++ b/extensions/@pgpm/database-jobs/pgpm-database-jobs.control @@ -0,0 +1,8 @@ +# pgpm-database-jobs extension +comment = 'pgpm-database-jobs extension' +default_version = '0.26.3' +module_pathname = '$libdir/pgpm-database-jobs' +requires = 'plpgsql,pgcrypto,pgpm-verify,pgpm-jwt-claims' +relocatable = false +superuser = false + diff --git a/extensions/@pgpm/database-jobs/pgpm.plan b/extensions/@pgpm/database-jobs/pgpm.plan new file mode 100644 index 00000000..b7f96107 --- /dev/null +++ b/extensions/@pgpm/database-jobs/pgpm.plan @@ -0,0 +1,41 @@ +%syntax-version=1.0.0 +%project=pgpm-database-jobs +%uri=pgpm-database-jobs +schemas/app_jobs/schema [pgpm-verify:@0.1.0] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/schema +schemas/app_jobs/triggers/tg_update_timestamps [schemas/app_jobs/schema] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/triggers/tg_update_timestamps +schemas/app_jobs/triggers/tg_add_job_with_row_id [schemas/app_jobs/schema] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/triggers/tg_add_job_with_row_id +schemas/app_jobs/triggers/tg_add_job_with_row [schemas/app_jobs/schema] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/triggers/tg_add_job_with_row +schemas/app_jobs/helpers/json_build_object_apply [schemas/app_jobs/schema] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/helpers/json_build_object_apply +schemas/app_jobs/triggers/tg_add_job_with_fields [schemas/app_jobs/schema schemas/app_jobs/helpers/json_build_object_apply] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/triggers/tg_add_job_with_fields +schemas/app_jobs/tables/scheduled_jobs/table [schemas/app_jobs/schema] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/scheduled_jobs/table +schemas/app_jobs/procedures/do_notify [schemas/app_jobs/schema] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/do_notify +schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job [schemas/app_jobs/schema schemas/app_jobs/tables/scheduled_jobs/table schemas/app_jobs/procedures/do_notify] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job +schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx [schemas/app_jobs/schema schemas/app_jobs/tables/scheduled_jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx +schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx [schemas/app_jobs/schema schemas/app_jobs/tables/scheduled_jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx +schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator [schemas/app_jobs/schema schemas/app_jobs/tables/scheduled_jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator +schemas/app_jobs/tables/jobs/table [schemas/app_jobs/schema] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/jobs/table +schemas/app_jobs/tables/jobs/triggers/timestamps [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/triggers/tg_update_timestamps] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/jobs/triggers/timestamps +schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count +schemas/app_jobs/tables/jobs/triggers/notify_worker [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/procedures/do_notify schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/jobs/triggers/notify_worker +schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count +schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx +schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx +schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator +schemas/app_jobs/tables/job_queues/table [schemas/app_jobs/schema] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/job_queues/table +schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx [schemas/app_jobs/schema schemas/app_jobs/tables/job_queues/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx +schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator [schemas/app_jobs/schema schemas/app_jobs/tables/job_queues/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator +schemas/app_jobs/procedures/run_scheduled_job [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/scheduled_jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/run_scheduled_job +schemas/app_jobs/procedures/reschedule_jobs [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/reschedule_jobs +schemas/app_jobs/procedures/release_scheduled_jobs [schemas/app_jobs/schema schemas/app_jobs/tables/scheduled_jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/release_scheduled_jobs +schemas/app_jobs/procedures/release_jobs [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/job_queues/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/release_jobs +schemas/app_jobs/procedures/permanently_fail_jobs [schemas/app_jobs/schema schemas/app_jobs/tables/job_queues/table schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/permanently_fail_jobs +schemas/app_jobs/procedures/get_scheduled_job [schemas/app_jobs/schema schemas/app_jobs/tables/scheduled_jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/get_scheduled_job +schemas/app_jobs/procedures/get_job [schemas/app_jobs/schema schemas/app_jobs/tables/job_queues/table schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/get_job +schemas/app_jobs/procedures/fail_job [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/job_queues/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/fail_job +schemas/app_jobs/procedures/complete_jobs [schemas/app_jobs/schema schemas/app_jobs/tables/job_queues/table schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/complete_jobs +schemas/app_jobs/procedures/complete_job [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/job_queues/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/complete_job +schemas/app_jobs/procedures/add_scheduled_job [schemas/app_jobs/schema schemas/app_jobs/tables/scheduled_jobs/table pgpm-jwt-claims:schemas/jwt_private/procedures/current_database_id] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/add_scheduled_job +schemas/app_jobs/procedures/add_job [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/job_queues/table pgpm-jwt-claims:schemas/jwt_private/procedures/current_database_id pgpm-jwt-claims:schemas/jwt_public/procedures/current_user_id] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/add_job +schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated [schemas/app_jobs/schema schemas/app_jobs/procedures/add_job] 2026-06-03T01:15:00Z pgpm # grant authenticated EXECUTE on add_job for INVOKER trigger support +schemas/app_jobs/procedures/remove_job [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/remove_job +schemas/app_jobs/procedures/force_unlock_workers [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/job_queues/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/force_unlock_workers diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/helpers/json_build_object_apply.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/helpers/json_build_object_apply.sql new file mode 100644 index 00000000..b1778898 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/helpers/json_build_object_apply.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/helpers/json_build_object_apply from pg + +BEGIN; + +DROP FUNCTION app_jobs.json_build_object_apply; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/add_job.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/add_job.sql new file mode 100644 index 00000000..44a65ae8 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/add_job.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/add_job from pg + +BEGIN; + +DROP FUNCTION app_jobs.add_job; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/add_scheduled_job.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/add_scheduled_job.sql new file mode 100644 index 00000000..882a98f9 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/add_scheduled_job.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/add_scheduled_job from pg + +BEGIN; + +DROP FUNCTION app_jobs.add_scheduled_job; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/complete_job.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/complete_job.sql new file mode 100644 index 00000000..7c0ea9df --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/complete_job.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/complete_job from pg + +BEGIN; + +DROP FUNCTION app_jobs.complete_job; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/complete_jobs.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/complete_jobs.sql new file mode 100644 index 00000000..3db9150e --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/complete_jobs.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/complete_jobs from pg + +BEGIN; + +DROP FUNCTION app_jobs.complete_jobs; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/do_notify.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/do_notify.sql new file mode 100644 index 00000000..58a8138a --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/do_notify.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/do_notify from pg + +BEGIN; + +DROP FUNCTION app_jobs.do_notify; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/fail_job.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/fail_job.sql new file mode 100644 index 00000000..ed96e401 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/fail_job.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/fail_job from pg + +BEGIN; + +DROP FUNCTION app_jobs.fail_job; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/force_unlock_workers.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/force_unlock_workers.sql new file mode 100644 index 00000000..aac5d270 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/force_unlock_workers.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/force_unlock_workers from pg + +BEGIN; + +DROP FUNCTION app_jobs.force_unlock_workers; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/get_job.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/get_job.sql new file mode 100644 index 00000000..469f6b4d --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/get_job.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/get_job from pg + +BEGIN; + +DROP FUNCTION app_jobs.get_job; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/get_scheduled_job.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/get_scheduled_job.sql new file mode 100644 index 00000000..f41f8fdb --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/get_scheduled_job.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/get_scheduled_job from pg + +BEGIN; + +DROP FUNCTION app_jobs.get_scheduled_job; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated.sql new file mode 100644 index 00000000..e020b77c --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated from pg + +BEGIN; + +REVOKE EXECUTE ON FUNCTION app_jobs.add_job(text, json, text, text, timestamptz, integer, integer, uuid, uuid, text) FROM authenticated; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/permanently_fail_jobs.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/permanently_fail_jobs.sql new file mode 100644 index 00000000..f0299ea8 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/permanently_fail_jobs.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/permanently_fail_jobs from pg + +BEGIN; + +DROP FUNCTION app_jobs.permanently_fail_jobs; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/release_jobs.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/release_jobs.sql new file mode 100644 index 00000000..8ece69ef --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/release_jobs.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/release_jobs from pg + +BEGIN; + +DROP FUNCTION app_jobs.release_jobs; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/release_scheduled_jobs.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/release_scheduled_jobs.sql new file mode 100644 index 00000000..a16e6e9a --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/release_scheduled_jobs.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/release_scheduled_jobs from pg + +BEGIN; + +DROP FUNCTION app_jobs.release_scheduled_jobs; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/remove_job.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/remove_job.sql new file mode 100644 index 00000000..e673bdee --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/remove_job.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/remove_job from pg + +BEGIN; + +DROP FUNCTION app_jobs.remove_job; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/reschedule_jobs.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/reschedule_jobs.sql new file mode 100644 index 00000000..34a44171 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/reschedule_jobs.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/reschedule_jobs from pg + +BEGIN; + +DROP FUNCTION app_jobs.reschedule_jobs; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/run_scheduled_job.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/run_scheduled_job.sql new file mode 100644 index 00000000..77886fc0 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/procedures/run_scheduled_job.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/run_scheduled_job from pg + +BEGIN; + +DROP FUNCTION app_jobs.run_scheduled_job; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/schema.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/schema.sql new file mode 100644 index 00000000..2b238d0f --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/schema.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/schema from pg + +BEGIN; + +DROP SCHEMA app_jobs; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql new file mode 100644 index 00000000..06a83378 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator from pg + +BEGIN; + +REVOKE SELECT, INSERT, UPDATE, DELETE ON TABLE app_jobs.job_queues FROM administrator; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql new file mode 100644 index 00000000..20290a2a --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx from pg + +BEGIN; + +DROP INDEX app_jobs.job_queues_locked_by_idx; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/job_queues/table.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/job_queues/table.sql new file mode 100644 index 00000000..79c62cbc --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/job_queues/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/job_queues/table from pg + +BEGIN; + +DROP TABLE app_jobs.job_queues; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql new file mode 100644 index 00000000..c67b07e2 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator from pg + +BEGIN; + +REVOKE SELECT, INSERT, UPDATE, DELETE ON TABLE app_jobs.jobs FROM administrator; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql new file mode 100644 index 00000000..f26cb13e --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx from pg + +BEGIN; + +DROP INDEX app_jobs.jobs_locked_by_idx; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql new file mode 100644 index 00000000..2268c2a0 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql @@ -0,0 +1,9 @@ +-- Revert schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx from pg + +BEGIN; + +DROP INDEX IF EXISTS app_jobs.priority_run_at_id_idx; +DROP INDEX IF EXISTS app_jobs.jobs_main_index; +DROP INDEX IF EXISTS app_jobs.jobs_no_queue_index; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/table.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/table.sql new file mode 100644 index 00000000..b4156ad3 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/jobs/table from pg + +BEGIN; + +DROP TABLE app_jobs.jobs; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql new file mode 100644 index 00000000..bf4f88c6 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count from pg +BEGIN; +DROP TRIGGER decrease_job_queue_count_on_delete ON app_jobs.jobs; +DROP TRIGGER decrease_job_queue_count_on_update ON app_jobs.jobs; +DROP FUNCTION app_jobs.tg_decrease_job_queue_count; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql new file mode 100644 index 00000000..5098a651 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count from pg +BEGIN; +DROP TRIGGER _500_increase_job_queue_count_on_insert ON app_jobs.jobs; +DROP TRIGGER _500_increase_job_queue_count_on_update ON app_jobs.jobs; +DROP FUNCTION app_jobs.tg_increase_job_queue_count; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql new file mode 100644 index 00000000..37a5f531 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql @@ -0,0 +1,6 @@ +-- Revert schemas/app_jobs/tables/jobs/triggers/notify_worker from pg +BEGIN; +DROP TRIGGER IF EXISTS _900_notify_worker ON app_jobs.jobs; +DROP TRIGGER IF EXISTS _900_after_insert ON app_jobs.jobs; +DROP FUNCTION IF EXISTS app_jobs.tg_jobs__after_insert; +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/timestamps.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/timestamps.sql new file mode 100644 index 00000000..7dc2f048 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/timestamps.sql @@ -0,0 +1,9 @@ +-- Revert schemas/app_jobs/tables/jobs/triggers/timestamps from pg +BEGIN; +ALTER TABLE app_jobs.jobs + DROP COLUMN created_at; +ALTER TABLE app_jobs.jobs + DROP COLUMN updated_at; +DROP TRIGGER _100_update_jobs_modtime_tg ON app_jobs.jobs; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql new file mode 100644 index 00000000..0990e98d --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator from pg + +BEGIN; + +REVOKE SELECT, INSERT, UPDATE, DELETE ON TABLE app_jobs.scheduled_jobs FROM administrator; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql new file mode 100644 index 00000000..5ff1e6d5 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx from pg + +BEGIN; + +DROP INDEX app_jobs.scheduled_jobs_locked_by_idx; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql new file mode 100644 index 00000000..be4b5878 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx from pg + +BEGIN; + +DROP INDEX app_jobs.scheduled_jobs_priority_id_idx; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/scheduled_jobs/table.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/scheduled_jobs/table.sql new file mode 100644 index 00000000..3a06f0da --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/scheduled_jobs/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/scheduled_jobs/table from pg + +BEGIN; + +DROP TABLE app_jobs.scheduled_jobs; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql new file mode 100644 index 00000000..5c1852c7 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql @@ -0,0 +1,8 @@ +-- Revert schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job from pg + +BEGIN; + +DROP TRIGGER _900_notify_scheduled_job ON app_jobs.scheduled_jobs; + + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_fields.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_fields.sql new file mode 100644 index 00000000..5384edfc --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_fields.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/triggers/tg_add_job_with_fields from pg + +BEGIN; + +DROP FUNCTION app_jobs.trigger_job_with_fields; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row.sql new file mode 100644 index 00000000..9d6b68a2 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/triggers/tg_add_job_with_row from pg + +BEGIN; + +DROP FUNCTION app_jobs.tg_add_job_with_row; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql new file mode 100644 index 00000000..1f0fb04b --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql @@ -0,0 +1,5 @@ +-- Revert schemas/app_jobs/triggers/tg_add_job_with_row_id from pg +BEGIN; +DROP FUNCTION app_jobs.tg_add_job_with_row_id; +COMMIT; + diff --git a/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/triggers/tg_update_timestamps.sql b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/triggers/tg_update_timestamps.sql new file mode 100644 index 00000000..37378b14 --- /dev/null +++ b/extensions/@pgpm/database-jobs/revert/schemas/app_jobs/triggers/tg_update_timestamps.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/triggers/tg_update_timestamps from pg + +BEGIN; + +DROP FUNCTION app_jobs.tg_update_timestamps; + +COMMIT; diff --git a/extensions/@pgpm/database-jobs/sql/pgpm-database-jobs--0.26.3.sql b/extensions/@pgpm/database-jobs/sql/pgpm-database-jobs--0.26.3.sql new file mode 100644 index 00000000..ab219b9e --- /dev/null +++ b/extensions/@pgpm/database-jobs/sql/pgpm-database-jobs--0.26.3.sql @@ -0,0 +1,916 @@ +\echo Use "CREATE EXTENSION pgpm-database-jobs" to load this file. \quit +CREATE SCHEMA IF NOT EXISTS app_jobs; + +GRANT USAGE ON SCHEMA app_jobs TO administrator; + +GRANT USAGE ON SCHEMA app_jobs TO authenticated; + +ALTER DEFAULT PRIVILEGES IN SCHEMA app_jobs + GRANT EXECUTE ON FUNCTIONS TO administrator; + +CREATE FUNCTION app_jobs.tg_update_timestamps() RETURNS trigger AS $EOFCODE$ +BEGIN + IF TG_OP = 'INSERT' THEN + NEW.created_at = NOW(); + NEW.updated_at = NOW(); + ELSIF TG_OP = 'UPDATE' THEN + NEW.created_at = OLD.created_at; + NEW.updated_at = greatest (now(), OLD.updated_at + interval '1 millisecond'); + END IF; + RETURN NEW; +END; +$EOFCODE$ LANGUAGE plpgsql; + +CREATE FUNCTION app_jobs.tg_add_job_with_row_id() RETURNS trigger AS $EOFCODE$ +BEGIN + IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN + PERFORM + app_jobs.add_job (tg_argv[0], json_build_object('id', NEW.id)); + RETURN NEW; + END IF; + IF (TG_OP = 'DELETE') THEN + PERFORM + app_jobs.add_job (tg_argv[0], json_build_object('id', OLD.id)); + RETURN OLD; + END IF; +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER; + +COMMENT ON FUNCTION app_jobs.tg_add_job_with_row_id IS 'Useful shortcut to create a job on insert or update. Pass the task name as the trigger argument, and the record id will automatically be available on the JSON payload.'; + +CREATE FUNCTION app_jobs.tg_add_job_with_row() RETURNS trigger AS $EOFCODE$ +BEGIN + IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN + PERFORM + app_jobs.add_job (TG_ARGV[0], to_json(NEW)); + RETURN NEW; + END IF; + IF (TG_OP = 'DELETE') THEN + PERFORM + app_jobs.add_job (TG_ARGV[0], to_json(OLD)); + RETURN OLD; + END IF; +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER; + +COMMENT ON FUNCTION app_jobs.tg_add_job_with_row IS 'Useful shortcut to create a job on insert or update. Pass the task name as the trigger argument, and the record data will automatically be available on the JSON payload.'; + +CREATE FUNCTION app_jobs.json_build_object_apply(arguments text[]) RETURNS pg_catalog.json AS $EOFCODE$ +DECLARE + arg text; + _sql text; + _res json; + args text[]; +BEGIN + _sql = 'SELECT json_build_object('; + FOR arg IN + SELECT + unnest(arguments) + LOOP + args = array_append(args, format('''%s''', arg)); + END LOOP; + _sql = _sql || format('%s);', array_to_string(args, ',')); + EXECUTE _sql INTO _res; + RETURN _res; +END; +$EOFCODE$ LANGUAGE plpgsql; + +CREATE FUNCTION app_jobs.trigger_job_with_fields() RETURNS trigger AS $EOFCODE$ +DECLARE + arg text; + fn text; + i int; + args text[]; +BEGIN + FOR i IN + SELECT + * + FROM + generate_series(1, TG_NARGS) g (i) + LOOP + IF (i = 1) THEN + fn = TG_ARGV[i - 1]; + ELSE + args = array_append(args, TG_ARGV[i - 1]); + IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN + EXECUTE format('SELECT ($1).%s::text', TG_ARGV[i - 1]) + USING NEW INTO arg; + END IF; + IF (TG_OP = 'DELETE') THEN + EXECUTE format('SELECT ($1).%s::text', TG_ARGV[i - 1]) + USING OLD INTO arg; + END IF; + args = array_append(args, arg); + END IF; + END LOOP; + PERFORM + app_jobs.add_job (fn, app_jobs.json_build_object_apply (args)); + IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN + RETURN NEW; + END IF; + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + END IF; +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER; + +CREATE TABLE app_jobs.scheduled_jobs ( + id bigserial PRIMARY KEY, + database_id uuid, + actor_id uuid, + entity_id uuid, + queue_name text DEFAULT NULL, + task_identifier text NOT NULL, + payload pg_catalog.json DEFAULT '{}'::json NOT NULL, + priority int DEFAULT 0 NOT NULL, + max_attempts int DEFAULT 25 NOT NULL, + key text, + locked_at timestamptz, + locked_by text, + schedule_info pg_catalog.json NOT NULL, + last_scheduled timestamptz, + last_scheduled_id bigint, + CHECK (length(key) < 513), + CHECK (length(task_identifier) < 127), + CHECK (max_attempts >= 1), + CHECK (length(queue_name) < 127), + CHECK (length(locked_by) > 3), + UNIQUE (key) +); + +COMMENT ON TABLE app_jobs.scheduled_jobs IS 'Recurring/cron-style job definitions: each row spawns jobs on a schedule, optionally scoped to a database'; + +COMMENT ON COLUMN app_jobs.scheduled_jobs.id IS 'Auto-incrementing scheduled job identifier'; + +COMMENT ON COLUMN app_jobs.scheduled_jobs.database_id IS 'Database this scheduled job belongs to (nullable for system-level schedules without tenant context)'; + +COMMENT ON COLUMN app_jobs.scheduled_jobs.actor_id IS 'User who created this scheduled job, read from JWT claims at creation time'; + +COMMENT ON COLUMN app_jobs.scheduled_jobs.entity_id IS 'Entity (org/team) this scheduled job is scoped to for billing; NULL means platform-level (resolved via database_id → owner_id)'; + +COMMENT ON COLUMN app_jobs.scheduled_jobs.queue_name IS 'Name of the queue spawned jobs are placed into'; + +COMMENT ON COLUMN app_jobs.scheduled_jobs.task_identifier IS 'Task type identifier for spawned jobs'; + +COMMENT ON COLUMN app_jobs.scheduled_jobs.payload IS 'JSON payload passed to each spawned job'; + +COMMENT ON COLUMN app_jobs.scheduled_jobs.priority IS 'Priority assigned to spawned jobs (lower = higher priority)'; + +COMMENT ON COLUMN app_jobs.scheduled_jobs.max_attempts IS 'Max retry attempts for spawned jobs'; + +COMMENT ON COLUMN app_jobs.scheduled_jobs.key IS 'Optional unique deduplication key'; + +COMMENT ON COLUMN app_jobs.scheduled_jobs.locked_at IS 'Timestamp when the scheduler locked this record for processing'; + +COMMENT ON COLUMN app_jobs.scheduled_jobs.locked_by IS 'Identifier of the scheduler worker holding the lock'; + +COMMENT ON COLUMN app_jobs.scheduled_jobs.schedule_info IS 'JSON schedule configuration (e.g. cron expression, interval)'; + +COMMENT ON COLUMN app_jobs.scheduled_jobs.last_scheduled IS 'Timestamp when a job was last spawned from this schedule'; + +COMMENT ON COLUMN app_jobs.scheduled_jobs.last_scheduled_id IS 'ID of the last job spawned from this schedule'; + +CREATE FUNCTION app_jobs.do_notify() RETURNS trigger AS $EOFCODE$ +BEGIN + PERFORM + pg_notify(TG_ARGV[0], ''); + RETURN NEW; +END; +$EOFCODE$ LANGUAGE plpgsql; + +CREATE TRIGGER _900_notify_scheduled_job + AFTER INSERT + ON app_jobs.scheduled_jobs + FOR EACH ROW + EXECUTE PROCEDURE app_jobs.do_notify('scheduled_jobs:insert'); + +CREATE INDEX scheduled_jobs_priority_id_idx ON app_jobs.scheduled_jobs (priority, id); + +CREATE INDEX scheduled_jobs_locked_by_idx ON app_jobs.scheduled_jobs (locked_by); + +GRANT SELECT, INSERT, UPDATE, DELETE ON app_jobs.scheduled_jobs TO administrator; + +CREATE TABLE app_jobs.jobs ( + id bigserial PRIMARY KEY, + database_id uuid, + actor_id uuid, + entity_id uuid, + organization_id uuid, + entity_type text, + queue_name text DEFAULT NULL, + task_identifier text NOT NULL, + payload pg_catalog.json DEFAULT '{}'::json NOT NULL, + priority int DEFAULT 0 NOT NULL, + run_at timestamptz DEFAULT now() NOT NULL, + attempts int DEFAULT 0 NOT NULL, + max_attempts int DEFAULT 25 NOT NULL, + key text, + last_error text, + locked_at timestamptz, + locked_by text, + is_available boolean GENERATED ALWAYS AS (locked_at IS NULL + AND attempts < max_attempts) STORED NOT NULL, + CHECK (length(key) < 513), + CHECK (length(task_identifier) < 127), + CHECK (max_attempts >= 1), + CHECK (length(queue_name) < 127), + CHECK (length(locked_by) > 3), + UNIQUE (key) +); + +COMMENT ON TABLE app_jobs.jobs IS 'Background job queue: each row is a pending or in-progress task, optionally scoped to a database'; + +COMMENT ON COLUMN app_jobs.jobs.id IS 'Auto-incrementing job identifier'; + +COMMENT ON COLUMN app_jobs.jobs.database_id IS 'Database this job belongs to (nullable for system-level jobs without tenant context)'; + +COMMENT ON COLUMN app_jobs.jobs.actor_id IS 'User who triggered this job, read from JWT claims at enqueue time'; + +COMMENT ON COLUMN app_jobs.jobs.entity_id IS 'Entity (org/team) this job is scoped to for billing; NULL means platform-level (resolved via database_id → owner_id)'; + +COMMENT ON COLUMN app_jobs.jobs.organization_id IS 'Top-level organization for this entity; resolved at enqueue time via get_organization_id(entity_type, entity_id)'; + +COMMENT ON COLUMN app_jobs.jobs.entity_type IS 'Entity type prefix (org, team, app, etc.) for interpreting entity_id'; + +COMMENT ON COLUMN app_jobs.jobs.queue_name IS 'Name of the queue this job belongs to; used for worker routing and concurrency control'; + +COMMENT ON COLUMN app_jobs.jobs.task_identifier IS 'Identifier for the task type (maps to a worker handler function)'; + +COMMENT ON COLUMN app_jobs.jobs.payload IS 'JSON payload of arguments passed to the task handler'; + +COMMENT ON COLUMN app_jobs.jobs.priority IS 'Execution priority; lower numbers run first (default 0)'; + +COMMENT ON COLUMN app_jobs.jobs.run_at IS 'Earliest time this job should be executed; used for delayed/scheduled execution'; + +COMMENT ON COLUMN app_jobs.jobs.attempts IS 'Number of times this job has been attempted so far'; + +COMMENT ON COLUMN app_jobs.jobs.max_attempts IS 'Maximum retry attempts before the job is considered permanently failed'; + +COMMENT ON COLUMN app_jobs.jobs.key IS 'Optional unique deduplication key; prevents duplicate jobs with the same key'; + +COMMENT ON COLUMN app_jobs.jobs.last_error IS 'Error message from the most recent failed attempt'; + +COMMENT ON COLUMN app_jobs.jobs.locked_at IS 'Timestamp when a worker locked this job for processing'; + +COMMENT ON COLUMN app_jobs.jobs.locked_by IS 'Identifier of the worker that currently holds the lock'; + +COMMENT ON COLUMN app_jobs.jobs.is_available IS 'Generated column: true when job is unlocked and has remaining attempts'; + +ALTER TABLE app_jobs.jobs + ADD COLUMN created_at timestamptz; + +ALTER TABLE app_jobs.jobs + ALTER COLUMN created_at SET DEFAULT now(); + +ALTER TABLE app_jobs.jobs + ADD COLUMN updated_at timestamptz; + +ALTER TABLE app_jobs.jobs + ALTER COLUMN updated_at SET DEFAULT now(); + +CREATE TRIGGER _100_update_jobs_modtime_tg + BEFORE INSERT OR UPDATE + ON app_jobs.jobs + FOR EACH ROW + EXECUTE PROCEDURE app_jobs.tg_update_timestamps(); + +CREATE FUNCTION app_jobs.tg_increase_job_queue_count() RETURNS trigger AS $EOFCODE$ +BEGIN + INSERT INTO app_jobs.job_queues (queue_name, job_count) + VALUES (NEW.queue_name, 1) + ON CONFLICT (queue_name) + DO UPDATE SET + job_count = job_queues.job_count + 1; + RETURN NEW; +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE; + +CREATE TRIGGER _500_increase_job_queue_count_on_insert + AFTER INSERT + ON app_jobs.jobs + FOR EACH ROW + WHEN (new.queue_name IS NOT NULL) + EXECUTE PROCEDURE app_jobs.tg_increase_job_queue_count(); + +CREATE TRIGGER _500_increase_job_queue_count_on_update + AFTER UPDATE OF queue_name + ON app_jobs.jobs + FOR EACH ROW + WHEN (new.queue_name IS DISTINCT FROM old.queue_name + AND new.queue_name IS NOT NULL) + EXECUTE PROCEDURE app_jobs.tg_increase_job_queue_count(); + +CREATE FUNCTION app_jobs.tg_jobs__after_insert() RETURNS trigger AS $EOFCODE$ +BEGIN + PERFORM + pg_notify('jobs:insert', ''); + RETURN NULL; +END; +$EOFCODE$ LANGUAGE plpgsql; + +CREATE TRIGGER _900_after_insert + AFTER INSERT + ON app_jobs.jobs + FOR EACH STATEMENT + EXECUTE PROCEDURE app_jobs.tg_jobs__after_insert(); + +CREATE FUNCTION app_jobs.tg_decrease_job_queue_count() RETURNS trigger AS $EOFCODE$ +DECLARE + v_new_job_count int; +BEGIN + UPDATE + app_jobs.job_queues + SET + job_count = job_queues.job_count - 1 + WHERE + queue_name = OLD.queue_name + RETURNING + job_count INTO v_new_job_count; + IF v_new_job_count <= 0 THEN + DELETE FROM app_jobs.job_queues + WHERE queue_name = OLD.queue_name + AND job_count <= 0; + END IF; + RETURN OLD; +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE; + +CREATE TRIGGER decrease_job_queue_count_on_delete + AFTER DELETE + ON app_jobs.jobs + FOR EACH ROW + WHEN (old.queue_name IS NOT NULL) + EXECUTE PROCEDURE app_jobs.tg_decrease_job_queue_count(); + +CREATE TRIGGER decrease_job_queue_count_on_update + AFTER UPDATE OF queue_name + ON app_jobs.jobs + FOR EACH ROW + WHEN (new.queue_name IS DISTINCT FROM old.queue_name + AND old.queue_name IS NOT NULL) + EXECUTE PROCEDURE app_jobs.tg_decrease_job_queue_count(); + +CREATE INDEX jobs_main_index ON app_jobs.jobs (priority, run_at) INCLUDE (id, queue_name) WHERE is_available = true; + +CREATE INDEX jobs_no_queue_index ON app_jobs.jobs (priority, run_at) INCLUDE (id) WHERE is_available = true + AND queue_name IS NULL; + +CREATE INDEX jobs_locked_by_idx ON app_jobs.jobs (locked_by); + +GRANT SELECT, INSERT, UPDATE, DELETE ON app_jobs.jobs TO administrator; + +CREATE TABLE app_jobs.job_queues ( + queue_name text NOT NULL PRIMARY KEY, + job_count int DEFAULT 0 NOT NULL, + locked_at timestamptz, + locked_by text +); + +COMMENT ON TABLE app_jobs.job_queues IS 'Queue metadata: tracks job counts and locking state for each named queue'; + +COMMENT ON COLUMN app_jobs.job_queues.queue_name IS 'Unique name identifying this queue'; + +COMMENT ON COLUMN app_jobs.job_queues.job_count IS 'Number of pending jobs in this queue'; + +COMMENT ON COLUMN app_jobs.job_queues.locked_at IS 'Timestamp when this queue was locked for batch processing'; + +COMMENT ON COLUMN app_jobs.job_queues.locked_by IS 'Identifier of the worker that currently holds the queue lock'; + +CREATE INDEX job_queues_locked_by_idx ON app_jobs.job_queues (locked_by); + +GRANT SELECT, INSERT, UPDATE, DELETE ON app_jobs.job_queues TO administrator; + +CREATE FUNCTION app_jobs.run_scheduled_job(id bigint, job_expiry interval DEFAULT '1 hours') RETURNS app_jobs.jobs AS $EOFCODE$ +DECLARE + j app_jobs.jobs; + last_id bigint; + lkd_by text; +BEGIN + -- check last scheduled + SELECT + last_scheduled_id + FROM + app_jobs.scheduled_jobs s + WHERE + s.id = run_scheduled_job.id INTO last_id; + + -- if it's been scheduled check if it's been run + + IF (last_id IS NOT NULL) THEN + SELECT + locked_by + FROM + app_jobs.jobs js + WHERE + js.id = last_id + AND (js.locked_at IS NULL -- never been run + OR js.locked_at >= (NOW() - job_expiry) + -- still running within a safe interval +) INTO lkd_by; + IF (FOUND) THEN + RAISE EXCEPTION 'ALREADY_SCHEDULED'; + END IF; + END IF; + + -- insert new job + INSERT INTO app_jobs.jobs ( + database_id, + actor_id, + entity_id, + queue_name, + task_identifier, + payload, + priority, + max_attempts, + key + ) SELECT + database_id, + actor_id, + entity_id, + queue_name, + task_identifier, + payload, + priority, + max_attempts, + key + FROM + app_jobs.scheduled_jobs s + WHERE + s.id = run_scheduled_job.id + RETURNING + * INTO j; + -- update the scheduled job + UPDATE + app_jobs.scheduled_jobs s + SET + last_scheduled = NOW(), + last_scheduled_id = j.id + WHERE + s.id = run_scheduled_job.id; + RETURN j; +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE; + +CREATE FUNCTION app_jobs.reschedule_jobs(job_ids bigint[], run_at timestamptz DEFAULT NULL, priority int DEFAULT NULL, attempts int DEFAULT NULL, max_attempts int DEFAULT NULL) RETURNS SETOF app_jobs.jobs LANGUAGE sql AS $EOFCODE$ + UPDATE + app_jobs.jobs + SET + run_at = coalesce(reschedule_jobs.run_at, jobs.run_at), + priority = coalesce(reschedule_jobs.priority, jobs.priority), + attempts = coalesce(reschedule_jobs.attempts, jobs.attempts), + max_attempts = coalesce(reschedule_jobs.max_attempts, jobs.max_attempts) + WHERE + id = ANY (job_ids) + AND (locked_by IS NULL + OR locked_at < NOW() - interval '4 hours') + RETURNING + *; +$EOFCODE$; + +CREATE FUNCTION app_jobs.release_scheduled_jobs(worker_id text, ids bigint[] DEFAULT NULL) RETURNS void AS $EOFCODE$ +DECLARE +BEGIN + -- clear the scheduled job + UPDATE + app_jobs.scheduled_jobs s + SET + locked_at = NULL, + locked_by = NULL + WHERE + locked_by = worker_id + AND (ids IS NULL + OR s.id = ANY (ids)); +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE; + +CREATE FUNCTION app_jobs.release_jobs(worker_id text) RETURNS void AS $EOFCODE$ +DECLARE +BEGIN + -- clear the job + UPDATE + app_jobs.jobs + SET + locked_at = NULL, + locked_by = NULL, + attempts = GREATEST (attempts - 1, 0) + WHERE + locked_by = worker_id; + -- clear the queue + UPDATE + app_jobs.job_queues + SET + locked_at = NULL, + locked_by = NULL + WHERE + locked_by = worker_id; +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE; + +CREATE FUNCTION app_jobs.permanently_fail_jobs(job_ids bigint[], error_message text DEFAULT NULL) RETURNS SETOF app_jobs.jobs LANGUAGE sql AS $EOFCODE$ + UPDATE + app_jobs.jobs + SET + last_error = coalesce(error_message, 'Manually marked as failed'), + attempts = max_attempts + WHERE + id = ANY (job_ids) + AND (locked_by IS NULL + OR locked_at < NOW() - interval '4 hours') + RETURNING + *; +$EOFCODE$; + +CREATE FUNCTION app_jobs.get_scheduled_job(worker_id text, task_identifiers text[] DEFAULT NULL) RETURNS app_jobs.scheduled_jobs LANGUAGE plpgsql AS $EOFCODE$ +DECLARE + v_job_id bigint; + v_row app_jobs.scheduled_jobs; +BEGIN + + -- + + IF worker_id IS NULL THEN + RAISE exception 'INVALID_WORKER_ID'; + END IF; + + -- + + SELECT + scheduled_jobs.id INTO v_job_id + FROM + app_jobs.scheduled_jobs + WHERE (scheduled_jobs.locked_at IS NULL) + AND (task_identifiers IS NULL + OR task_identifier = ANY (task_identifiers)) + ORDER BY + priority ASC, + id ASC + LIMIT 1 + FOR UPDATE + SKIP LOCKED; + + -- + + IF v_job_id IS NULL THEN + RETURN NULL; + END IF; + + -- + + UPDATE + app_jobs.scheduled_jobs + SET + locked_by = worker_id, + locked_at = NOW() + WHERE + id = v_job_id + RETURNING + * INTO v_row; + + -- + + RETURN v_row; +END; +$EOFCODE$; + +CREATE FUNCTION app_jobs.get_job(worker_id text, task_identifiers text[] DEFAULT NULL, job_expiry interval DEFAULT '4 hours') RETURNS app_jobs.jobs LANGUAGE plpgsql AS $EOFCODE$ +DECLARE + v_job_id bigint; + v_queue_name text; + v_row app_jobs.jobs; + v_now timestamptz = now(); +BEGIN + IF worker_id IS NULL THEN + RAISE EXCEPTION 'INVALID_WORKER_ID'; + END IF; + + SELECT jobs.queue_name, jobs.id + INTO v_queue_name, v_job_id + FROM app_jobs.jobs + WHERE is_available = true + AND (jobs.locked_at IS NULL + OR jobs.locked_at < (v_now - job_expiry)) + AND (jobs.queue_name IS NULL + OR jobs.queue_name IN ( + SELECT jq.queue_name + FROM app_jobs.job_queues jq + WHERE (jq.locked_at IS NULL + OR jq.locked_at < (v_now - job_expiry)) + FOR UPDATE SKIP LOCKED + )) + AND run_at <= v_now + AND attempts < max_attempts + AND (task_identifiers IS NULL + OR task_identifier = ANY (task_identifiers)) + ORDER BY priority ASC, run_at ASC, id ASC + LIMIT 1 + FOR UPDATE SKIP LOCKED; + + IF v_job_id IS NULL THEN + RETURN NULL; + END IF; + + IF v_queue_name IS NOT NULL THEN + UPDATE app_jobs.job_queues + SET locked_by = worker_id, locked_at = v_now + WHERE job_queues.queue_name = v_queue_name; + END IF; + + UPDATE app_jobs.jobs + SET + attempts = attempts + 1, + locked_by = worker_id, + locked_at = v_now + WHERE id = v_job_id + RETURNING * INTO v_row; + + RETURN v_row; +END; +$EOFCODE$; + +CREATE FUNCTION app_jobs.fail_job(worker_id text, job_id bigint, error_message text) RETURNS app_jobs.jobs LANGUAGE plpgsql STRICT AS $EOFCODE$ +DECLARE + v_row app_jobs.jobs; +BEGIN + UPDATE + app_jobs.jobs + SET + last_error = error_message, + run_at = greatest (now(), run_at) + (exp(least (attempts, 10))::text || ' seconds')::interval, + locked_by = NULL, + locked_at = NULL + WHERE + id = job_id + AND locked_by = worker_id + RETURNING + * INTO v_row; + IF v_row.queue_name IS NOT NULL THEN + UPDATE + app_jobs.job_queues + SET + locked_by = NULL, + locked_at = NULL + WHERE + queue_name = v_row.queue_name + AND locked_by = worker_id; + END IF; + RETURN v_row; +END; +$EOFCODE$; + +CREATE FUNCTION app_jobs.complete_jobs(job_ids bigint[]) RETURNS SETOF app_jobs.jobs LANGUAGE sql AS $EOFCODE$ + DELETE FROM app_jobs.jobs + WHERE id = ANY (job_ids) + AND (locked_by IS NULL + OR locked_at < NOW() - interval '4 hours') + RETURNING + *; +$EOFCODE$; + +CREATE FUNCTION app_jobs.complete_job(worker_id text, job_id bigint) RETURNS app_jobs.jobs LANGUAGE plpgsql AS $EOFCODE$ +DECLARE + v_row app_jobs.jobs; +BEGIN + DELETE FROM app_jobs.jobs + WHERE id = job_id + RETURNING + * INTO v_row; + IF v_row.queue_name IS NOT NULL THEN + UPDATE + app_jobs.job_queues + SET + locked_by = NULL, + locked_at = NULL + WHERE + queue_name = v_row.queue_name + AND locked_by = worker_id; + END IF; + RETURN v_row; +END; +$EOFCODE$; + +CREATE FUNCTION app_jobs.add_scheduled_job(identifier text, payload pg_catalog.json DEFAULT '{}'::json, schedule_info pg_catalog.json DEFAULT '{}'::json, job_key text DEFAULT NULL, queue_name text DEFAULT NULL, max_attempts int DEFAULT 25, priority int DEFAULT 0, entity_id uuid DEFAULT NULL) RETURNS app_jobs.scheduled_jobs AS $EOFCODE$ +DECLARE + v_job app_jobs.scheduled_jobs; + v_database_id uuid; + v_actor_id uuid; +BEGIN + v_database_id := jwt_private.current_database_id(); + v_actor_id := jwt_public.current_user_id(); + + IF job_key IS NOT NULL THEN + + -- Upsert job + INSERT INTO app_jobs.scheduled_jobs ( + database_id, + actor_id, + entity_id, + task_identifier, + payload, + queue_name, + schedule_info, + max_attempts, + key, + priority + ) VALUES ( + v_database_id, + v_actor_id, + add_scheduled_job.entity_id, + identifier, + coalesce(payload, '{}'::json), + queue_name, + schedule_info, + coalesce(max_attempts, 25), + job_key, + coalesce(priority, 0) + ) + ON CONFLICT (key) + DO UPDATE SET + task_identifier = EXCLUDED.task_identifier, + payload = EXCLUDED.payload, + queue_name = EXCLUDED.queue_name, + max_attempts = EXCLUDED.max_attempts, + schedule_info = EXCLUDED.schedule_info, + priority = EXCLUDED.priority + WHERE + scheduled_jobs.locked_at IS NULL + RETURNING + * INTO v_job; + + -- If upsert succeeded (insert or update), return early + + IF NOT (v_job IS NULL) THEN + RETURN v_job; + END IF; + + -- Upsert failed -> there must be an existing scheduled job that is locked. Remove + -- and allow a new one to be inserted + + DELETE FROM + app_jobs.scheduled_jobs + WHERE + KEY = job_key; + END IF; + + INSERT INTO app_jobs.scheduled_jobs ( + database_id, + actor_id, + entity_id, + task_identifier, + payload, + queue_name, + schedule_info, + max_attempts, + priority + ) VALUES ( + v_database_id, + v_actor_id, + add_scheduled_job.entity_id, + identifier, + payload, + queue_name, + schedule_info, + max_attempts, + priority + ) RETURNING * INTO v_job; + RETURN v_job; +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER; + +CREATE FUNCTION app_jobs.add_job(identifier text, payload pg_catalog.json DEFAULT '{}'::json, job_key text DEFAULT NULL, queue_name text DEFAULT NULL, run_at timestamptz DEFAULT now(), max_attempts int DEFAULT 25, priority int DEFAULT 0, entity_id uuid DEFAULT NULL, organization_id uuid DEFAULT NULL, entity_type text DEFAULT NULL) RETURNS app_jobs.jobs AS $EOFCODE$ +DECLARE + v_job app_jobs.jobs; + v_database_id uuid; + v_actor_id uuid; +BEGIN + -- Read context from JWT claims + v_database_id := jwt_private.current_database_id(); + v_actor_id := jwt_public.current_user_id(); + + IF job_key IS NOT NULL THEN + -- Upsert job + INSERT INTO app_jobs.jobs ( + database_id, + actor_id, + entity_id, + organization_id, + entity_type, + task_identifier, + payload, + queue_name, + run_at, + max_attempts, + key, + priority + ) VALUES ( + v_database_id, + v_actor_id, + add_job.entity_id, + add_job.organization_id, + add_job.entity_type, + identifier, + coalesce(payload, '{}'::json), + queue_name, + coalesce(run_at, now()), + coalesce(max_attempts, 25), + job_key, + coalesce(priority, 0) + ) + ON CONFLICT (key) + DO UPDATE SET + task_identifier = EXCLUDED.task_identifier, + payload = EXCLUDED.payload, + queue_name = EXCLUDED.queue_name, + max_attempts = EXCLUDED.max_attempts, + run_at = EXCLUDED.run_at, + priority = EXCLUDED.priority, + -- always reset error/retry state + attempts = 0, last_error = NULL + WHERE + jobs.locked_at IS NULL + RETURNING + * INTO v_job; + + -- If upsert succeeded (insert or update), return early + IF NOT (v_job IS NULL) THEN + RETURN v_job; + END IF; + + -- Upsert failed -> there must be an existing job that is locked. Remove + -- existing key to allow a new one to be inserted, and prevent any + -- subsequent retries by bumping attempts to the max allowed. + UPDATE + app_jobs.jobs + SET + key = NULL, + attempts = jobs.max_attempts + WHERE + key = job_key; + END IF; + + INSERT INTO app_jobs.jobs ( + database_id, + actor_id, + entity_id, + organization_id, + entity_type, + task_identifier, + payload, + queue_name, + run_at, + max_attempts, + priority + ) VALUES ( + v_database_id, + v_actor_id, + add_job.entity_id, + add_job.organization_id, + add_job.entity_type, + identifier, + payload, + queue_name, + run_at, + max_attempts, + priority + ) + RETURNING * INTO v_job; + + RETURN v_job; +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER; + +GRANT EXECUTE ON FUNCTION app_jobs.add_job(text, pg_catalog.json, text, text, timestamptz, int, int, uuid, uuid, text) TO authenticated; + +CREATE FUNCTION app_jobs.remove_job(job_key text) RETURNS app_jobs.jobs LANGUAGE plpgsql STRICT AS $EOFCODE$ +DECLARE + v_job app_jobs.jobs; +BEGIN + DELETE FROM app_jobs.jobs + WHERE key = job_key + AND (locked_at IS NULL + OR locked_at < NOW() - interval '4 hours') + RETURNING * INTO v_job; + + IF NOT (v_job IS NULL) THEN + RETURN v_job; + END IF; + + UPDATE app_jobs.jobs + SET + key = NULL, + attempts = jobs.max_attempts + WHERE key = job_key + RETURNING * INTO v_job; + + RETURN v_job; +END; +$EOFCODE$; + +CREATE FUNCTION app_jobs.force_unlock_workers(worker_ids text[]) RETURNS void LANGUAGE sql VOLATILE AS $EOFCODE$ + UPDATE app_jobs.jobs + SET locked_at = NULL, locked_by = NULL + WHERE locked_by = ANY (worker_ids); + + UPDATE app_jobs.job_queues + SET locked_at = NULL, locked_by = NULL + WHERE locked_by = ANY (worker_ids); +$EOFCODE$; \ No newline at end of file diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/helpers/json_build_object_apply.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/helpers/json_build_object_apply.sql new file mode 100644 index 00000000..e05072cf --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/helpers/json_build_object_apply.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/helpers/json_build_object_apply on pg + +BEGIN; + +SELECT verify_function ('app_jobs.json_build_object_apply'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/add_job.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/add_job.sql new file mode 100644 index 00000000..c841e7d0 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/add_job.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/add_job on pg + +BEGIN; + +SELECT verify_function ('app_jobs.add_job'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/add_scheduled_job.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/add_scheduled_job.sql new file mode 100644 index 00000000..a2f7d481 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/add_scheduled_job.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/add_scheduled_job on pg + +BEGIN; + +SELECT verify_function ('app_jobs.add_scheduled_job'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/complete_job.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/complete_job.sql new file mode 100644 index 00000000..4bd179ae --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/complete_job.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/complete_job on pg + +BEGIN; + +SELECT verify_function ('app_jobs.complete_job'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/complete_jobs.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/complete_jobs.sql new file mode 100644 index 00000000..aa9a5a45 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/complete_jobs.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/complete_jobs on pg + +BEGIN; + +SELECT verify_function ('app_jobs.complete_jobs'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/do_notify.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/do_notify.sql new file mode 100644 index 00000000..df64a9f4 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/do_notify.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/do_notify on pg + +BEGIN; + +SELECT verify_function ('app_jobs.do_notify'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/fail_job.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/fail_job.sql new file mode 100644 index 00000000..b9c65b48 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/fail_job.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/fail_job on pg + +BEGIN; + +SELECT verify_function ('app_jobs.fail_job'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/force_unlock_workers.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/force_unlock_workers.sql new file mode 100644 index 00000000..a71b0bbd --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/force_unlock_workers.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/force_unlock_workers on pg + +BEGIN; + +SELECT verify_function ('app_jobs.force_unlock_workers'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/get_job.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/get_job.sql new file mode 100644 index 00000000..86170be1 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/get_job.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/get_job on pg + +BEGIN; + +SELECT verify_function ('app_jobs.get_job'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/get_scheduled_job.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/get_scheduled_job.sql new file mode 100644 index 00000000..bb7e58d7 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/get_scheduled_job.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/get_scheduled_job on pg + +BEGIN; + +SELECT verify_function ('app_jobs.get_scheduled_job'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated.sql new file mode 100644 index 00000000..75a33c48 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated on pg + +BEGIN; + +SELECT has_function_privilege('authenticated', 'app_jobs.add_job(text, json, text, text, timestamptz, integer, integer, uuid, uuid, text)', 'EXECUTE'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/permanently_fail_jobs.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/permanently_fail_jobs.sql new file mode 100644 index 00000000..dfd8852f --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/permanently_fail_jobs.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/permanently_fail_jobs on pg + +BEGIN; + +SELECT verify_function ('app_jobs.permanently_fail_jobs'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/release_jobs.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/release_jobs.sql new file mode 100644 index 00000000..70004e7e --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/release_jobs.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/release_jobs on pg + +BEGIN; + +SELECT verify_function ('app_jobs.release_jobs'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/release_scheduled_jobs.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/release_scheduled_jobs.sql new file mode 100644 index 00000000..5b9b5929 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/release_scheduled_jobs.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/release_scheduled_jobs on pg + +BEGIN; + +SELECT verify_function ('app_jobs.release_scheduled_jobs'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/remove_job.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/remove_job.sql new file mode 100644 index 00000000..b855f409 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/remove_job.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/remove_job on pg + +BEGIN; + +SELECT verify_function ('app_jobs.remove_job'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/reschedule_jobs.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/reschedule_jobs.sql new file mode 100644 index 00000000..80ab587b --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/reschedule_jobs.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/reschedule_jobs on pg + +BEGIN; + +SELECT verify_function ('app_jobs.reschedule_jobs'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/run_scheduled_job.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/run_scheduled_job.sql new file mode 100644 index 00000000..02257023 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/procedures/run_scheduled_job.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/run_scheduled_job on pg + +BEGIN; + +SELECT verify_function ('app_jobs.run_scheduled_job'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/schema.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/schema.sql new file mode 100644 index 00000000..5e0b19d4 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/schema.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/schema on pg + +BEGIN; + +SELECT verify_schema ('app_jobs'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql new file mode 100644 index 00000000..d645d855 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql @@ -0,0 +1,10 @@ +-- Verify schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator on pg + +BEGIN; + + SELECT has_table_privilege('administrator', 'app_jobs.job_queues', 'SELECT'); + SELECT has_table_privilege('administrator', 'app_jobs.job_queues', 'INSERT'); + SELECT has_table_privilege('administrator', 'app_jobs.job_queues', 'UPDATE'); + SELECT has_table_privilege('administrator', 'app_jobs.job_queues', 'DELETE'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql new file mode 100644 index 00000000..bb378660 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx on pg + +BEGIN; + +SELECT verify_index ('app_jobs.job_queues', 'job_queues_locked_by_idx'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/job_queues/table.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/job_queues/table.sql new file mode 100644 index 00000000..3a5e4b1c --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/job_queues/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/tables/job_queues/table on pg + +BEGIN; + +SELECT verify_table ('app_jobs.job_queues'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql new file mode 100644 index 00000000..6255d716 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql @@ -0,0 +1,10 @@ +-- Verify schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator on pg + +BEGIN; + + SELECT has_table_privilege('administrator', 'app_jobs.jobs', 'SELECT'); + SELECT has_table_privilege('administrator', 'app_jobs.jobs', 'INSERT'); + SELECT has_table_privilege('administrator', 'app_jobs.jobs', 'UPDATE'); + SELECT has_table_privilege('administrator', 'app_jobs.jobs', 'DELETE'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql new file mode 100644 index 00000000..3635677a --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx on pg + +BEGIN; + +SELECT verify_index ('app_jobs.jobs', 'jobs_locked_by_idx'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql new file mode 100644 index 00000000..eeec4f53 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql @@ -0,0 +1,8 @@ +-- Verify schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx on pg + +BEGIN; + +SELECT verify_index ('app_jobs.jobs', 'jobs_main_index'); +SELECT verify_index ('app_jobs.jobs', 'jobs_no_queue_index'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/table.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/table.sql new file mode 100644 index 00000000..aaa0584d --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/tables/jobs/table on pg + +BEGIN; + +SELECT verify_table ('app_jobs.jobs'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql new file mode 100644 index 00000000..97b717d0 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql @@ -0,0 +1,10 @@ +-- Verify schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count on pg +BEGIN; +SELECT + verify_function ('app_jobs.tg_decrease_job_queue_count'); +SELECT + verify_trigger ('app_jobs.decrease_job_queue_count_on_delete'); +SELECT + verify_trigger ('app_jobs.decrease_job_queue_count_on_update'); +ROLLBACK; + diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql new file mode 100644 index 00000000..a6e89dd1 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql @@ -0,0 +1,10 @@ +-- Verify schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count on pg +BEGIN; +SELECT + verify_function ('app_jobs.tg_increase_job_queue_count'); +SELECT + verify_trigger ('app_jobs._500_increase_job_queue_count_on_insert'); +SELECT + verify_trigger ('app_jobs._500_increase_job_queue_count_on_update'); +ROLLBACK; + diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql new file mode 100644 index 00000000..96a054a0 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql @@ -0,0 +1,5 @@ +-- Verify schemas/app_jobs/tables/jobs/triggers/notify_worker on pg +BEGIN; +SELECT + verify_trigger ('app_jobs._900_after_insert'); +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/timestamps.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/timestamps.sql new file mode 100644 index 00000000..ed9466a3 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/timestamps.sql @@ -0,0 +1,16 @@ +-- Verify schemas/app_jobs/tables/jobs/triggers/timestamps on pg +BEGIN; +SELECT + created_at +FROM + app_jobs.jobs +LIMIT 1; +SELECT + updated_at +FROM + app_jobs.jobs +LIMIT 1; +SELECT + verify_trigger ('app_jobs._100_update_jobs_modtime_tg'); +ROLLBACK; + diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql new file mode 100644 index 00000000..c4aa4eb6 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql @@ -0,0 +1,10 @@ +-- Verify schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator on pg + +BEGIN; + + SELECT has_table_privilege('administrator', 'app_jobs.scheduled_jobs', 'SELECT'); + SELECT has_table_privilege('administrator', 'app_jobs.scheduled_jobs', 'INSERT'); + SELECT has_table_privilege('administrator', 'app_jobs.scheduled_jobs', 'UPDATE'); + SELECT has_table_privilege('administrator', 'app_jobs.scheduled_jobs', 'DELETE'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql new file mode 100644 index 00000000..34ee9f11 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx on pg + +BEGIN; + +SELECT verify_index ('app_jobs.scheduled_jobs', 'scheduled_jobs_locked_by_idx'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql new file mode 100644 index 00000000..d26a6822 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx on pg + +BEGIN; + +SELECT verify_index ('app_jobs.scheduled_jobs', 'scheduled_jobs_priority_id_idx'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/table.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/table.sql new file mode 100644 index 00000000..065f427b --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/tables/scheduled_jobs/table on pg + +BEGIN; + +SELECT verify_table ('app_jobs.scheduled_jobs'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql new file mode 100644 index 00000000..599c63a3 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql @@ -0,0 +1,8 @@ +-- Verify schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job on pg + +BEGIN; + + +SELECT verify_trigger ('app_jobs._900_notify_scheduled_job'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_fields.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_fields.sql new file mode 100644 index 00000000..9b36e4f2 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_fields.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/triggers/tg_add_job_with_fields on pg + +BEGIN; + +SELECT verify_function ('app_jobs.trigger_job_with_fields'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row.sql new file mode 100644 index 00000000..bdf8cc7e --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/triggers/tg_add_job_with_row on pg + +BEGIN; + +SELECT verify_function ('app_jobs.tg_add_job_with_row'); + +ROLLBACK; diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql new file mode 100644 index 00000000..72b5a7b9 --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql @@ -0,0 +1,6 @@ +-- Verify schemas/app_jobs/triggers/tg_add_job_with_row_id on pg +BEGIN; +SELECT + verify_function ('app_jobs.tg_add_job_with_row_id'); +ROLLBACK; + diff --git a/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/triggers/tg_update_timestamps.sql b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/triggers/tg_update_timestamps.sql new file mode 100644 index 00000000..fd53ed3e --- /dev/null +++ b/extensions/@pgpm/database-jobs/verify/schemas/app_jobs/triggers/tg_update_timestamps.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/triggers/tg_update_timestamps on pg + +BEGIN; + +SELECT verify_function ('app_jobs.tg_update_timestamps'); + +ROLLBACK; diff --git a/extensions/@pgpm/inflection/LICENSE b/extensions/@pgpm/inflection/LICENSE new file mode 100644 index 00000000..7b18c918 --- /dev/null +++ b/extensions/@pgpm/inflection/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2025 Dan Lynch +Copyright (c) 2025 Constructive + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/@pgpm/inflection/Makefile b/extensions/@pgpm/inflection/Makefile new file mode 100644 index 00000000..8acaf844 --- /dev/null +++ b/extensions/@pgpm/inflection/Makefile @@ -0,0 +1,6 @@ +EXTENSION = pgpm-inflection +DATA = sql/pgpm-inflection--0.15.5.sql + +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) diff --git a/extensions/@pgpm/inflection/README.md b/extensions/@pgpm/inflection/README.md new file mode 100644 index 00000000..f9dbfdf2 --- /dev/null +++ b/extensions/@pgpm/inflection/README.md @@ -0,0 +1,409 @@ +# @pgpm/inflection + +

+ +

+ +

+ + + + + +

+ +String inflection utilities for PostgreSQL naming conventions + +## Overview + +`@pgpm/inflection` provides comprehensive string transformation functions for PostgreSQL, enabling seamless conversion between different naming conventions. This package is essential for code generation, schema introspection, and maintaining consistent naming patterns across your database. It includes pluralization, singularization, case conversion, and slugification utilities. + +## Features + +- **Case Conversion**: Transform between camelCase, PascalCase, snake_case, and kebab-case +- **Pluralization**: Convert singular words to plural forms with English grammar rules +- **Singularization**: Convert plural words to singular forms +- **Slugification**: Create URL-friendly slugs from strings +- **Rule-Based System**: Extensible inflection rules stored in database table +- **Uncountable Words**: Handles special cases like "sheep", "fish", "data" +- **Pure plpgsql**: No external dependencies required + +## Installation + +If you have `pgpm` installed: + +```bash +pgpm install @pgpm/inflection +pgpm deploy +``` + +This is a quick way to get started. The sections below provide more detailed installation options. + +### Prerequisites + +```bash +# Install pgpm CLI +npm install -g pgpm + +# Start local Postgres (via Docker) and export env vars +pgpm docker start +eval "$(pgpm env)" +``` + +> **Tip:** Already running Postgres? Skip the Docker step and just export your `PG*` environment variables. + +### **Add to an Existing Package** + +```bash +# 1. Install the package +pgpm install @pgpm/inflection + +# 2. Deploy locally +pgpm deploy +``` + +### **Add to a New Project** + +```bash +# 1. Create a workspace +pgpm init workspace + +# 2. Create your first module +cd my-workspace +pgpm init + +# 3. Install a package +cd packages/my-module +pgpm install @pgpm/inflection + +# 4. Deploy everything +pgpm deploy --createdb --database mydb1 +``` + +## Core Functions + +### Case Conversion Functions + +#### inflection.camel(text) + +Convert string to camelCase. + +```sql +SELECT inflection.camel('user_profile_image'); +-- userProfileImage + +SELECT inflection.camel('UserProfileImage'); +-- userProfileImage + +SELECT inflection.camel('user-profile-image'); +-- userProfileImage +``` + +#### inflection.pascal(text) + +Convert string to PascalCase. + +```sql +SELECT inflection.pascal('user_profile_image'); +-- UserProfileImage + +SELECT inflection.pascal('user-profile-image'); +-- UserProfileImage +``` + +#### inflection.underscore(text) + +Convert string to snake_case. + +```sql +SELECT inflection.underscore('UserProfileImage'); +-- user_profile_image + +SELECT inflection.underscore('userProfileImage'); +-- user_profile_image + +SELECT inflection.underscore('user-profile-image'); +-- user_profile_image +``` + +#### inflection.dashed(text) + +Convert string to kebab-case. + +```sql +SELECT inflection.dashed('UserProfileImage'); +-- user-profile-image + +SELECT inflection.dashed('user_profile_image'); +-- user-profile-image +``` + +### Pluralization Functions + +#### inflection.plural(text) + +Convert singular word to plural form. + +```sql +SELECT inflection.plural('user'); +-- users + +SELECT inflection.plural('person'); +-- people + +SELECT inflection.plural('child'); +-- children + +SELECT inflection.plural('category'); +-- categories + +SELECT inflection.plural('status'); +-- statuses +``` + +#### inflection.singular(text) + +Convert plural word to singular form. + +```sql +SELECT inflection.singular('users'); +-- user + +SELECT inflection.singular('people'); +-- person + +SELECT inflection.singular('children'); +-- child + +SELECT inflection.singular('categories'); +-- category +``` + +### Slugification Functions + +#### inflection.slugify(text) + +Create URL-friendly slug from string. + +```sql +SELECT inflection.slugify('Hello World!'); +-- hello-world + +SELECT inflection.slugify('User Profile & Settings'); +-- user-profile-settings + +SELECT inflection.slugify(' Multiple Spaces '); +-- multiple-spaces +``` + +## Usage Examples + +### Database Schema Generation + +Generate table and column names following conventions: + +```sql +-- Convert API field names to database columns +CREATE TABLE users ( + id uuid PRIMARY KEY, + user_name text, -- from userName + email_address text, -- from emailAddress + created_at timestamptz DEFAULT now() +); + +-- Function to convert camelCase to snake_case +CREATE FUNCTION api_to_db_column(field_name text) +RETURNS text AS $$ +BEGIN + RETURN inflection.underscore(field_name); +END; +$$ LANGUAGE plpgsql; + +SELECT api_to_db_column('firstName'); -- first_name +SELECT api_to_db_column('emailAddress'); -- email_address +``` + +### GraphQL Schema Generation + +Generate GraphQL type names from database tables: + +```sql +-- Convert table names to GraphQL types +SELECT inflection.pascal(inflection.singular(table_name)) as graphql_type +FROM information_schema.tables +WHERE table_schema = 'public'; + +-- user_profiles → UserProfile +-- blog_posts → BlogPost +-- categories → Category +``` + +### REST API Endpoint Generation + +Create consistent API endpoints: + +```sql +-- Generate REST endpoints from table names +SELECT + '/' || inflection.dashed(inflection.plural(table_name)) as endpoint, + table_name +FROM information_schema.tables +WHERE table_schema = 'public'; + +-- users → /users +-- blog_posts → /blog-posts +-- user_profiles → /user-profiles +``` + +### Code Generation + +Generate TypeScript interfaces from database schema: + +```sql +-- Generate TypeScript interface names +CREATE FUNCTION generate_ts_interface(table_name text) +RETURNS text AS $$ +BEGIN + RETURN 'export interface ' || + inflection.pascal(inflection.singular(table_name)) || + ' {'; +END; +$$ LANGUAGE plpgsql; + +SELECT generate_ts_interface('user_profiles'); +-- export interface UserProfile { +``` + +### URL Slug Generation + +Create SEO-friendly URLs: + +```sql +-- Generate slugs for blog posts +CREATE TABLE blog_posts ( + id serial PRIMARY KEY, + title text NOT NULL, + slug text GENERATED ALWAYS AS (inflection.slugify(title)) STORED, + content text, + created_at timestamptz DEFAULT now() +); + +INSERT INTO blog_posts (title, content) +VALUES ('How to Use PostgreSQL', 'Content here...'); + +SELECT slug FROM blog_posts; +-- how-to-use-postgresql +``` + +## Integration Examples + +### With @pgpm/db-meta-schema + +Use inflection for schema introspection and code generation: + +```sql +-- Generate model names from tables +SELECT + table_name, + inflection.pascal(inflection.singular(table_name)) as model_name, + inflection.camel(inflection.plural(table_name)) as collection_name +FROM information_schema.tables +WHERE table_schema = 'public'; + +-- user_profiles → UserProfile (model), userProfiles (collection) +-- blog_posts → BlogPost (model), blogPosts (collection) +``` + +### With @pgpm/utils + +Combine with other utilities for advanced transformations: + +```sql +-- Generate API response field names +SELECT + column_name, + inflection.camel(column_name) as api_field_name +FROM information_schema.columns +WHERE table_name = 'users'; + +-- user_name → userName +-- email_address → emailAddress +-- created_at → createdAt +``` + +## Inflection Rules + +The package uses a rule-based system stored in the `inflection.inflection_rules` table: + +```sql +-- View pluralization rules +SELECT * FROM inflection.inflection_rules WHERE type = 'plural'; + +-- View singularization rules +SELECT * FROM inflection.inflection_rules WHERE type = 'singular'; +``` + +### Adding Custom Rules + +You can extend the inflection system with custom rules: + +```sql +-- Add custom pluralization rule +INSERT INTO inflection.inflection_rules (type, test, replacement) +VALUES ('plural', '(ox)$', '\1en'); + +-- Now "ox" → "oxen" +SELECT inflection.plural('ox'); +-- oxen +``` + +### Uncountable Words + +Some words don't change between singular and plural: + +```sql +SELECT inflection.plural('sheep'); +-- sheep + +SELECT inflection.plural('fish'); +-- fish + +SELECT inflection.plural('data'); +-- data +``` + +## Use Cases + +- **ORM Code Generation**: Generate model classes from database tables +- **API Development**: Convert between database and API naming conventions +- **GraphQL Schema**: Generate GraphQL types from database schema +- **Documentation**: Create consistent naming in generated documentation +- **Migration Scripts**: Transform legacy naming to modern conventions +- **URL Generation**: Create SEO-friendly slugs for content +- **Multi-Language Support**: Handle naming conventions across different programming languages + +## Testing + +```bash +pnpm test +``` + +## Dependencies + +- `@pgpm/verify`: Verification utilities + +## Related Tooling + +* [pgpm](https://github.com/constructive-io/constructive/tree/main/packages/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages. +* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/packages/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation. +* [supabase-test](https://github.com/constructive-io/constructive/tree/main/packages/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready. +* [graphile-test](https://github.com/constructive-io/constructive/tree/main/packages/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts. +* [pgsql-parser](https://github.com/constructive-io/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax. +* [libpg-query-node](https://github.com/constructive-io/libpg-query-node): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees. +* [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. + +## Disclaimer + +AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND. + +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. diff --git a/extensions/@pgpm/inflection/deploy/extension/defaults.sql b/extensions/@pgpm/inflection/deploy/extension/defaults.sql new file mode 100644 index 00000000..1d50acc9 --- /dev/null +++ b/extensions/@pgpm/inflection/deploy/extension/defaults.sql @@ -0,0 +1,8 @@ +-- Deploy extension/defaults to pg + +BEGIN; + +-- hstore, unaccent +GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public to public; + +COMMIT; diff --git a/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/camel.sql b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/camel.sql new file mode 100644 index 00000000..382fc9f9 --- /dev/null +++ b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/camel.sql @@ -0,0 +1,25 @@ +-- Deploy schemas/inflection/procedures/camel to pg + +-- requires: schemas/inflection/schema +-- requires: schemas/inflection/procedures/underscore + +BEGIN; + +CREATE FUNCTION inflection.camel (str text) + RETURNS text + AS $$ +DECLARE + result text[]; +BEGIN + str = inflection.underscore(str); + FOR result IN + SELECT regexp_matches(str, E'(_[a-zA-Z0-9])', 'g') + LOOP + str = replace(str, result[1], upper(result[1])); + END LOOP; + return regexp_replace(substring(str FROM 1 FOR 1) || substring(str FROM 2 FOR length(str)), E'[_]+', '', 'gi'); +END; +$$ +LANGUAGE 'plpgsql' STABLE; + +COMMIT; diff --git a/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/dashed.sql b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/dashed.sql new file mode 100644 index 00000000..7fc208cc --- /dev/null +++ b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/dashed.sql @@ -0,0 +1,29 @@ +-- Deploy schemas/inflection/procedures/dashed to pg + +-- requires: schemas/inflection/schema +-- requires: schemas/inflection/procedures/underscore + +BEGIN; + +CREATE FUNCTION inflection.dashed(str text) + RETURNS text + AS $$ + WITH underscored AS ( + SELECT + inflection.underscore(str) AS value +), +dashes AS ( + SELECT + regexp_replace(value, '_', '-', 'gi') AS value + FROM + underscored +) +SELECT + value +FROM + dashes; +$$ +LANGUAGE 'sql' +IMMUTABLE; +COMMIT; + diff --git a/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/lower.sql b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/lower.sql new file mode 100644 index 00000000..31cc8c4d --- /dev/null +++ b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/lower.sql @@ -0,0 +1,13 @@ +-- Deploy schemas/inflection/procedures/lower to pg + +-- requires: schemas/inflection/schema + +BEGIN; + +CREATE FUNCTION inflection.lower ( str text ) RETURNS text AS $EOFCODE$ +BEGIN + return lower(str); +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +COMMIT; diff --git a/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/no_consecutive_caps.sql b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/no_consecutive_caps.sql new file mode 100644 index 00000000..736a4c6e --- /dev/null +++ b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/no_consecutive_caps.sql @@ -0,0 +1,53 @@ +-- Deploy schemas/inflection/procedures/no_consecutive_caps to pg + +-- requires: schemas/inflection/schema + +BEGIN; + +CREATE FUNCTION inflection.no_consecutive_caps_till_end( + str text +) returns text as $$ +DECLARE + result text[]; + temp text; +BEGIN + FOR result IN + SELECT regexp_matches(str, E'([A-Z])([A-Z]+$)', 'g') + LOOP + temp = result[1] || lower(result[2]); + str = replace(str, result[1] || result[2], temp); + END LOOP; + return str; +END; +$$ +LANGUAGE 'plpgsql' STABLE; + + +CREATE FUNCTION inflection.no_consecutive_caps_till_lower( + str text +) returns text as $$ +DECLARE + result text[]; + temp text; +BEGIN + FOR result IN + SELECT regexp_matches(str, E'([A-Z])([A-Z]+)[A-Z][a-z]', 'g') + LOOP + temp = result[1] || lower(result[2]); + str = replace(str, result[1] || result[2], temp); + END LOOP; + + return str; +END; +$$ +LANGUAGE 'plpgsql' STABLE; + + +CREATE FUNCTION inflection.no_consecutive_caps( + str text +) returns text as $$ + select inflection.no_consecutive_caps_till_lower(inflection.no_consecutive_caps_till_end(str)); +$$ +LANGUAGE 'sql' STABLE; + +COMMIT; diff --git a/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/no_single_underscores.sql b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/no_single_underscores.sql new file mode 100644 index 00000000..b716392e --- /dev/null +++ b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/no_single_underscores.sql @@ -0,0 +1,70 @@ +-- Deploy schemas/inflection/procedures/no_single_underscores to pg + +-- requires: schemas/inflection/schema + +BEGIN; + +CREATE FUNCTION inflection.no_single_underscores_in_beginning( + str text +) returns text as $$ +DECLARE + result text[]; + temp text; +BEGIN + FOR result IN + SELECT regexp_matches(str, E'(^[a-z])(_)', 'g') + LOOP + str = replace(str, result[1] || result[2], result[1]); + END LOOP; + return str; +END; +$$ +LANGUAGE 'plpgsql' STABLE; + + +CREATE FUNCTION inflection.no_single_underscores_at_end( + str text +) returns text as $$ +DECLARE + result text[]; + temp text; +BEGIN + FOR result IN + SELECT regexp_matches(str, E'(_)([a-z]$)', 'g') + LOOP + str = replace(str, result[1] || result[2], result[2]); + END LOOP; + + return str; +END; +$$ +LANGUAGE 'plpgsql' STABLE; + +CREATE FUNCTION inflection.no_single_underscores_in_middle( + str text +) returns text as $$ +DECLARE + result text[]; + temp text; +BEGIN + FOR result IN + SELECT regexp_matches(str, E'(_)([a-z]_)', 'g') + LOOP + str = replace(str, result[1] || result[2], result[2]); + END LOOP; + + return str; +END; +$$ +LANGUAGE 'plpgsql' STABLE; + + +CREATE FUNCTION inflection.no_single_underscores( + str text +) returns text as $$ + select + inflection.no_single_underscores_in_middle(inflection.no_single_underscores_at_end(inflection.no_single_underscores_in_beginning(str))); +$$ +LANGUAGE 'sql' STABLE; + +COMMIT; diff --git a/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/pascal.sql b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/pascal.sql new file mode 100644 index 00000000..7a50cf1b --- /dev/null +++ b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/pascal.sql @@ -0,0 +1,20 @@ +-- Deploy schemas/inflection/procedures/pascal to pg +-- requires: schemas/inflection/schema +-- requires: schemas/inflection/procedures/camel + +BEGIN; + +CREATE FUNCTION inflection.pascal (str text) + RETURNS text + AS $$ +DECLARE + result text[]; +BEGIN + str = inflection.camel(str); + return upper(substring(str FROM 1 FOR 1)) || substring(str FROM 2 FOR length(str)); +END; +$$ +LANGUAGE 'plpgsql' STABLE; + +COMMIT; + diff --git a/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/pg_slugify.sql b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/pg_slugify.sql new file mode 100644 index 00000000..a230c734 --- /dev/null +++ b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/pg_slugify.sql @@ -0,0 +1,68 @@ +-- Deploy schemas/inflection/procedures/pg_slugify to pg +-- requires: schemas/inflection/schema +-- requires: schemas/inflection/procedures/no_consecutive_caps +-- NOTE: this does NOT lowercase, and uses underscores instead of dashes + +BEGIN; +CREATE FUNCTION inflection.pg_slugify (value text, allow_unicode boolean) + RETURNS text + AS $$ + WITH normalized AS ( + SELECT + CASE WHEN allow_unicode THEN + value + ELSE + unaccent (value) + END AS value +), +no_consecutive_caps AS ( + SELECT + inflection.no_consecutive_caps (value) AS value +FROM + normalized +), +remove_chars AS ( + SELECT + regexp_replace(value, E'[^\\w\\s-]', '', 'gi') AS value +FROM + no_consecutive_caps +), +trimmed AS ( + SELECT + trim(value) AS value +FROM + remove_chars +), +hyphenated AS ( + SELECT + regexp_replace(value, E'[-\\s]+', '-', 'gi') AS value +FROM + trimmed +), +underscored AS ( + SELECT + regexp_replace(value, E'[-]+', '_', 'gi') AS value +FROM + hyphenated +), +removedups AS ( + SELECT + regexp_replace(value, E'[_]+', '_', 'gi') AS value +FROM + underscored +) +SELECT + value +FROM + removedups; +$$ +LANGUAGE SQL +STRICT IMMUTABLE; +-- default false overload +CREATE FUNCTION inflection.pg_slugify (text) + RETURNS text + AS 'SELECT inflection.pg_slugify($1, false)' + LANGUAGE SQL + IMMUTABLE; +COMMIT; + diff --git a/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/plural.sql b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/plural.sql new file mode 100644 index 00000000..83f812eb --- /dev/null +++ b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/plural.sql @@ -0,0 +1,36 @@ +-- Deploy schemas/inflection/procedures/plural to pg + +-- requires: schemas/inflection/schema +-- requires: schemas/inflection/tables/inflection_rules/table +-- requires: schemas/inflection/procedures/should_skip_uncountable + +BEGIN; + +CREATE FUNCTION inflection.plural (str text) + RETURNS text + AS $$ +DECLARE + result record; + matches text[]; +BEGIN + IF inflection.should_skip_uncountable(lower(str)) THEN + return str; + END IF; + + FOR result IN + SELECT * FROM inflection.inflection_rules where type='plural' + LOOP + matches = regexp_matches(str, result.test, 'gi'); + IF (array_length(matches, 1) > 0) THEN + IF (result.replacement IS NULL) THEN + return str; + END IF; + str = regexp_replace(str, result.test, result.replacement, 'gi'); + return str; + END IF; + END LOOP; + return str; +END; +$$ +LANGUAGE 'plpgsql' IMMUTABLE; +COMMIT; diff --git a/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/should_skip_uncountable.sql b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/should_skip_uncountable.sql new file mode 100644 index 00000000..394c948c --- /dev/null +++ b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/should_skip_uncountable.sql @@ -0,0 +1,17 @@ +-- Deploy schemas/inflection/procedures/should_skip_uncountable to pg +-- requires: schemas/inflection/schema +-- requires: schemas/inflection/procedures/uncountable_words + +BEGIN; + +CREATE FUNCTION inflection.should_skip_uncountable (str text) + RETURNS boolean + AS $$ + SELECT + str = ANY (inflection.uncountable_words ()); +$$ +LANGUAGE 'sql' +IMMUTABLE; + +COMMIT; + diff --git a/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/singular.sql b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/singular.sql new file mode 100644 index 00000000..e9ee3a0f --- /dev/null +++ b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/singular.sql @@ -0,0 +1,36 @@ +-- Deploy schemas/inflection/procedures/singular to pg + +-- requires: schemas/inflection/schema +-- requires: schemas/inflection/tables/inflection_rules/table +-- requires: schemas/inflection/procedures/should_skip_uncountable + +BEGIN; + +CREATE FUNCTION inflection.singular (str text) + RETURNS text + AS $$ +DECLARE + result record; + matches text[]; +BEGIN + IF inflection.should_skip_uncountable(lower(str)) THEN + return str; + END IF; + + FOR result IN + SELECT * FROM inflection.inflection_rules where type='singular' + LOOP + matches = regexp_matches(str, result.test, 'gi'); + IF (array_length(matches, 1) > 0) THEN + IF (result.replacement IS NULL) THEN + return str; + END IF; + str = regexp_replace(str, result.test, result.replacement, 'gi'); + return str; + END IF; + END LOOP; + return str; +END; +$$ +LANGUAGE 'plpgsql' IMMUTABLE; +COMMIT; diff --git a/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/slugify.sql b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/slugify.sql new file mode 100644 index 00000000..e1f9e943 --- /dev/null +++ b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/slugify.sql @@ -0,0 +1,59 @@ +-- Deploy schemas/inflection/procedures/slugify to pg + +-- requires: schemas/inflection/schema + +-- https://schinckel.net/2015/12/16/slugify()-for-postgres-(almost)/ + +BEGIN; +CREATE FUNCTION inflection.slugify (value text, allow_unicode boolean) + RETURNS text + AS $$ + WITH normalized AS ( + SELECT + CASE WHEN allow_unicode THEN + value + ELSE + unaccent (value) + END AS value +), +remove_chars AS ( + SELECT + regexp_replace(value, E'[^\\w\\s-]', '', 'gi') AS value +FROM + normalized +), +lowercase AS ( + SELECT + lower(value) AS value +FROM + remove_chars +), +trimmed AS ( + SELECT + trim(value) AS value +FROM + lowercase +), +hyphenated AS ( + SELECT + regexp_replace(value, E'[-\\s]+', '-', 'gi') AS value +FROM + trimmed +) +SELECT + value +FROM + hyphenated; +$$ +LANGUAGE SQL +STRICT IMMUTABLE; + +-- default false overload +CREATE FUNCTION inflection.slugify (text) + RETURNS text + AS 'SELECT inflection.slugify($1, false)' + LANGUAGE SQL + IMMUTABLE; + + +COMMIT; diff --git a/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/slugify_trigger.sql b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/slugify_trigger.sql new file mode 100644 index 00000000..a56241f7 --- /dev/null +++ b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/slugify_trigger.sql @@ -0,0 +1,21 @@ +-- Deploy schemas/inflection/procedures/slugify_trigger to pg + +-- requires: schemas/inflection/schema +-- requires: schemas/inflection/procedures/slugify + +BEGIN; + +-- USAGE inflection.slugify_trigger ('field_name') + +CREATE FUNCTION inflection.slugify_trigger() +RETURNS TRIGGER AS $$ +DECLARE + value text := to_json(NEW) ->> TG_ARGV[0]; +BEGIN + NEW := NEW #= (TG_ARGV[0] || '=>' || inflection.slugify(value))::hstore; + RETURN NEW; +END; +$$ +LANGUAGE 'plpgsql' VOLATILE; + +COMMIT; \ No newline at end of file diff --git a/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/uncountable_words.sql b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/uncountable_words.sql new file mode 100644 index 00000000..5ef70dca --- /dev/null +++ b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/uncountable_words.sql @@ -0,0 +1,14 @@ +-- Deploy schemas/inflection/procedures/uncountable_words to pg + +-- requires: schemas/inflection/schema + +BEGIN; + +CREATE FUNCTION inflection.uncountable_words () + RETURNS text[] + AS $$ +select ARRAY[ 'accommodation', 'adulthood', 'advertising', 'advice', 'aggression', 'aid', 'air', 'aircraft', 'alcohol', 'anger', 'applause', 'arithmetic', 'assistance', 'athletics', 'bacon', 'baggage', 'beef', 'biology', 'blood', 'botany', 'bread', 'butter', 'carbon', 'cardboard', 'cash', 'chalk', 'chaos', 'chess', 'crossroads', 'countryside', 'dancing', 'deer', 'dignity', 'dirt', 'dust', 'economics', 'education', 'electricity', 'engineering', 'enjoyment', 'envy', 'equipment', 'ethics', 'evidence', 'evolution', 'fame', 'fiction', 'flour', 'flu', 'food', 'fuel', 'fun', 'furniture', 'gallows', 'garbage', 'garlic', 'genetics', 'gold', 'golf', 'gossip', 'grammar', 'gratitude', 'grief', 'guilt', 'gymnastics', 'happiness', 'hardware', 'harm', 'hate', 'hatred', 'health', 'heat', 'help', 'homework', 'honesty', 'honey', 'hospitality', 'housework', 'humour', 'hunger', 'hydrogen', 'ice', 'importance', 'inflation', 'information', 'innocence', 'iron', 'irony', 'jam', 'jewelry', 'judo', 'karate', 'knowledge', 'lack', 'laughter', 'lava', 'leather', 'leisure', 'lightning', 'linguine', 'linguini', 'linguistics', 'literature', 'litter', 'livestock', 'logic', 'loneliness', 'luck', 'luggage', 'macaroni', 'machinery', 'magic', 'management', 'mankind', 'marble', 'mathematics', 'mayonnaise', 'measles', 'methane', 'milk', 'minus', 'money', 'mud', 'music', 'mumps', 'nature', 'news', 'nitrogen', 'nonsense', 'nurture', 'nutrition', 'obedience', 'obesity', 'oxygen', 'pasta', 'patience', 'physics', 'poetry', 'pollution', 'poverty', 'pride', 'psychology', 'publicity', 'punctuation', 'quartz', 'racism', 'relaxation', 'reliability', 'research', 'respect', 'revenge', 'rice', 'rubbish', 'rum', 'safety', 'scenery', 'seafood', 'seaside', 'series', 'shame', 'sheep', 'shopping', 'sleep', 'smoke', 'smoking', 'snow', 'soap', 'software', 'soil', 'spaghetti', 'species', 'steam', 'stuff', 'stupidity', 'sunshine', 'symmetry', 'tennis', 'thirst', 'thunder', 'timber', 'traffic', 'transportation', 'trust', 'underwear', 'unemployment', 'unity', 'validity', 'veal', 'vegetation', 'vegetarianism', 'vengeance', 'violence', 'vitality', 'warmth', 'wealth', 'weather', 'welfare', 'wheat', 'wildlife', 'wisdom', 'yoga', 'zinc', 'zoology' ]; +$$ +LANGUAGE 'sql' IMMUTABLE; + +COMMIT; diff --git a/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/underscore.sql b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/underscore.sql new file mode 100644 index 00000000..e1a79e5e --- /dev/null +++ b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/underscore.sql @@ -0,0 +1,59 @@ +-- Deploy schemas/inflection/procedures/underscore to pg +-- requires: schemas/inflection/schema +-- requires: schemas/inflection/procedures/pg_slugify +-- requires: schemas/inflection/procedures/no_single_underscores + +BEGIN; +CREATE FUNCTION inflection.underscore (str text) + RETURNS text + AS $$ + WITH slugged AS ( + SELECT + inflection.pg_slugify(str) AS value +), +convertedupper AS ( + SELECT + lower(regexp_replace(value, E'([A-Z])', E'\_\\1', 'g')) AS value + FROM + slugged +), +noprefix AS ( + SELECT + regexp_replace(value, E'^_', '', 'g') AS value + FROM + convertedupper +), +removedups AS ( + SELECT + regexp_replace(value, E'[_]+', '_', 'gi') AS value +FROM + noprefix +), +stripedges AS ( + SELECT + regexp_replace(regexp_replace(value, E'([A-Z])_$', E'\\1', 'gi'), E'^_([A-Z])', E'\\1', 'gi') AS value +FROM + removedups +), +nosingles AS ( + SELECT + inflection.no_single_underscores(value) AS value +FROM + stripedges +) +SELECT + value +FROM + nosingles; +$$ +LANGUAGE 'sql' +IMMUTABLE; + +CREATE FUNCTION inflection.underscore (parts text[]) + RETURNS text + AS $$ + SELECT inflection.underscore(array_to_string(parts, '_')); +$$ +LANGUAGE 'sql' +IMMUTABLE; +COMMIT; diff --git a/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/upper.sql b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/upper.sql new file mode 100644 index 00000000..9d1092cf --- /dev/null +++ b/extensions/@pgpm/inflection/deploy/schemas/inflection/procedures/upper.sql @@ -0,0 +1,13 @@ +-- Deploy schemas/inflection/procedures/upper to pg + +-- requires: schemas/inflection/schema + +BEGIN; + +CREATE FUNCTION inflection.upper ( str text ) RETURNS text AS $EOFCODE$ +BEGIN + return upper(str); +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +COMMIT; diff --git a/extensions/@pgpm/inflection/deploy/schemas/inflection/schema.sql b/extensions/@pgpm/inflection/deploy/schemas/inflection/schema.sql new file mode 100644 index 00000000..74a4ff3e --- /dev/null +++ b/extensions/@pgpm/inflection/deploy/schemas/inflection/schema.sql @@ -0,0 +1,15 @@ +-- Deploy schemas/inflection/schema to pg + + +BEGIN; + +CREATE SCHEMA inflection; + +GRANT USAGE ON SCHEMA inflection +TO public; + +ALTER DEFAULT PRIVILEGES IN SCHEMA inflection +GRANT EXECUTE ON FUNCTIONS +TO public; + +COMMIT; diff --git a/extensions/@pgpm/inflection/deploy/schemas/inflection/tables/inflection_rules/fixtures/1589249334312_fixture.sql b/extensions/@pgpm/inflection/deploy/schemas/inflection/tables/inflection_rules/fixtures/1589249334312_fixture.sql new file mode 100644 index 00000000..93072f12 --- /dev/null +++ b/extensions/@pgpm/inflection/deploy/schemas/inflection/tables/inflection_rules/fixtures/1589249334312_fixture.sql @@ -0,0 +1,150 @@ +-- Deploy schemas/inflection/tables/inflection_rules/fixtures/1589249334312_fixture to pg + +-- requires: schemas/inflection/schema +-- requires: schemas/inflection/tables/inflection_rules/table + +BEGIN; + +INSERT INTO inflection.inflection_rules + (type, test, replacement) VALUES + -- plural guards: already-plural words return as-is (NULL replacement) + ('plural', '^(m|wom)en$', NULL), + ('plural', '(pe)ople$', NULL), + ('plural', '(child)ren$', NULL), + ('plural', '([ti])a$', NULL), + ('plural', '((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$', NULL), + ('plural', '(.+base)s$', NULL), + ('plural', '(database)s$', NULL), + ('plural', '(drive)s$', NULL), + ('plural', '(hi|ti)ves$', NULL), + ('plural', '(curve)s$', NULL), + ('plural', '([lr])ves$', NULL), + ('plural', '([^fo])ves$', NULL), + ('plural', '([^aeiouy]|qu)ies$', NULL), + ('plural', '(s)eries$', NULL), + ('plural', '(m)ovies$', NULL), + ('plural', '(x|ch|ss|sh)es$', NULL), + ('plural', '([m|l])ice$', NULL), + ('plural', '(bus)es$', NULL), + ('plural', '(o)es$', NULL), + ('plural', '(shoe)s$', NULL), + ('plural', '(cris|ax|test)es$', NULL), + ('plural', '(octop|vir)uses$', NULL), + ('plural', '(alias|canvas|status|campus)es$', NULL), + ('plural', '^(summons|bonus)es$', NULL), + ('plural', '^(ox)en', NULL), + ('plural', '(matr)ices$', NULL), + ('plural', '(vert|ind)ices$', NULL), + ('plural', '^feet$', NULL), + ('plural', '^teeth$', NULL), + ('plural', '^geese$', NULL), + ('plural', '(quiz)zes$', NULL), + ('plural', '^(whereas)es$', NULL), + ('plural', '^(criteri)a$', NULL), + ('plural', '^genera$', NULL), + -- plural replacement rules + ('plural', '^(m|wom)an$', E'\\1en'), + ('plural', '(pe)rson$', E'\\1ople'), + ('plural', '(child)$', E'\\1ren'), + ('plural', '(drive)$', E'\\1s'), + ('plural', '^(ox)$', E'\\1en'), + ('plural', '(ax|test)is$', E'\\1es'), + ('plural', '(octop|vir)us$', E'\\1uses'), + ('plural', '(alias|status|canvas|campus)$', E'\\1es'), + ('plural', '^(summons|bonus)$', E'\\1es'), + ('plural', '(bu)s$', E'\\1ses'), + ('plural', '(buffal|tomat|potat)o$', E'\\1oes'), + ('plural', '([ti])um$', E'\\1a'), + ('plural', 'sis$', E'ses'), + ('plural', '(?:([^f])fe|([lr])f)$', E'\\1\\2ves'), + ('plural', '^(focus)$', E'\\1es'), + ('plural', '(hi|ti)ve$', E'\\1ves'), + ('plural', '([^aeiouy]|qu)y$', E'\\1ies'), + ('plural', '(matr)ix$', E'\\1ices'), + ('plural', '(vert|ind)ex$', E'\\1ices'), + ('plural', '(x|ch|ss|sh)$', E'\\1es'), + ('plural', '([m|l])ouse$', E'\\1ice'), + ('plural', '^foot$', E'feet'), + ('plural', '^tooth$', E'teeth'), + ('plural', '^goose$', E'geese'), + ('plural', '(quiz)$', E'\\1zes'), + ('plural', '^(whereas)$', E'\\1es'), + ('plural', '^(criteri)on$', E'\\1a'), + ('plural', '^genus$', E'genera'), + ('plural', 's$', E's'), + ('plural', '$', E's'), + -- singular guards: already-singular words return as-is (NULL replacement) + ('singular', '^(m|wom)an$', NULL), + ('singular', '(pe)rson$', NULL), + ('singular', '(child)$', NULL), + ('singular', '(drive)$', NULL), + ('singular', '^(ox)$', NULL), + ('singular', '(ax|test)is$', NULL), + ('singular', '(octop|vir)us$', NULL), + ('singular', '(alias|status|canvas|campus)$', NULL), + ('singular', '^(summons|bonus)$', NULL), + ('singular', '(bu)s$', NULL), + ('singular', '(buffal|tomat|potat)o$', NULL), + ('singular', '([ti])um$', NULL), + ('singular', 'sis$', NULL), + ('singular', '(?:([^f])fe|([lr])f)$', NULL), + ('singular', '^(focus)$', NULL), + ('singular', '(hi|ti)ve$', NULL), + ('singular', '([^aeiouy]|qu)y$', NULL), + ('singular', '(x|ch|ss|sh)$', NULL), + ('singular', '(matr)ix$', NULL), + ('singular', '(vert|ind)ex$', NULL), + ('singular', '([m|l])ouse$', NULL), + ('singular', '^foot$', NULL), + ('singular', '^tooth$', NULL), + ('singular', '^goose$', NULL), + ('singular', '(quiz)$', NULL), + ('singular', '^(whereas)$', NULL), + ('singular', '^(criteri)on$', NULL), + ('singular', '^genus$', NULL), + -- singular replacement rules + ('singular', '^(m|wom)en$', E'\\1an'), + ('singular', '(pe)ople$', E'\\1rson'), + ('singular', '(child)ren$', E'\\1'), + ('singular', '(database)s$', E'\\1'), + ('singular', '(drive)s$', E'\\1'), + ('singular', '^genera$', E'genus'), + ('singular', '^(criteri)a$', E'\\1on'), + -- Latin suffix overrides (PostGraphile-compatible) + ('singular', '(schema)ta$', E'\\1'), + ('singular', '(phenomen)a$', E'\\1on'), + ('singular', '(memorand)a$', E'\\1um'), + ('singular', '(curricul)a$', E'\\1um'), + ('singular', '([ti])a$', E'\\1um'), + ('singular', '(.+base)s$', E'\\1'), + ('singular', '((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$', E'\\1\\2sis'), + ('singular', '(hi|ti)ves$', E'\\1ve'), + ('singular', '(curve)s$', E'\\1'), + ('singular', '([lr])ves$', E'\\1f'), + ('singular', '([a])ves$', E'\\1ve'), + ('singular', '([^fo])ves$', E'\\1fe'), + ('singular', '(m)ovies$', E'\\1ovie'), + ('singular', '([^aeiouy]|qu)ies$', E'\\1y'), + ('singular', '(s)eries$', E'\\1eries'), + ('singular', '(x|ch|ss|sh)es$', E'\\1'), + ('singular', '([m|l])ice$', E'\\1ouse'), + ('singular', '(bus)es$', E'\\1'), + ('singular', '(o)es$', E'\\1'), + ('singular', '(shoe)s$', E'\\1'), + ('singular', '(cris|ax|test)es$', E'\\1is'), + ('singular', '(octop|vir)uses$', E'\\1us'), + ('singular', '(alias|canvas|status|campus)es$', E'\\1'), + ('singular', '^(summons|bonus)es$', E'\\1'), + ('singular', '^(ox)en', E'\\1'), + ('singular', '(matr)ices$', E'\\1ix'), + ('singular', '(vert|ind)ices$', E'\\1ex'), + ('singular', '^feet$', E'foot'), + ('singular', '^teeth$', E'tooth'), + ('singular', '^geese$', E'goose'), + ('singular', '(quiz)zes$', E'\\1'), + ('singular', '^(whereas)es$', E'\\1'), + ('singular', 'ss$', E'ss'), + ('singular', 's$', E'') +; + +COMMIT; diff --git a/extensions/@pgpm/inflection/deploy/schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx.sql b/extensions/@pgpm/inflection/deploy/schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx.sql new file mode 100644 index 00000000..4a43b40e --- /dev/null +++ b/extensions/@pgpm/inflection/deploy/schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx.sql @@ -0,0 +1,12 @@ +-- Deploy schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx to pg + +-- requires: schemas/inflection/schema +-- requires: schemas/inflection/tables/inflection_rules/table + +BEGIN; + +CREATE INDEX inflection_rules_type_idx ON inflection.inflection_rules ( + type +); + +COMMIT; diff --git a/extensions/@pgpm/inflection/deploy/schemas/inflection/tables/inflection_rules/table.sql b/extensions/@pgpm/inflection/deploy/schemas/inflection/tables/inflection_rules/table.sql new file mode 100644 index 00000000..89f1e6b1 --- /dev/null +++ b/extensions/@pgpm/inflection/deploy/schemas/inflection/tables/inflection_rules/table.sql @@ -0,0 +1,16 @@ +-- Deploy schemas/inflection/tables/inflection_rules/table to pg + +-- requires: schemas/inflection/schema + +BEGIN; + +CREATE TABLE inflection.inflection_rules ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + type text, -- singular, plural + test text, + replacement text +); + +GRANT select on inflection.inflection_rules to PUBLIC; + +COMMIT; diff --git a/extensions/@pgpm/inflection/package.json b/extensions/@pgpm/inflection/package.json new file mode 100644 index 00000000..22d4c2ae --- /dev/null +++ b/extensions/@pgpm/inflection/package.json @@ -0,0 +1,38 @@ +{ + "name": "@pgpm/inflection", + "version": "0.26.0", + "description": "String inflection utilities for PostgreSQL naming conventions", + "author": "Dan Lynch ", + "contributors": [ + "Constructive " + ], + "keywords": [ + "postgresql", + "pgpm", + "inflection", + "strings" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "bundle": "pgpm package", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@pgpm/verify": "0.26.0" + }, + "devDependencies": { + "pgpm": "^4.23.2" + }, + "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" + }, + "gitHead": "3badf1e5e2fc71deae9e194b779599d17ab28a0d" +} diff --git a/extensions/@pgpm/inflection/pgpm-inflection.control b/extensions/@pgpm/inflection/pgpm-inflection.control new file mode 100644 index 00000000..1049a69b --- /dev/null +++ b/extensions/@pgpm/inflection/pgpm-inflection.control @@ -0,0 +1,8 @@ +# pgpm-inflection extension +comment = 'pgpm-inflection extension' +default_version = '0.15.5' +module_pathname = '$libdir/pgpm-inflection' +requires = 'plpgsql,unaccent,pgpm-verify' +relocatable = false +superuser = false + \ No newline at end of file diff --git a/extensions/@pgpm/inflection/pgpm.plan b/extensions/@pgpm/inflection/pgpm.plan new file mode 100644 index 00000000..d289d21f --- /dev/null +++ b/extensions/@pgpm/inflection/pgpm.plan @@ -0,0 +1,20 @@ +%syntax-version=1.0.0 +%project=pgpm-inflection +%uri=pgpm-inflection + +schemas/inflection/schema 2017-08-11T08:11:51Z skitch # add schemas/inflection/schema +schemas/inflection/procedures/no_consecutive_caps [schemas/inflection/schema] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/no_consecutive_caps +schemas/inflection/procedures/pg_slugify [schemas/inflection/schema schemas/inflection/procedures/no_consecutive_caps] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/pg_slugify +schemas/inflection/procedures/no_single_underscores [schemas/inflection/schema] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/no_single_underscores +schemas/inflection/procedures/underscore [schemas/inflection/schema schemas/inflection/procedures/pg_slugify schemas/inflection/procedures/no_single_underscores] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/underscore +schemas/inflection/procedures/camel [schemas/inflection/schema schemas/inflection/procedures/underscore] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/camel +schemas/inflection/procedures/dashed [schemas/inflection/schema schemas/inflection/procedures/underscore] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/dashed +schemas/inflection/procedures/pascal [schemas/inflection/schema schemas/inflection/procedures/camel] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/pascal +schemas/inflection/tables/inflection_rules/table [schemas/inflection/schema] 2017-08-11T08:11:51Z skitch # add schemas/inflection/tables/inflection_rules/table +schemas/inflection/procedures/uncountable_words [schemas/inflection/schema] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/uncountable_words +schemas/inflection/procedures/should_skip_uncountable [schemas/inflection/schema schemas/inflection/procedures/uncountable_words] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/should_skip_uncountable +schemas/inflection/procedures/plural [schemas/inflection/schema schemas/inflection/tables/inflection_rules/table schemas/inflection/procedures/should_skip_uncountable] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/plural +schemas/inflection/procedures/singular [schemas/inflection/schema schemas/inflection/tables/inflection_rules/table schemas/inflection/procedures/should_skip_uncountable] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/singular +schemas/inflection/procedures/slugify [schemas/inflection/schema] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/slugify +schemas/inflection/tables/inflection_rules/fixtures/1589249334312_fixture [schemas/inflection/schema schemas/inflection/tables/inflection_rules/table] 2017-08-11T08:11:51Z skitch # add schemas/inflection/tables/inflection_rules/fixtures/1589249334312_fixture +schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx [schemas/inflection/schema schemas/inflection/tables/inflection_rules/table] 2017-08-11T08:11:51Z skitch # add schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx diff --git a/extensions/@pgpm/inflection/revert/extension/defaults.sql b/extensions/@pgpm/inflection/revert/extension/defaults.sql new file mode 100644 index 00000000..a5f303fe --- /dev/null +++ b/extensions/@pgpm/inflection/revert/extension/defaults.sql @@ -0,0 +1,5 @@ +-- Revert extension/defaults from pg + +BEGIN; + +COMMIT; diff --git a/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/camel.sql b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/camel.sql new file mode 100644 index 00000000..7356e0f7 --- /dev/null +++ b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/camel.sql @@ -0,0 +1,7 @@ +-- Revert schemas/inflection/procedures/camel from pg + +BEGIN; + +DROP FUNCTION inflection.camel; + +COMMIT; diff --git a/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/dashed.sql b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/dashed.sql new file mode 100644 index 00000000..d822eafa --- /dev/null +++ b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/dashed.sql @@ -0,0 +1,7 @@ +-- Revert schemas/inflection/procedures/dashed from pg + +BEGIN; + +DROP FUNCTION inflection.dashed; + +COMMIT; diff --git a/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/lower.sql b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/lower.sql new file mode 100644 index 00000000..47c0f9dd --- /dev/null +++ b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/lower.sql @@ -0,0 +1,7 @@ +-- Revert schemas/inflection/procedures/lower from pg + +BEGIN; + +DROP FUNCTION inflection.lower; + +COMMIT; diff --git a/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/no_consecutive_caps.sql b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/no_consecutive_caps.sql new file mode 100644 index 00000000..4afef615 --- /dev/null +++ b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/no_consecutive_caps.sql @@ -0,0 +1,7 @@ +-- Revert schemas/inflection/procedures/no_consecutive_caps from pg + +BEGIN; + +DROP FUNCTION inflection.no_consecutive_caps; + +COMMIT; diff --git a/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/no_single_underscores.sql b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/no_single_underscores.sql new file mode 100644 index 00000000..4eaf90b0 --- /dev/null +++ b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/no_single_underscores.sql @@ -0,0 +1,7 @@ +-- Revert schemas/inflection/procedures/no_single_underscores from pg + +BEGIN; + +DROP FUNCTION inflection.no_single_underscores; + +COMMIT; diff --git a/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/pascal.sql b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/pascal.sql new file mode 100644 index 00000000..c0d2b843 --- /dev/null +++ b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/pascal.sql @@ -0,0 +1,7 @@ +-- Revert schemas/inflection/procedures/pascal from pg + +BEGIN; + +DROP FUNCTION inflection.pascal; + +COMMIT; diff --git a/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/pg_slugify.sql b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/pg_slugify.sql new file mode 100644 index 00000000..61c77dd4 --- /dev/null +++ b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/pg_slugify.sql @@ -0,0 +1,8 @@ +-- Revert schemas/inflection/procedures/pg_slugify from pg + +BEGIN; + +DROP FUNCTION inflection.pg_slugify(text); +DROP FUNCTION inflection.pg_slugify(text, boolean); + +COMMIT; diff --git a/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/plural.sql b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/plural.sql new file mode 100644 index 00000000..de84f828 --- /dev/null +++ b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/plural.sql @@ -0,0 +1,7 @@ +-- Revert schemas/inflection/procedures/plural from pg + +BEGIN; + +DROP FUNCTION inflection.plural; + +COMMIT; diff --git a/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/should_skip_uncountable.sql b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/should_skip_uncountable.sql new file mode 100644 index 00000000..62ce338f --- /dev/null +++ b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/should_skip_uncountable.sql @@ -0,0 +1,7 @@ +-- Revert schemas/inflection/procedures/should_skip_uncountable from pg + +BEGIN; + +DROP FUNCTION inflection.should_skip_uncountable; + +COMMIT; diff --git a/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/singular.sql b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/singular.sql new file mode 100644 index 00000000..afa571ce --- /dev/null +++ b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/singular.sql @@ -0,0 +1,7 @@ +-- Revert schemas/inflection/procedures/singular from pg + +BEGIN; + +DROP FUNCTION inflection.singular; + +COMMIT; diff --git a/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/slugify.sql b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/slugify.sql new file mode 100644 index 00000000..d0e69566 --- /dev/null +++ b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/slugify.sql @@ -0,0 +1,8 @@ +-- Revert schemas/inflection/procedures/slugify from pg + +BEGIN; + +DROP FUNCTION inflection.slugify(text); +DROP FUNCTION inflection.slugify(text, boolean); + +COMMIT; diff --git a/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/slugify_trigger.sql b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/slugify_trigger.sql new file mode 100644 index 00000000..8257a458 --- /dev/null +++ b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/slugify_trigger.sql @@ -0,0 +1,7 @@ +-- Revert schemas/inflection/procedures/slugify_trigger from pg + +BEGIN; + +DROP FUNCTION inflection.slugify_trigger; + +COMMIT; diff --git a/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/uncountable_words.sql b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/uncountable_words.sql new file mode 100644 index 00000000..52b07ed9 --- /dev/null +++ b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/uncountable_words.sql @@ -0,0 +1,7 @@ +-- Revert schemas/inflection/procedures/uncountable_words from pg + +BEGIN; + +DROP FUNCTION inflection.uncountable_words; + +COMMIT; diff --git a/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/underscore.sql b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/underscore.sql new file mode 100644 index 00000000..a8e3167f --- /dev/null +++ b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/underscore.sql @@ -0,0 +1,8 @@ +-- Revert schemas/inflection/procedures/underscore from pg + +BEGIN; + +DROP FUNCTION inflection.underscore(text[]); +DROP FUNCTION inflection.underscore(text); + +COMMIT; diff --git a/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/upper.sql b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/upper.sql new file mode 100644 index 00000000..2fa310a4 --- /dev/null +++ b/extensions/@pgpm/inflection/revert/schemas/inflection/procedures/upper.sql @@ -0,0 +1,7 @@ +-- Revert schemas/inflection/procedures/upper from pg + +BEGIN; + +DROP FUNCTION inflection.upper; + +COMMIT; diff --git a/extensions/@pgpm/inflection/revert/schemas/inflection/schema.sql b/extensions/@pgpm/inflection/revert/schemas/inflection/schema.sql new file mode 100644 index 00000000..51f6fa84 --- /dev/null +++ b/extensions/@pgpm/inflection/revert/schemas/inflection/schema.sql @@ -0,0 +1,7 @@ +-- Revert schemas/inflection/schema from pg + +BEGIN; + +DROP SCHEMA inflection CASCADE; + +COMMIT; diff --git a/extensions/@pgpm/inflection/revert/schemas/inflection/tables/inflection_rules/fixtures/1589249334312_fixture.sql b/extensions/@pgpm/inflection/revert/schemas/inflection/tables/inflection_rules/fixtures/1589249334312_fixture.sql new file mode 100644 index 00000000..00225fdc --- /dev/null +++ b/extensions/@pgpm/inflection/revert/schemas/inflection/tables/inflection_rules/fixtures/1589249334312_fixture.sql @@ -0,0 +1,5 @@ +-- Revert schemas/inflection/tables/inflection_rules/fixtures/1589249334312_fixture from pg + +BEGIN; + +COMMIT; diff --git a/extensions/@pgpm/inflection/revert/schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx.sql b/extensions/@pgpm/inflection/revert/schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx.sql new file mode 100644 index 00000000..8eb83142 --- /dev/null +++ b/extensions/@pgpm/inflection/revert/schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx.sql @@ -0,0 +1,7 @@ +-- Revert schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx from pg + +BEGIN; + +DROP INDEX inflection.inflection_rules_type_idx; + +COMMIT; diff --git a/extensions/@pgpm/inflection/revert/schemas/inflection/tables/inflection_rules/table.sql b/extensions/@pgpm/inflection/revert/schemas/inflection/tables/inflection_rules/table.sql new file mode 100644 index 00000000..7f9101ea --- /dev/null +++ b/extensions/@pgpm/inflection/revert/schemas/inflection/tables/inflection_rules/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/inflection/tables/inflection_rules/table from pg + +BEGIN; + +DROP TABLE inflection.inflection_rules; + +COMMIT; diff --git a/extensions/@pgpm/inflection/sql/pgpm-inflection--0.15.3.sql b/extensions/@pgpm/inflection/sql/pgpm-inflection--0.15.3.sql new file mode 100644 index 00000000..59ca2149 --- /dev/null +++ b/extensions/@pgpm/inflection/sql/pgpm-inflection--0.15.3.sql @@ -0,0 +1,482 @@ +\echo Use "CREATE EXTENSION pgpm-inflection" to load this file. \quit +CREATE SCHEMA inflection; + +GRANT USAGE ON SCHEMA inflection TO PUBLIC; + +ALTER DEFAULT PRIVILEGES IN SCHEMA inflection + GRANT EXECUTE ON FUNCTIONS TO PUBLIC; + +CREATE FUNCTION inflection.no_consecutive_caps_till_end(str text) RETURNS text AS $EOFCODE$ +DECLARE + result text[]; + temp text; +BEGIN + FOR result IN + SELECT regexp_matches(str, E'([A-Z])([A-Z]+$)', 'g') + LOOP + temp = result[1] || lower(result[2]); + str = replace(str, result[1] || result[2], temp); + END LOOP; + return str; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION inflection.no_consecutive_caps_till_lower(str text) RETURNS text AS $EOFCODE$ +DECLARE + result text[]; + temp text; +BEGIN + FOR result IN + SELECT regexp_matches(str, E'([A-Z])([A-Z]+)[A-Z][a-z]', 'g') + LOOP + temp = result[1] || lower(result[2]); + str = replace(str, result[1] || result[2], temp); + END LOOP; + + return str; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION inflection.no_consecutive_caps(str text) RETURNS text AS $EOFCODE$ + select inflection.no_consecutive_caps_till_lower(inflection.no_consecutive_caps_till_end(str)); +$EOFCODE$ LANGUAGE sql STABLE; + +CREATE FUNCTION inflection.pg_slugify(value text, allow_unicode boolean) RETURNS text AS $EOFCODE$ + WITH normalized AS ( + SELECT + CASE WHEN allow_unicode THEN + value + ELSE + unaccent (value) + END AS value +), +no_consecutive_caps AS ( + SELECT + inflection.no_consecutive_caps (value) AS value +FROM + normalized +), +remove_chars AS ( + SELECT + regexp_replace(value, E'[^\\w\\s-]', '', 'gi') AS value +FROM + no_consecutive_caps +), +trimmed AS ( + SELECT + trim(value) AS value +FROM + remove_chars +), +hyphenated AS ( + SELECT + regexp_replace(value, E'[-\\s]+', '-', 'gi') AS value +FROM + trimmed +), +underscored AS ( + SELECT + regexp_replace(value, E'[-]+', '_', 'gi') AS value +FROM + hyphenated +), +removedups AS ( + SELECT + regexp_replace(value, E'[_]+', '_', 'gi') AS value +FROM + underscored +) +SELECT + value +FROM + removedups; +$EOFCODE$ LANGUAGE sql STRICT IMMUTABLE; + +CREATE FUNCTION inflection.pg_slugify(text) RETURNS text AS $EOFCODE$SELECT inflection.pg_slugify($1, false)$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION inflection.no_single_underscores_in_beginning(str text) RETURNS text AS $EOFCODE$ +DECLARE + result text[]; + temp text; +BEGIN + FOR result IN + SELECT regexp_matches(str, E'(^[a-z])(_)', 'g') + LOOP + str = replace(str, result[1] || result[2], result[1]); + END LOOP; + return str; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION inflection.no_single_underscores_at_end(str text) RETURNS text AS $EOFCODE$ +DECLARE + result text[]; + temp text; +BEGIN + FOR result IN + SELECT regexp_matches(str, E'(_)([a-z]$)', 'g') + LOOP + str = replace(str, result[1] || result[2], result[2]); + END LOOP; + + return str; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION inflection.no_single_underscores_in_middle(str text) RETURNS text AS $EOFCODE$ +DECLARE + result text[]; + temp text; +BEGIN + FOR result IN + SELECT regexp_matches(str, E'(_)([a-z]_)', 'g') + LOOP + str = replace(str, result[1] || result[2], result[2]); + END LOOP; + + return str; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION inflection.no_single_underscores(str text) RETURNS text AS $EOFCODE$ + select + inflection.no_single_underscores_in_middle(inflection.no_single_underscores_at_end(inflection.no_single_underscores_in_beginning(str))); +$EOFCODE$ LANGUAGE sql STABLE; + +CREATE FUNCTION inflection.underscore(str text) RETURNS text AS $EOFCODE$ + WITH slugged AS ( + SELECT + inflection.pg_slugify(str) AS value +), +convertedupper AS ( + SELECT + lower(regexp_replace(value, E'([A-Z])', E'\_\\1', 'g')) AS value + FROM + slugged +), +noprefix AS ( + SELECT + regexp_replace(value, E'^_', '', 'g') AS value + FROM + convertedupper +), +removedups AS ( + SELECT + regexp_replace(value, E'[_]+', '_', 'gi') AS value +FROM + noprefix +), +stripedges AS ( + SELECT + regexp_replace(regexp_replace(value, E'([A-Z])_$', E'\\1', 'gi'), E'^_([A-Z])', E'\\1', 'gi') AS value +FROM + removedups +), +nosingles AS ( + SELECT + inflection.no_single_underscores(value) AS value +FROM + stripedges +) +SELECT + value +FROM + nosingles; +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION inflection.underscore(parts text[]) RETURNS text AS $EOFCODE$ + SELECT inflection.underscore(array_to_string(parts, '_')); +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION inflection.camel(str text) RETURNS text AS $EOFCODE$ +DECLARE + result text[]; +BEGIN + str = inflection.underscore(str); + FOR result IN + SELECT regexp_matches(str, E'(_[a-zA-Z0-9])', 'g') + LOOP + str = replace(str, result[1], upper(result[1])); + END LOOP; + return regexp_replace(substring(str FROM 1 FOR 1) || substring(str FROM 2 FOR length(str)), E'[_]+', '', 'gi'); +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION inflection.dashed(str text) RETURNS text AS $EOFCODE$ + WITH underscored AS ( + SELECT + inflection.underscore(str) AS value +), +dashes AS ( + SELECT + regexp_replace(value, '_', '-', 'gi') AS value + FROM + underscored +) +SELECT + value +FROM + dashes; +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION inflection.pascal(str text) RETURNS text AS $EOFCODE$ +DECLARE + result text[]; +BEGIN + str = inflection.camel(str); + return upper(substring(str FROM 1 FOR 1)) || substring(str FROM 2 FOR length(str)); +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE TABLE inflection.inflection_rules ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + type text, + test text, + replacement text +); + +GRANT SELECT ON inflection.inflection_rules TO PUBLIC; + +CREATE FUNCTION inflection.plural(str text) RETURNS text AS $EOFCODE$ +DECLARE + result record; + matches text[]; +BEGIN + IF inflection.should_skip_uncountable(lower(str)) THEN + return str; + END IF; + + FOR result IN + SELECT * FROM inflection.inflection_rules where type='plural' + LOOP + matches = regexp_matches(str, result.test, 'gi'); + IF (array_length(matches, 1) > 0) THEN + IF (result.replacement IS NULL) THEN + return str; + END IF; + str = regexp_replace(str, result.test, result.replacement, 'gi'); + return str; + END IF; + END LOOP; + return str; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION inflection.uncountable_words() RETURNS text[] AS $EOFCODE$ +select ARRAY[ 'accommodation', 'adulthood', 'advertising', 'advice', 'aggression', 'aid', 'air', 'aircraft', 'alcohol', 'anger', 'applause', 'arithmetic', 'assistance', 'athletics', 'bacon', 'baggage', 'beef', 'biology', 'blood', 'botany', 'bread', 'butter', 'carbon', 'cardboard', 'cash', 'chalk', 'chaos', 'chess', 'crossroads', 'countryside', 'dancing', 'deer', 'dignity', 'dirt', 'dust', 'economics', 'education', 'electricity', 'engineering', 'enjoyment', 'envy', 'equipment', 'ethics', 'evidence', 'evolution', 'fame', 'fiction', 'flour', 'flu', 'food', 'fuel', 'fun', 'furniture', 'gallows', 'garbage', 'garlic', 'genetics', 'gold', 'golf', 'gossip', 'grammar', 'gratitude', 'grief', 'guilt', 'gymnastics', 'happiness', 'hardware', 'harm', 'hate', 'hatred', 'health', 'heat', 'help', 'homework', 'honesty', 'honey', 'hospitality', 'housework', 'humour', 'hunger', 'hydrogen', 'ice', 'importance', 'inflation', 'information', 'innocence', 'iron', 'irony', 'jam', 'jewelry', 'judo', 'karate', 'knowledge', 'lack', 'laughter', 'lava', 'leather', 'leisure', 'lightning', 'linguine', 'linguini', 'linguistics', 'literature', 'litter', 'livestock', 'logic', 'loneliness', 'luck', 'luggage', 'macaroni', 'machinery', 'magic', 'management', 'mankind', 'marble', 'mathematics', 'mayonnaise', 'measles', 'methane', 'milk', 'minus', 'money', 'mud', 'music', 'mumps', 'nature', 'news', 'nitrogen', 'nonsense', 'nurture', 'nutrition', 'obedience', 'obesity', 'oxygen', 'pasta', 'patience', 'physics', 'poetry', 'pollution', 'poverty', 'pride', 'psychology', 'publicity', 'punctuation', 'quartz', 'racism', 'relaxation', 'reliability', 'research', 'respect', 'revenge', 'rice', 'rubbish', 'rum', 'safety', 'scenery', 'seafood', 'seaside', 'series', 'shame', 'sheep', 'shopping', 'sleep', 'smoke', 'smoking', 'snow', 'soap', 'software', 'soil', 'spaghetti', 'species', 'steam', 'stuff', 'stupidity', 'sunshine', 'symmetry', 'tennis', 'thirst', 'thunder', 'timber', 'traffic', 'transportation', 'trust', 'underwear', 'unemployment', 'unity', 'validity', 'veal', 'vegetation', 'vegetarianism', 'vengeance', 'violence', 'vitality', 'warmth', 'wealth', 'weather', 'welfare', 'wheat', 'wildlife', 'wisdom', 'yoga', 'zinc', 'zoology' ]; +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION inflection.should_skip_uncountable(str text) RETURNS boolean AS $EOFCODE$ + SELECT + str = ANY (inflection.uncountable_words ()); +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION inflection.singular(str text) RETURNS text AS $EOFCODE$ +DECLARE + result record; + matches text[]; +BEGIN + IF inflection.should_skip_uncountable(lower(str)) THEN + return str; + END IF; + + FOR result IN + SELECT * FROM inflection.inflection_rules where type='singular' + LOOP + matches = regexp_matches(str, result.test, 'gi'); + IF (array_length(matches, 1) > 0) THEN + IF (result.replacement IS NULL) THEN + return str; + END IF; + str = regexp_replace(str, result.test, result.replacement, 'gi'); + return str; + END IF; + END LOOP; + return str; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION inflection.slugify(value text, allow_unicode boolean) RETURNS text AS $EOFCODE$ + WITH normalized AS ( + SELECT + CASE WHEN allow_unicode THEN + value + ELSE + unaccent (value) + END AS value +), +remove_chars AS ( + SELECT + regexp_replace(value, E'[^\\w\\s-]', '', 'gi') AS value +FROM + normalized +), +lowercase AS ( + SELECT + lower(value) AS value +FROM + remove_chars +), +trimmed AS ( + SELECT + trim(value) AS value +FROM + lowercase +), +hyphenated AS ( + SELECT + regexp_replace(value, E'[-\\s]+', '-', 'gi') AS value +FROM + trimmed +) +SELECT + value +FROM + hyphenated; +$EOFCODE$ LANGUAGE sql STRICT IMMUTABLE; + +CREATE FUNCTION inflection.slugify(text) RETURNS text AS $EOFCODE$SELECT inflection.slugify($1, false)$EOFCODE$ LANGUAGE sql IMMUTABLE; + +INSERT INTO inflection.inflection_rules ( + type, + test, + replacement +) VALUES + -- plural guards: already-plural words return as-is (NULL replacement) + ('plural', '^(m|wom)en$', NULL), + ('plural', '(pe)ople$', NULL), + ('plural', '(child)ren$', NULL), + ('plural', '([ti])a$', NULL), + ('plural', '((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$', NULL), + ('plural', '(database|codebase|firebase|knowledgebase)s$', NULL), + ('plural', '(drive)s$', NULL), + ('plural', '(hi|ti)ves$', NULL), + ('plural', '(curve)s$', NULL), + ('plural', '([lr])ves$', NULL), + ('plural', '([^fo])ves$', NULL), + ('plural', '([^aeiouy]|qu)ies$', NULL), + ('plural', '(s)eries$', NULL), + ('plural', '(m)ovies$', NULL), + ('plural', '(x|ch|ss|sh)es$', NULL), + ('plural', '([m|l])ice$', NULL), + ('plural', '(bus)es$', NULL), + ('plural', '(o)es$', NULL), + ('plural', '(shoe)s$', NULL), + ('plural', '(cris|ax|test)es$', NULL), + ('plural', '(octop|vir)uses$', NULL), + ('plural', '(alias|canvas|status|campus)es$', NULL), + ('plural', '^(summons|bonus)es$', NULL), + ('plural', '^(ox)en', NULL), + ('plural', '(matr)ices$', NULL), + ('plural', '(vert|ind)ices$', NULL), + ('plural', '^feet$', NULL), + ('plural', '^teeth$', NULL), + ('plural', '^geese$', NULL), + ('plural', '(quiz)zes$', NULL), + ('plural', '^(whereas)es$', NULL), + ('plural', '^(criteri)a$', NULL), + ('plural', '^genera$', NULL), + -- plural replacement rules + ('plural', '^(m|wom)an$', E'\\1en'), + ('plural', '(pe)rson$', E'\\1ople'), + ('plural', '(child)$', E'\\1ren'), + ('plural', '(drive)$', E'\\1s'), + ('plural', '^(ox)$', E'\\1en'), + ('plural', '(ax|test)is$', E'\\1es'), + ('plural', '(octop|vir)us$', E'\\1uses'), + ('plural', '(alias|status|canvas|campus)$', E'\\1es'), + ('plural', '^(summons|bonus)$', E'\\1es'), + ('plural', '(bu)s$', E'\\1ses'), + ('plural', '(buffal|tomat|potat)o$', E'\\1oes'), + ('plural', '([ti])um$', E'\\1a'), + ('plural', 'sis$', 'ses'), + ('plural', '(?:([^f])fe|([lr])f)$', E'\\1\\2ves'), + ('plural', '^(focus)$', E'\\1es'), + ('plural', '(hi|ti)ve$', E'\\1ves'), + ('plural', '([^aeiouy]|qu)y$', E'\\1ies'), + ('plural', '(matr)ix$', E'\\1ices'), + ('plural', '(vert|ind)ex$', E'\\1ices'), + ('plural', '(x|ch|ss|sh)$', E'\\1es'), + ('plural', '([m|l])ouse$', E'\\1ice'), + ('plural', '^foot$', 'feet'), + ('plural', '^tooth$', 'teeth'), + ('plural', '^goose$', 'geese'), + ('plural', '(quiz)$', E'\\1zes'), + ('plural', '^(whereas)$', E'\\1es'), + ('plural', '^(criteri)on$', E'\\1a'), + ('plural', '^genus$', 'genera'), + ('plural', 's$', 's'), + ('plural', '$', 's'), + -- singular guards: already-singular words return as-is (NULL replacement) + ('singular', '^(m|wom)an$', NULL), + ('singular', '(pe)rson$', NULL), + ('singular', '(child)$', NULL), + ('singular', '(drive)$', NULL), + ('singular', '^(ox)$', NULL), + ('singular', '(ax|test)is$', NULL), + ('singular', '(octop|vir)us$', NULL), + ('singular', '(alias|status|canvas|campus)$', NULL), + ('singular', '^(summons|bonus)$', NULL), + ('singular', '(bu)s$', NULL), + ('singular', '(buffal|tomat|potat)o$', NULL), + ('singular', '([ti])um$', NULL), + ('singular', 'sis$', NULL), + ('singular', '(?:([^f])fe|([lr])f)$', NULL), + ('singular', '^(focus)$', NULL), + ('singular', '(hi|ti)ve$', NULL), + ('singular', '([^aeiouy]|qu)y$', NULL), + ('singular', '(x|ch|ss|sh)$', NULL), + ('singular', '(matr)ix$', NULL), + ('singular', '(vert|ind)ex$', NULL), + ('singular', '([m|l])ouse$', NULL), + ('singular', '^foot$', NULL), + ('singular', '^tooth$', NULL), + ('singular', '^goose$', NULL), + ('singular', '(quiz)$', NULL), + ('singular', '^(whereas)$', NULL), + ('singular', '^(criteri)on$', NULL), + ('singular', '^genus$', NULL), + -- singular replacement rules + ('singular', '^(m|wom)en$', E'\\1an'), + ('singular', '(pe)ople$', E'\\1rson'), + ('singular', '(child)ren$', E'\\1'), + ('singular', '(database|codebase|firebase|knowledgebase)s$', E'\\1'), + ('singular', '(drive)s$', E'\\1'), + ('singular', '^genera$', 'genus'), + ('singular', '^(criteri)a$', E'\\1on'), + -- Latin suffix overrides (PostGraphile-compatible) + ('singular', '(schema)ta$', E'\\1'), + ('singular', '(phenomen)a$', E'\\1on'), + ('singular', '(memorand)a$', E'\\1um'), + ('singular', '(curricul)a$', E'\\1um'), + ('singular', '([ti])a$', E'\\1um'), + ('singular', '((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$', E'\\1\\2sis'), + ('singular', '(hi|ti)ves$', E'\\1ve'), + ('singular', '(curve)s$', E'\\1'), + ('singular', '([lr])ves$', E'\\1f'), + ('singular', '([a])ves$', E'\\1ve'), + ('singular', '([^fo])ves$', E'\\1fe'), + ('singular', '(m)ovies$', E'\\1ovie'), + ('singular', '([^aeiouy]|qu)ies$', E'\\1y'), + ('singular', '(s)eries$', E'\\1eries'), + ('singular', '(x|ch|ss|sh)es$', E'\\1'), + ('singular', '([m|l])ice$', E'\\1ouse'), + ('singular', '(bus)es$', E'\\1'), + ('singular', '(o)es$', E'\\1'), + ('singular', '(shoe)s$', E'\\1'), + ('singular', '(cris|ax|test)es$', E'\\1is'), + ('singular', '(octop|vir)uses$', E'\\1us'), + ('singular', '(alias|canvas|status|campus)es$', E'\\1'), + ('singular', '^(summons|bonus)es$', E'\\1'), + ('singular', '^(ox)en', E'\\1'), + ('singular', '(matr)ices$', E'\\1ix'), + ('singular', '(vert|ind)ices$', E'\\1ex'), + ('singular', '^feet$', 'foot'), + ('singular', '^teeth$', 'tooth'), + ('singular', '^geese$', 'goose'), + ('singular', '(quiz)zes$', E'\\1'), + ('singular', '^(whereas)es$', E'\\1'), + ('singular', 'ss$', 'ss'), + ('singular', 's$', ''); + +CREATE INDEX inflection_rules_type_idx ON inflection.inflection_rules (type); diff --git a/extensions/@pgpm/inflection/sql/pgpm-inflection--0.15.5.sql b/extensions/@pgpm/inflection/sql/pgpm-inflection--0.15.5.sql new file mode 100644 index 00000000..ee69d58a --- /dev/null +++ b/extensions/@pgpm/inflection/sql/pgpm-inflection--0.15.5.sql @@ -0,0 +1,1041 @@ +\echo Use "CREATE EXTENSION pgpm-inflection" to load this file. \quit +CREATE SCHEMA inflection; + +GRANT USAGE ON SCHEMA inflection TO PUBLIC; + +ALTER DEFAULT PRIVILEGES IN SCHEMA inflection + GRANT EXECUTE ON FUNCTIONS TO PUBLIC; + +CREATE FUNCTION inflection.no_consecutive_caps_till_end( + str text +) RETURNS text AS $EOFCODE$ +DECLARE + result text[]; + temp text; +BEGIN + FOR result IN + SELECT regexp_matches(str, E'([A-Z])([A-Z]+$)', 'g') + LOOP + temp = result[1] || lower(result[2]); + str = replace(str, result[1] || result[2], temp); + END LOOP; + return str; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION inflection.no_consecutive_caps_till_lower( + str text +) RETURNS text AS $EOFCODE$ +DECLARE + result text[]; + temp text; +BEGIN + FOR result IN + SELECT regexp_matches(str, E'([A-Z])([A-Z]+)[A-Z][a-z]', 'g') + LOOP + temp = result[1] || lower(result[2]); + str = replace(str, result[1] || result[2], temp); + END LOOP; + + return str; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION inflection.no_consecutive_caps( + str text +) RETURNS text AS $EOFCODE$ + select inflection.no_consecutive_caps_till_lower(inflection.no_consecutive_caps_till_end(str)); +$EOFCODE$ LANGUAGE sql STABLE; + +CREATE FUNCTION inflection.pg_slugify( + value text, + allow_unicode boolean +) RETURNS text AS $EOFCODE$ + WITH normalized AS ( + SELECT + CASE WHEN allow_unicode THEN + value + ELSE + unaccent (value) + END AS value +), +no_consecutive_caps AS ( + SELECT + inflection.no_consecutive_caps (value) AS value +FROM + normalized +), +remove_chars AS ( + SELECT + regexp_replace(value, E'[^\\w\\s-]', '', 'gi') AS value +FROM + no_consecutive_caps +), +trimmed AS ( + SELECT + trim(value) AS value +FROM + remove_chars +), +hyphenated AS ( + SELECT + regexp_replace(value, E'[-\\s]+', '-', 'gi') AS value +FROM + trimmed +), +underscored AS ( + SELECT + regexp_replace(value, E'[-]+', '_', 'gi') AS value +FROM + hyphenated +), +removedups AS ( + SELECT + regexp_replace(value, E'[_]+', '_', 'gi') AS value +FROM + underscored +) +SELECT + value +FROM + removedups; +$EOFCODE$ LANGUAGE sql STRICT IMMUTABLE; + +CREATE FUNCTION inflection.pg_slugify( + text +) RETURNS text AS $EOFCODE$SELECT inflection.pg_slugify($1, false)$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION inflection.no_single_underscores_in_beginning( + str text +) RETURNS text AS $EOFCODE$ +DECLARE + result text[]; + temp text; +BEGIN + FOR result IN + SELECT regexp_matches(str, E'(^[a-z])(_)', 'g') + LOOP + str = replace(str, result[1] || result[2], result[1]); + END LOOP; + return str; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION inflection.no_single_underscores_at_end( + str text +) RETURNS text AS $EOFCODE$ +DECLARE + result text[]; + temp text; +BEGIN + FOR result IN + SELECT regexp_matches(str, E'(_)([a-z]$)', 'g') + LOOP + str = replace(str, result[1] || result[2], result[2]); + END LOOP; + + return str; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION inflection.no_single_underscores_in_middle( + str text +) RETURNS text AS $EOFCODE$ +DECLARE + result text[]; + temp text; +BEGIN + FOR result IN + SELECT regexp_matches(str, E'(_)([a-z]_)', 'g') + LOOP + str = replace(str, result[1] || result[2], result[2]); + END LOOP; + + return str; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION inflection.no_single_underscores( + str text +) RETURNS text AS $EOFCODE$ + select + inflection.no_single_underscores_in_middle(inflection.no_single_underscores_at_end(inflection.no_single_underscores_in_beginning(str))); +$EOFCODE$ LANGUAGE sql STABLE; + +CREATE FUNCTION inflection.underscore( + str text +) RETURNS text AS $EOFCODE$ + WITH slugged AS ( + SELECT + inflection.pg_slugify(str) AS value +), +convertedupper AS ( + SELECT + lower(regexp_replace(value, E'([A-Z])', E'\_\\1', 'g')) AS value + FROM + slugged +), +noprefix AS ( + SELECT + regexp_replace(value, E'^_', '', 'g') AS value + FROM + convertedupper +), +removedups AS ( + SELECT + regexp_replace(value, E'[_]+', '_', 'gi') AS value +FROM + noprefix +), +stripedges AS ( + SELECT + regexp_replace(regexp_replace(value, E'([A-Z])_$', E'\\1', 'gi'), E'^_([A-Z])', E'\\1', 'gi') AS value +FROM + removedups +), +nosingles AS ( + SELECT + inflection.no_single_underscores(value) AS value +FROM + stripedges +) +SELECT + value +FROM + nosingles; +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION inflection.underscore( + parts text[] +) RETURNS text AS $EOFCODE$ + SELECT inflection.underscore(array_to_string(parts, '_')); +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION inflection.camel( + str text +) RETURNS text AS $EOFCODE$ +DECLARE + result text[]; +BEGIN + str = inflection.underscore(str); + FOR result IN + SELECT regexp_matches(str, E'(_[a-zA-Z0-9])', 'g') + LOOP + str = replace(str, result[1], upper(result[1])); + END LOOP; + return regexp_replace(substring(str FROM 1 FOR 1) || substring(str FROM 2 FOR length(str)), E'[_]+', '', 'gi'); +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION inflection.dashed( + str text +) RETURNS text AS $EOFCODE$ + WITH underscored AS ( + SELECT + inflection.underscore(str) AS value +), +dashes AS ( + SELECT + regexp_replace(value, '_', '-', 'gi') AS value + FROM + underscored +) +SELECT + value +FROM + dashes; +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION inflection.pascal( + str text +) RETURNS text AS $EOFCODE$ +DECLARE + result text[]; +BEGIN + str = inflection.camel(str); + return upper(substring(str FROM 1 FOR 1)) || substring(str FROM 2 FOR length(str)); +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE TABLE inflection.inflection_rules ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + type text, + test text, + replacement text +); + +GRANT SELECT ON inflection.inflection_rules TO PUBLIC; + +CREATE FUNCTION inflection.uncountable_words() RETURNS text[] AS $EOFCODE$ +select ARRAY[ 'accommodation', 'adulthood', 'advertising', 'advice', 'aggression', 'aid', 'air', 'aircraft', 'alcohol', 'anger', 'applause', 'arithmetic', 'assistance', 'athletics', 'bacon', 'baggage', 'beef', 'biology', 'blood', 'botany', 'bread', 'butter', 'carbon', 'cardboard', 'cash', 'chalk', 'chaos', 'chess', 'crossroads', 'countryside', 'dancing', 'deer', 'dignity', 'dirt', 'dust', 'economics', 'education', 'electricity', 'engineering', 'enjoyment', 'envy', 'equipment', 'ethics', 'evidence', 'evolution', 'fame', 'fiction', 'flour', 'flu', 'food', 'fuel', 'fun', 'furniture', 'gallows', 'garbage', 'garlic', 'genetics', 'gold', 'golf', 'gossip', 'grammar', 'gratitude', 'grief', 'guilt', 'gymnastics', 'happiness', 'hardware', 'harm', 'hate', 'hatred', 'health', 'heat', 'help', 'homework', 'honesty', 'honey', 'hospitality', 'housework', 'humour', 'hunger', 'hydrogen', 'ice', 'importance', 'inflation', 'information', 'innocence', 'iron', 'irony', 'jam', 'jewelry', 'judo', 'karate', 'knowledge', 'lack', 'laughter', 'lava', 'leather', 'leisure', 'lightning', 'linguine', 'linguini', 'linguistics', 'literature', 'litter', 'livestock', 'logic', 'loneliness', 'luck', 'luggage', 'macaroni', 'machinery', 'magic', 'management', 'mankind', 'marble', 'mathematics', 'mayonnaise', 'measles', 'methane', 'milk', 'minus', 'money', 'mud', 'music', 'mumps', 'nature', 'news', 'nitrogen', 'nonsense', 'nurture', 'nutrition', 'obedience', 'obesity', 'oxygen', 'pasta', 'patience', 'physics', 'poetry', 'pollution', 'poverty', 'pride', 'psychology', 'publicity', 'punctuation', 'quartz', 'racism', 'relaxation', 'reliability', 'research', 'respect', 'revenge', 'rice', 'rubbish', 'rum', 'safety', 'scenery', 'seafood', 'seaside', 'series', 'shame', 'sheep', 'shopping', 'sleep', 'smoke', 'smoking', 'snow', 'soap', 'software', 'soil', 'spaghetti', 'species', 'steam', 'stuff', 'stupidity', 'sunshine', 'symmetry', 'tennis', 'thirst', 'thunder', 'timber', 'traffic', 'transportation', 'trust', 'underwear', 'unemployment', 'unity', 'validity', 'veal', 'vegetation', 'vegetarianism', 'vengeance', 'violence', 'vitality', 'warmth', 'wealth', 'weather', 'welfare', 'wheat', 'wildlife', 'wisdom', 'yoga', 'zinc', 'zoology' ]; +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION inflection.should_skip_uncountable( + str text +) RETURNS boolean AS $EOFCODE$ + SELECT + str = ANY (inflection.uncountable_words ()); +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION inflection.plural( + str text +) RETURNS text AS $EOFCODE$ +DECLARE + result record; + matches text[]; +BEGIN + IF inflection.should_skip_uncountable(lower(str)) THEN + return str; + END IF; + + FOR result IN + SELECT * FROM inflection.inflection_rules where type='plural' + LOOP + matches = regexp_matches(str, result.test, 'gi'); + IF (array_length(matches, 1) > 0) THEN + IF (result.replacement IS NULL) THEN + return str; + END IF; + str = regexp_replace(str, result.test, result.replacement, 'gi'); + return str; + END IF; + END LOOP; + return str; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION inflection.singular( + str text +) RETURNS text AS $EOFCODE$ +DECLARE + result record; + matches text[]; +BEGIN + IF inflection.should_skip_uncountable(lower(str)) THEN + return str; + END IF; + + FOR result IN + SELECT * FROM inflection.inflection_rules where type='singular' + LOOP + matches = regexp_matches(str, result.test, 'gi'); + IF (array_length(matches, 1) > 0) THEN + IF (result.replacement IS NULL) THEN + return str; + END IF; + str = regexp_replace(str, result.test, result.replacement, 'gi'); + return str; + END IF; + END LOOP; + return str; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION inflection.slugify( + value text, + allow_unicode boolean +) RETURNS text AS $EOFCODE$ + WITH normalized AS ( + SELECT + CASE WHEN allow_unicode THEN + value + ELSE + unaccent (value) + END AS value +), +remove_chars AS ( + SELECT + regexp_replace(value, E'[^\\w\\s-]', '', 'gi') AS value +FROM + normalized +), +lowercase AS ( + SELECT + lower(value) AS value +FROM + remove_chars +), +trimmed AS ( + SELECT + trim(value) AS value +FROM + lowercase +), +hyphenated AS ( + SELECT + regexp_replace(value, E'[-\\s]+', '-', 'gi') AS value +FROM + trimmed +) +SELECT + value +FROM + hyphenated; +$EOFCODE$ LANGUAGE sql STRICT IMMUTABLE; + +CREATE FUNCTION inflection.slugify( + text +) RETURNS text AS $EOFCODE$SELECT inflection.slugify($1, false)$EOFCODE$ LANGUAGE sql IMMUTABLE; + +INSERT INTO inflection.inflection_rules ( + type, + test, + replacement +) VALUES +( + 'plural', + '^(m|wom)en$', + NULL +), +( + 'plural', + '(pe)ople$', + NULL +), +( + 'plural', + '(child)ren$', + NULL +), +( + 'plural', + '([ti])a$', + NULL +), +( + 'plural', + '((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$', + NULL +), +( + 'plural', + '(database)s$', + NULL +), +( + 'plural', + '(drive)s$', + NULL +), +( + 'plural', + '(hi|ti)ves$', + NULL +), +( + 'plural', + '(curve)s$', + NULL +), +( + 'plural', + '([lr])ves$', + NULL +), +( + 'plural', + '([^fo])ves$', + NULL +), +( + 'plural', + '([^aeiouy]|qu)ies$', + NULL +), +( + 'plural', + '(s)eries$', + NULL +), +( + 'plural', + '(m)ovies$', + NULL +), +( + 'plural', + '(x|ch|ss|sh)es$', + NULL +), +( + 'plural', + '([m|l])ice$', + NULL +), +( + 'plural', + '(bus)es$', + NULL +), +( + 'plural', + '(o)es$', + NULL +), +( + 'plural', + '(shoe)s$', + NULL +), +( + 'plural', + '(cris|ax|test)es$', + NULL +), +( + 'plural', + '(octop|vir)uses$', + NULL +), +( + 'plural', + '(alias|canvas|status|campus)es$', + NULL +), +( + 'plural', + '^(summons|bonus)es$', + NULL +), +( + 'plural', + '^(ox)en', + NULL +), +( + 'plural', + '(matr)ices$', + NULL +), +( + 'plural', + '(vert|ind)ices$', + NULL +), +( + 'plural', + '^feet$', + NULL +), +( + 'plural', + '^teeth$', + NULL +), +( + 'plural', + '^geese$', + NULL +), +( + 'plural', + '(quiz)zes$', + NULL +), +( + 'plural', + '^(whereas)es$', + NULL +), +( + 'plural', + '^(criteri)a$', + NULL +), +( + 'plural', + '^genera$', + NULL +), +( + 'plural', + '^(m|wom)an$', + E'\\1en' +), +( + 'plural', + '(pe)rson$', + E'\\1ople' +), +( + 'plural', + '(child)$', + E'\\1ren' +), +( + 'plural', + '(drive)$', + E'\\1s' +), +( + 'plural', + '^(ox)$', + E'\\1en' +), +( + 'plural', + '(ax|test)is$', + E'\\1es' +), +( + 'plural', + '(octop|vir)us$', + E'\\1uses' +), +( + 'plural', + '(alias|status|canvas|campus)$', + E'\\1es' +), +( + 'plural', + '^(summons|bonus)$', + E'\\1es' +), +( + 'plural', + '(bu)s$', + E'\\1ses' +), +( + 'plural', + '(buffal|tomat|potat)o$', + E'\\1oes' +), +( + 'plural', + '([ti])um$', + E'\\1a' +), +( + 'plural', + 'sis$', + 'ses' +), +( + 'plural', + '(?:([^f])fe|([lr])f)$', + E'\\1\\2ves' +), +( + 'plural', + '^(focus)$', + E'\\1es' +), +( + 'plural', + '(hi|ti)ve$', + E'\\1ves' +), +( + 'plural', + '([^aeiouy]|qu)y$', + E'\\1ies' +), +( + 'plural', + '(matr)ix$', + E'\\1ices' +), +( + 'plural', + '(vert|ind)ex$', + E'\\1ices' +), +( + 'plural', + '(x|ch|ss|sh)$', + E'\\1es' +), +( + 'plural', + '([m|l])ouse$', + E'\\1ice' +), +( + 'plural', + '^foot$', + 'feet' +), +( + 'plural', + '^tooth$', + 'teeth' +), +( + 'plural', + '^goose$', + 'geese' +), +( + 'plural', + '(quiz)$', + E'\\1zes' +), +( + 'plural', + '^(whereas)$', + E'\\1es' +), +( + 'plural', + '^(criteri)on$', + E'\\1a' +), +( + 'plural', + '^genus$', + 'genera' +), +( + 'plural', + 's$', + 's' +), +( + 'plural', + '$', + 's' +), +( + 'singular', + '^(m|wom)an$', + NULL +), +( + 'singular', + '(pe)rson$', + NULL +), +( + 'singular', + '(child)$', + NULL +), +( + 'singular', + '(drive)$', + NULL +), +( + 'singular', + '^(ox)$', + NULL +), +( + 'singular', + '(ax|test)is$', + NULL +), +( + 'singular', + '(octop|vir)us$', + NULL +), +( + 'singular', + '(alias|status|canvas|campus)$', + NULL +), +( + 'singular', + '^(summons|bonus)$', + NULL +), +( + 'singular', + '(bu)s$', + NULL +), +( + 'singular', + '(buffal|tomat|potat)o$', + NULL +), +( + 'singular', + '([ti])um$', + NULL +), +( + 'singular', + 'sis$', + NULL +), +( + 'singular', + '(?:([^f])fe|([lr])f)$', + NULL +), +( + 'singular', + '^(focus)$', + NULL +), +( + 'singular', + '(hi|ti)ve$', + NULL +), +( + 'singular', + '([^aeiouy]|qu)y$', + NULL +), +( + 'singular', + '(x|ch|ss|sh)$', + NULL +), +( + 'singular', + '(matr)ix$', + NULL +), +( + 'singular', + '(vert|ind)ex$', + NULL +), +( + 'singular', + '([m|l])ouse$', + NULL +), +( + 'singular', + '^foot$', + NULL +), +( + 'singular', + '^tooth$', + NULL +), +( + 'singular', + '^goose$', + NULL +), +( + 'singular', + '(quiz)$', + NULL +), +( + 'singular', + '^(whereas)$', + NULL +), +( + 'singular', + '^(criteri)on$', + NULL +), +( + 'singular', + '^genus$', + NULL +), +( + 'singular', + '^(m|wom)en$', + E'\\1an' +), +( + 'singular', + '(pe)ople$', + E'\\1rson' +), +( + 'singular', + '(child)ren$', + E'\\1' +), +( + 'singular', + '(database)s$', + E'\\1' +), +( + 'singular', + '(drive)s$', + E'\\1' +), +( + 'singular', + '^genera$', + 'genus' +), +( + 'singular', + '^(criteri)a$', + E'\\1on' +), +( + 'singular', + '(schema)ta$', + E'\\1' +), +( + 'singular', + '(phenomen)a$', + E'\\1on' +), +( + 'singular', + '(memorand)a$', + E'\\1um' +), +( + 'singular', + '(curricul)a$', + E'\\1um' +), +( + 'singular', + '([ti])a$', + E'\\1um' +), +( + 'singular', + '((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$', + E'\\1\\2sis' +), +( + 'singular', + '(hi|ti)ves$', + E'\\1ve' +), +( + 'singular', + '(curve)s$', + E'\\1' +), +( + 'singular', + '([lr])ves$', + E'\\1f' +), +( + 'singular', + '([a])ves$', + E'\\1ve' +), +( + 'singular', + '([^fo])ves$', + E'\\1fe' +), +( + 'singular', + '(m)ovies$', + E'\\1ovie' +), +( + 'singular', + '([^aeiouy]|qu)ies$', + E'\\1y' +), +( + 'singular', + '(s)eries$', + E'\\1eries' +), +( + 'singular', + '(x|ch|ss|sh)es$', + E'\\1' +), +( + 'singular', + '([m|l])ice$', + E'\\1ouse' +), +( + 'singular', + '(bus)es$', + E'\\1' +), +( + 'singular', + '(o)es$', + E'\\1' +), +( + 'singular', + '(shoe)s$', + E'\\1' +), +( + 'singular', + '(cris|ax|test)es$', + E'\\1is' +), +( + 'singular', + '(octop|vir)uses$', + E'\\1us' +), +( + 'singular', + '(alias|canvas|status|campus)es$', + E'\\1' +), +( + 'singular', + '^(summons|bonus)es$', + E'\\1' +), +( + 'singular', + '^(ox)en', + E'\\1' +), +( + 'singular', + '(matr)ices$', + E'\\1ix' +), +( + 'singular', + '(vert|ind)ices$', + E'\\1ex' +), +( + 'singular', + '^feet$', + 'foot' +), +( + 'singular', + '^teeth$', + 'tooth' +), +( + 'singular', + '^geese$', + 'goose' +), +( + 'singular', + '(quiz)zes$', + E'\\1' +), +( + 'singular', + '^(whereas)es$', + E'\\1' +), +( + 'singular', + 'ss$', + 'ss' +), +( + 'singular', + 's$', + '' +); + +CREATE INDEX inflection_rules_type_idx ON inflection.inflection_rules (type); \ No newline at end of file diff --git a/extensions/@pgpm/inflection/verify/extension/defaults.sql b/extensions/@pgpm/inflection/verify/extension/defaults.sql new file mode 100644 index 00000000..87245abb --- /dev/null +++ b/extensions/@pgpm/inflection/verify/extension/defaults.sql @@ -0,0 +1,5 @@ +-- Verify extension/defaults on pg + +BEGIN; + +ROLLBACK; diff --git a/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/camel.sql b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/camel.sql new file mode 100644 index 00000000..08b938e7 --- /dev/null +++ b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/camel.sql @@ -0,0 +1,7 @@ +-- Verify schemas/inflection/procedures/camel on pg + +BEGIN; + +SELECT verify_function ('inflection.camel'); + +ROLLBACK; diff --git a/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/dashed.sql b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/dashed.sql new file mode 100644 index 00000000..97ab5b3f --- /dev/null +++ b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/dashed.sql @@ -0,0 +1,7 @@ +-- Verify schemas/inflection/procedures/dashed on pg + +BEGIN; + +SELECT verify_function ('inflection.dashed'); + +ROLLBACK; diff --git a/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/lower.sql b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/lower.sql new file mode 100644 index 00000000..41e379df --- /dev/null +++ b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/lower.sql @@ -0,0 +1,7 @@ +-- Verify schemas/inflection/procedures/lower on pg + +BEGIN; + +SELECT verify_function ('inflection.lower'); + +ROLLBACK; diff --git a/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/no_consecutive_caps.sql b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/no_consecutive_caps.sql new file mode 100644 index 00000000..725e139f --- /dev/null +++ b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/no_consecutive_caps.sql @@ -0,0 +1,7 @@ +-- Verify schemas/inflection/procedures/no_consecutive_caps on pg + +BEGIN; + +SELECT verify_function ('inflection.no_consecutive_caps'); + +ROLLBACK; diff --git a/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/no_single_underscores.sql b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/no_single_underscores.sql new file mode 100644 index 00000000..e09f21c6 --- /dev/null +++ b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/no_single_underscores.sql @@ -0,0 +1,7 @@ +-- Verify schemas/inflection/procedures/no_single_underscores on pg + +BEGIN; + +SELECT verify_function ('inflection.no_single_underscores'); + +ROLLBACK; diff --git a/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/pascal.sql b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/pascal.sql new file mode 100644 index 00000000..06f693b3 --- /dev/null +++ b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/pascal.sql @@ -0,0 +1,7 @@ +-- Verify schemas/inflection/procedures/pascal on pg + +BEGIN; + +SELECT verify_function ('inflection.pascal'); + +ROLLBACK; diff --git a/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/pg_slugify.sql b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/pg_slugify.sql new file mode 100644 index 00000000..44194412 --- /dev/null +++ b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/pg_slugify.sql @@ -0,0 +1,7 @@ +-- Verify schemas/inflection/procedures/pg_slugify on pg + +BEGIN; + +SELECT verify_function ('inflection.pg_slugify'); + +ROLLBACK; diff --git a/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/plural.sql b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/plural.sql new file mode 100644 index 00000000..a0aee55d --- /dev/null +++ b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/plural.sql @@ -0,0 +1,7 @@ +-- Verify schemas/inflection/procedures/plural on pg + +BEGIN; + +SELECT verify_function ('inflection.plural'); + +ROLLBACK; diff --git a/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/should_skip_uncountable.sql b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/should_skip_uncountable.sql new file mode 100644 index 00000000..4c7d135e --- /dev/null +++ b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/should_skip_uncountable.sql @@ -0,0 +1,7 @@ +-- Verify schemas/inflection/procedures/should_skip_uncountable on pg + +BEGIN; + +SELECT verify_function ('inflection.should_skip_uncountable'); + +ROLLBACK; diff --git a/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/singular.sql b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/singular.sql new file mode 100644 index 00000000..a39805e3 --- /dev/null +++ b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/singular.sql @@ -0,0 +1,7 @@ +-- Verify schemas/inflection/procedures/singular on pg + +BEGIN; + +SELECT verify_function ('inflection.singular'); + +ROLLBACK; diff --git a/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/slugify.sql b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/slugify.sql new file mode 100644 index 00000000..c824b2c4 --- /dev/null +++ b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/slugify.sql @@ -0,0 +1,7 @@ +-- Verify schemas/inflection/procedures/slugify on pg + +BEGIN; + +SELECT verify_function ('inflection.slugify'); + +ROLLBACK; diff --git a/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/slugify_trigger.sql b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/slugify_trigger.sql new file mode 100644 index 00000000..bbaba2bb --- /dev/null +++ b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/slugify_trigger.sql @@ -0,0 +1,7 @@ +-- Verify schemas/inflection/procedures/slugify_trigger on pg + +BEGIN; + +SELECT verify_function ('inflection.slugify_trigger'); + +ROLLBACK; diff --git a/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/uncountable_words.sql b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/uncountable_words.sql new file mode 100644 index 00000000..8cf2e9c2 --- /dev/null +++ b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/uncountable_words.sql @@ -0,0 +1,7 @@ +-- Verify schemas/inflection/procedures/uncountable_words on pg + +BEGIN; + +SELECT verify_function ('inflection.uncountable_words'); + +ROLLBACK; diff --git a/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/underscore.sql b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/underscore.sql new file mode 100644 index 00000000..3b4f9597 --- /dev/null +++ b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/underscore.sql @@ -0,0 +1,8 @@ +-- Verify schemas/inflection/procedures/underscore on pg + +BEGIN; + +SELECT verify_function ('inflection.underscore(text)'); +SELECT verify_function ('inflection.underscore(text[])'); + +ROLLBACK; diff --git a/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/upper.sql b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/upper.sql new file mode 100644 index 00000000..6ea15974 --- /dev/null +++ b/extensions/@pgpm/inflection/verify/schemas/inflection/procedures/upper.sql @@ -0,0 +1,7 @@ +-- Verify schemas/inflection/procedures/upper on pg + +BEGIN; + +SELECT verify_function ('inflection.upper'); + +ROLLBACK; diff --git a/extensions/@pgpm/inflection/verify/schemas/inflection/schema.sql b/extensions/@pgpm/inflection/verify/schemas/inflection/schema.sql new file mode 100644 index 00000000..45ffe8f5 --- /dev/null +++ b/extensions/@pgpm/inflection/verify/schemas/inflection/schema.sql @@ -0,0 +1,7 @@ +-- Verify schemas/inflection/schema on pg + +BEGIN; + +SELECT verify_schema ('inflection'); + +ROLLBACK; diff --git a/extensions/@pgpm/inflection/verify/schemas/inflection/tables/inflection_rules/fixtures/1589249334312_fixture.sql b/extensions/@pgpm/inflection/verify/schemas/inflection/tables/inflection_rules/fixtures/1589249334312_fixture.sql new file mode 100644 index 00000000..1f32d95e --- /dev/null +++ b/extensions/@pgpm/inflection/verify/schemas/inflection/tables/inflection_rules/fixtures/1589249334312_fixture.sql @@ -0,0 +1,5 @@ +-- Verify schemas/inflection/tables/inflection_rules/fixtures/1589249334312_fixture on pg + +BEGIN; + +ROLLBACK; diff --git a/extensions/@pgpm/inflection/verify/schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx.sql b/extensions/@pgpm/inflection/verify/schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx.sql new file mode 100644 index 00000000..16b0a8cf --- /dev/null +++ b/extensions/@pgpm/inflection/verify/schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx.sql @@ -0,0 +1,7 @@ +-- Verify schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx on pg + +BEGIN; + +SELECT verify_index ('inflection.inflection_rules', 'inflection_rules_type_idx'); + +ROLLBACK; diff --git a/extensions/@pgpm/inflection/verify/schemas/inflection/tables/inflection_rules/table.sql b/extensions/@pgpm/inflection/verify/schemas/inflection/tables/inflection_rules/table.sql new file mode 100644 index 00000000..2c45bf6f --- /dev/null +++ b/extensions/@pgpm/inflection/verify/schemas/inflection/tables/inflection_rules/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/inflection/tables/inflection_rules/table on pg + +BEGIN; + +SELECT verify_table ('inflection.inflection_rules'); + +ROLLBACK; diff --git a/extensions/@pgpm/jwt-claims/LICENSE b/extensions/@pgpm/jwt-claims/LICENSE new file mode 100644 index 00000000..7b18c918 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2025 Dan Lynch +Copyright (c) 2025 Constructive + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/@pgpm/jwt-claims/Makefile b/extensions/@pgpm/jwt-claims/Makefile new file mode 100644 index 00000000..e3f7bef2 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/Makefile @@ -0,0 +1,6 @@ +EXTENSION = pgpm-jwt-claims +DATA = sql/pgpm-jwt-claims--0.15.5.sql + +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) diff --git a/extensions/@pgpm/jwt-claims/README.md b/extensions/@pgpm/jwt-claims/README.md new file mode 100644 index 00000000..a7f848f8 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/README.md @@ -0,0 +1,321 @@ +# @pgpm/jwt-claims + +

+ +

+ +

+ + + + + +

+ +JWT claim handling and validation functions. + +## Overview + +`@pgpm/jwt-claims` provides PostgreSQL functions for extracting and working with JWT (JSON Web Token) claims stored in PostgreSQL session variables. This package enables seamless integration between JWT-based authentication systems and PostgreSQL, allowing database functions to access user context, group memberships, IP addresses, and other JWT payload data. + +## Features + +- **User Context Functions**: Extract user ID from JWT claims +- **Request Metadata**: Get IP address and user agent from requests +- **Database Context**: Access database ID from JWT claims +- **Type-Safe Extraction**: Proper error handling for invalid claim values +- **Session Variables**: Uses PostgreSQL's `current_setting()` for claim storage + +## Installation + +If you have `pgpm` installed: + +```bash +pgpm install @pgpm/jwt-claims +pgpm deploy +``` + +This is a quick way to get started. The sections below provide more detailed installation options. + +### Prerequisites + +```bash +# Install pgpm CLI +npm install -g pgpm + +# Start local Postgres (via Docker) and export env vars +pgpm docker start +eval "$(pgpm env)" +``` + +> **Tip:** Already running Postgres? Skip the Docker step and just export your `PG*` environment variables. + +### **Add to an Existing Package** + +```bash +# 1. Install the package +pgpm install @pgpm/jwt-claims + +# 2. Deploy locally +pgpm deploy +``` + +### **Add to a New Project** + +```bash +# 1. Create a workspace +pgpm init workspace + +# 2. Create your first module +cd my-workspace +pgpm init + +# 3. Install a package +cd packages/my-module +pgpm install @pgpm/jwt-claims + +# 4. Deploy everything +pgpm deploy --createdb --database mydb1 +``` + +## Core Functions + +### jwt_public.current_user_id() +Extracts the user ID from JWT claims. + +**Returns:** `uuid` - The current user's ID, or NULL if not set + +**Usage:** +```sql +SELECT jwt_public.current_user_id(); +``` + +**JWT Claim:** `jwt.claims.user_id` + +### jwt_public.current_ip_address() +Extracts the client's IP address from JWT claims. + +**Returns:** `text` - The client's IP address, or NULL if not set + +**Usage:** +```sql +SELECT jwt_public.current_ip_address(); +``` + +**JWT Claim:** `jwt.claims.ip_address` + +### jwt_public.current_user_agent() +Extracts the client's user agent from JWT claims. + +**Returns:** `text` - The client's user agent string, or NULL if not set + +**Usage:** +```sql +SELECT jwt_public.current_user_agent(); +``` + +**JWT Claim:** `jwt.claims.user_agent` + +### jwt_private.current_database_id() +Extracts the database ID from JWT claims (private function). + +**Returns:** `uuid` - The database ID, or NULL if not set + +**Usage:** +```sql +SELECT jwt_private.current_database_id(); +``` + +**JWT Claim:** `jwt.claims.database_id` + +## Usage + +### Setting JWT Claims + +JWT claims are set as PostgreSQL session variables, typically by your authentication middleware: + +```sql +-- Set user ID claim +SELECT set_config('jwt.claims.user_id', 'user-uuid-here', false); + +-- Set IP address claim +SELECT set_config('jwt.claims.ip_address', '192.168.1.1', false); + +-- Set user agent claim +SELECT set_config('jwt.claims.user_agent', 'Mozilla/5.0...', false); + +-- Set database ID claim +SELECT set_config('jwt.claims.database_id', 'database-uuid-here', false); +``` + +### Using Claims in Row-Level Security + +```sql +-- Enable RLS on a table +ALTER TABLE posts ENABLE ROW LEVEL SECURITY; + +-- Users can only see their own posts +CREATE POLICY user_posts ON posts + FOR ALL + TO authenticated + USING (user_id = jwt_public.current_user_id()); + +``` + +### Using Claims in Functions + +```sql +-- Function that uses current user ID +CREATE FUNCTION create_post(title text, content text) +RETURNS uuid AS $$ +DECLARE + new_post_id uuid; +BEGIN + INSERT INTO posts (user_id, title, content) + VALUES (jwt_public.current_user_id(), title, content) + RETURNING id INTO new_post_id; + + RETURN new_post_id; +END; +$$ LANGUAGE plpgsql; + +``` + +### Audit Logging with JWT Claims + +```sql +-- Audit log table +CREATE TABLE audit_log ( + id serial PRIMARY KEY, + user_id uuid, + ip_address text, + user_agent text, + action text, + timestamp timestamptz DEFAULT now() +); + +-- Trigger function for audit logging +CREATE FUNCTION log_action() +RETURNS trigger AS $$ +BEGIN + INSERT INTO audit_log (user_id, ip_address, user_agent, action) + VALUES ( + jwt_public.current_user_id(), + jwt_public.current_ip_address(), + jwt_public.current_user_agent(), + TG_OP || ' on ' || TG_TABLE_NAME + ); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Add trigger to table +CREATE TRIGGER audit_posts +AFTER INSERT OR UPDATE OR DELETE ON posts +FOR EACH ROW +EXECUTE FUNCTION log_action(); +``` + +### Multi-Tenancy with Database ID + +```sql +-- Filter data by database ID +CREATE FUNCTION get_tenant_data() +RETURNS SETOF my_table AS $$ +BEGIN + RETURN QUERY + SELECT * FROM my_table + WHERE database_id = jwt_private.current_database_id(); +END; +$$ LANGUAGE plpgsql; +``` + +## Integration with Other Packages + +### With @pgpm/stamps + +The stamps package uses JWT claims for automatic user tracking: + +```sql +-- Stamps automatically uses jwt_public.current_user_id() +-- for created_by and updated_by columns +``` + +### With @pgpm/achievements + +The achievements package uses JWT claims for user context: + +```sql +-- Check current user's achievements +SELECT * FROM status_public.steps_required('newbie'); +-- Uses jwt_public.current_user_id() internally +``` + +### With PGPM roles + +Ensure the standard roles exist (pgpm admin-users bootstrap), then combine JWT claims with role-based access: + +```sql +-- Set role based on JWT claim +CREATE FUNCTION set_user_role() +RETURNS void AS $$ +DECLARE + user_role text; +BEGIN + user_role := current_setting('jwt.claims.role', true); + + IF user_role = 'admin' THEN + SET LOCAL ROLE administrator; + ELSIF user_role = 'user' THEN + SET LOCAL ROLE authenticated; + ELSE + SET LOCAL ROLE anonymous; + END IF; +END; +$$ LANGUAGE plpgsql; +``` + +## Error Handling + +All functions include error handling for invalid claim values: + +```sql +-- If jwt.claims.user_id is not a valid UUID +SELECT jwt_public.current_user_id(); +-- Returns NULL and raises NOTICE: 'Invalid UUID value' +``` + +## Security Considerations + +1. **Trust the Source**: Only set JWT claims from trusted authentication middleware +2. **Validate Claims**: Always validate JWT signatures before setting claims +3. **Session Scope**: Claims are session-scoped and don't persist across connections +4. **No Direct Access**: Users cannot directly modify session variables in most configurations +5. **Use HTTPS**: Always transmit JWTs over HTTPS to prevent interception + +## Dependencies + +- `@pgpm/types`: Core PostgreSQL types +- `@pgpm/verify`: Verification utilities + +## Testing + +```bash +pnpm test +``` + +## Related Tooling + +* [pgpm](https://github.com/constructive-io/constructive/tree/main/packages/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages. +* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/packages/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation. +* [supabase-test](https://github.com/constructive-io/constructive/tree/main/packages/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready. +* [graphile-test](https://github.com/constructive-io/constructive/tree/main/packages/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts. +* [pgsql-parser](https://github.com/constructive-io/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax. +* [libpg-query-node](https://github.com/constructive-io/libpg-query-node): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees. +* [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. + +## Disclaimer + +AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND. + +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. diff --git a/extensions/@pgpm/jwt-claims/deploy/schemas/ctx/procedures/ip_address.sql b/extensions/@pgpm/jwt-claims/deploy/schemas/ctx/procedures/ip_address.sql new file mode 100644 index 00000000..864bb146 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/deploy/schemas/ctx/procedures/ip_address.sql @@ -0,0 +1,18 @@ +-- Deploy schemas/ctx/procedures/ip_address to pg +-- Retrieves the client's IP address from JWT claims + +-- requires: schemas/ctx/schema + +BEGIN; + +-- Returns the client's IP address from the JWT claims +-- Useful for logging, rate limiting, and geo-based features +CREATE FUNCTION ctx.ip_address() + RETURNS inet +AS $$ + SELECT nullif(current_setting('jwt.claims.ip_address', true), '')::inet; +$$ +LANGUAGE 'sql' STABLE; + +COMMIT; + diff --git a/extensions/@pgpm/jwt-claims/deploy/schemas/ctx/procedures/origin.sql b/extensions/@pgpm/jwt-claims/deploy/schemas/ctx/procedures/origin.sql new file mode 100644 index 00000000..5c23772f --- /dev/null +++ b/extensions/@pgpm/jwt-claims/deploy/schemas/ctx/procedures/origin.sql @@ -0,0 +1,18 @@ +-- Deploy schemas/ctx/procedures/origin to pg +-- Retrieves the request origin from JWT claims + +-- requires: schemas/ctx/schema + +BEGIN; + +-- Returns the request origin from the JWT claims +-- Used for CORS validation and origin-based access control +CREATE FUNCTION ctx.origin() + RETURNS origin +AS $$ + SELECT nullif(current_setting('jwt.claims.origin', true), '')::origin; +$$ +LANGUAGE 'sql' STABLE; + +COMMIT; + diff --git a/extensions/@pgpm/jwt-claims/deploy/schemas/ctx/procedures/security_definer.sql b/extensions/@pgpm/jwt-claims/deploy/schemas/ctx/procedures/security_definer.sql new file mode 100644 index 00000000..03808209 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/deploy/schemas/ctx/procedures/security_definer.sql @@ -0,0 +1,29 @@ +-- Deploy schemas/ctx/procedures/security_definer to pg +-- Creates functions for security definer context checks + +-- requires: schemas/ctx/schema + +BEGIN; + +-- Creates two helper functions for security definer context: +-- ctx.security_definer() - Returns the name of the security definer user +-- ctx.is_security_definer() - Returns true if current user is the security definer +-- These are useful for RLS policies that need to bypass checks for system operations +DO $LQLMIGRATION$ + DECLARE + BEGIN + EXECUTE format('CREATE FUNCTION ctx.security_definer() returns text as $FUNC$ + SELECT ''%s''; +$FUNC$ +LANGUAGE ''sql'';', current_user); + EXECUTE format('CREATE FUNCTION ctx.is_security_definer() returns bool as $FUNC$ + SELECT ''%s'' = current_user; +$FUNC$ +LANGUAGE ''sql'';', current_user); + END; +$LQLMIGRATION$; +GRANT EXECUTE ON FUNCTION ctx.security_definer() TO PUBLIC; +GRANT EXECUTE ON FUNCTION ctx.is_security_definer() TO PUBLIC; + +COMMIT; + diff --git a/extensions/@pgpm/jwt-claims/deploy/schemas/ctx/procedures/uagent.sql b/extensions/@pgpm/jwt-claims/deploy/schemas/ctx/procedures/uagent.sql new file mode 100644 index 00000000..cf4423f5 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/deploy/schemas/ctx/procedures/uagent.sql @@ -0,0 +1,18 @@ +-- Deploy schemas/ctx/procedures/uagent to pg +-- Retrieves the current user's agent string from JWT claims + +-- requires: schemas/ctx/schema + +BEGIN; + +-- Returns the current user agent string from the JWT claims +-- This is a shorthand for jwt_public.current_user_agent() +CREATE FUNCTION ctx.uagent() + RETURNS text +AS $$ + SELECT nullif(current_setting('jwt.claims.user_agent', true), ''); +$$ +LANGUAGE 'sql' STABLE; + +COMMIT; + diff --git a/extensions/@pgpm/jwt-claims/deploy/schemas/ctx/procedures/uid.sql b/extensions/@pgpm/jwt-claims/deploy/schemas/ctx/procedures/uid.sql new file mode 100644 index 00000000..ed2bca9a --- /dev/null +++ b/extensions/@pgpm/jwt-claims/deploy/schemas/ctx/procedures/uid.sql @@ -0,0 +1,18 @@ +-- Deploy schemas/ctx/procedures/uid to pg +-- Retrieves the current user's ID from JWT claims + +-- requires: schemas/ctx/schema + +BEGIN; + +-- Returns the current user's UUID from the JWT claims +-- This is a shorthand for jwt_public.current_user_id() +CREATE FUNCTION ctx.uid() + RETURNS uuid +AS $$ + SELECT nullif(current_setting('jwt.claims.user_id', true), '')::uuid; +$$ +LANGUAGE 'sql' STABLE; + +COMMIT; + diff --git a/extensions/@pgpm/jwt-claims/deploy/schemas/ctx/schema.sql b/extensions/@pgpm/jwt-claims/deploy/schemas/ctx/schema.sql new file mode 100644 index 00000000..c2446b83 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/deploy/schemas/ctx/schema.sql @@ -0,0 +1,16 @@ +-- Deploy schemas/ctx/schema to pg + + +BEGIN; + +CREATE SCHEMA ctx; + +GRANT USAGE ON SCHEMA ctx +TO authenticated, anonymous; + +ALTER DEFAULT PRIVILEGES IN SCHEMA ctx +GRANT EXECUTE ON FUNCTIONS +TO authenticated; + +COMMIT; + diff --git a/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_private/procedures/current_database_id.sql b/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_private/procedures/current_database_id.sql new file mode 100644 index 00000000..3bd2dad9 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_private/procedures/current_database_id.sql @@ -0,0 +1,34 @@ +-- Deploy schemas/jwt_private/procedures/current_database_id to pg +-- Retrieves the current database ID from JWT claims (private/internal use) + +-- requires: schemas/jwt_private/schema + +BEGIN; + +-- Returns the current database UUID from the JWT claims +-- Used for multi-tenant database isolation +-- Returns NULL if the claim is not set or invalid +CREATE FUNCTION jwt_private.current_database_id() + RETURNS uuid +AS $$ +DECLARE + v_identifier_id uuid; +BEGIN + IF current_setting('jwt.claims.database_id', TRUE) + IS NOT NULL THEN + BEGIN + v_identifier_id = current_setting('jwt.claims.database_id', TRUE)::uuid; + EXCEPTION + WHEN OTHERS THEN + RAISE NOTICE 'Invalid UUID value'; + RETURN NULL; + END; + RETURN v_identifier_id; + ELSE + RETURN NULL; + END IF; +END; +$$ +LANGUAGE 'plpgsql' STABLE; + +COMMIT; diff --git a/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_private/procedures/current_session_id.sql b/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_private/procedures/current_session_id.sql new file mode 100644 index 00000000..ccf41ae0 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_private/procedures/current_session_id.sql @@ -0,0 +1,18 @@ +-- Deploy schemas/jwt_private/procedures/current_session_id to pg +-- Retrieves the current session ID from JWT claims (private/internal use) + +-- requires: schemas/jwt_private/schema + +BEGIN; + +-- Returns the current session UUID from the JWT claims +-- Used for session tracking, revocation, and audit logging +-- This is kept private to prevent session IDs from being exposed to the frontend +CREATE FUNCTION jwt_private.current_session_id() + RETURNS uuid +AS $$ + SELECT nullif(current_setting('jwt.claims.session_id', true), '')::uuid; +$$ +LANGUAGE 'sql' STABLE; + +COMMIT; diff --git a/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_private/procedures/current_token_id.sql b/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_private/procedures/current_token_id.sql new file mode 100644 index 00000000..67d0cba9 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_private/procedures/current_token_id.sql @@ -0,0 +1,18 @@ +-- Deploy schemas/jwt_private/procedures/current_token_id to pg +-- Retrieves the current JWT token ID from claims (private/internal use) + +-- requires: schemas/jwt_private/schema + +BEGIN; + +-- Returns the current JWT token UUID from the claims +-- Used for token tracking, revocation, and audit logging +CREATE FUNCTION jwt_private.current_token_id() + RETURNS uuid +AS $$ + SELECT nullif(current_setting('jwt.claims.token_id', true), '')::uuid; +$$ +LANGUAGE 'sql' STABLE; + +COMMIT; + diff --git a/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_private/schema.sql b/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_private/schema.sql new file mode 100644 index 00000000..d5d3c4a1 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_private/schema.sql @@ -0,0 +1,15 @@ +-- Deploy schemas/jwt_private/schema to pg + + +BEGIN; + +CREATE SCHEMA jwt_private; + +GRANT USAGE ON SCHEMA jwt_private +TO authenticated, anonymous; + +ALTER DEFAULT PRIVILEGES IN SCHEMA jwt_private +GRANT EXECUTE ON FUNCTIONS +TO authenticated; + +COMMIT; diff --git a/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_public/procedures/current_ip_address.sql b/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_public/procedures/current_ip_address.sql new file mode 100644 index 00000000..1c2b9d24 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_public/procedures/current_ip_address.sql @@ -0,0 +1,34 @@ +-- Deploy schemas/jwt_public/procedures/current_ip_address to pg +-- Retrieves the client's IP address from JWT claims with validation + +-- requires: schemas/jwt_public/schema + +BEGIN; + +-- Returns the client's IP address from the JWT claims +-- Includes error handling for invalid IP address values +-- Returns NULL if the claim is not set or invalid +CREATE FUNCTION jwt_public.current_ip_address() + RETURNS inet +AS $$ +DECLARE + v_ip_addr inet; +BEGIN + IF current_setting('jwt.claims.ip_address', TRUE) + IS NOT NULL THEN + BEGIN + v_ip_addr = trim(current_setting('jwt.claims.ip_address', TRUE))::inet; + EXCEPTION + WHEN OTHERS THEN + RAISE NOTICE 'Invalid IP'; + RETURN NULL; + END; + RETURN v_ip_addr; + ELSE + RETURN NULL; + END IF; +END; +$$ +LANGUAGE 'plpgsql' STABLE; + +COMMIT; diff --git a/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_public/procedures/current_origin.sql b/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_public/procedures/current_origin.sql new file mode 100644 index 00000000..99f33cca --- /dev/null +++ b/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_public/procedures/current_origin.sql @@ -0,0 +1,18 @@ +-- Deploy schemas/jwt_public/procedures/current_origin to pg +-- Retrieves the request origin from JWT claims + +-- requires: schemas/jwt_public/schema + +BEGIN; + +-- Returns the request origin from the JWT claims +-- Used for CORS validation and origin-based access control +CREATE FUNCTION jwt_public.current_origin() + RETURNS origin +AS $$ + SELECT nullif(current_setting('jwt.claims.origin', true), '')::origin; +$$ +LANGUAGE 'sql' STABLE; + +COMMIT; + diff --git a/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_public/procedures/current_user_agent.sql b/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_public/procedures/current_user_agent.sql new file mode 100644 index 00000000..85919c3d --- /dev/null +++ b/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_public/procedures/current_user_agent.sql @@ -0,0 +1,34 @@ +-- Deploy schemas/jwt_public/procedures/current_user_agent to pg +-- Retrieves the client's user agent string from JWT claims with validation + +-- requires: schemas/jwt_public/schema + +BEGIN; + +-- Returns the client's user agent string from the JWT claims +-- Includes error handling for invalid values +-- Returns NULL if the claim is not set or invalid +CREATE FUNCTION jwt_public.current_user_agent() + RETURNS text +AS $$ +DECLARE + v_uagent text; +BEGIN + IF current_setting('jwt.claims.user_agent', TRUE) + IS NOT NULL THEN + BEGIN + v_uagent = current_setting('jwt.claims.user_agent', TRUE); + EXCEPTION + WHEN OTHERS THEN + RAISE NOTICE 'Invalid UserAgent'; + RETURN NULL; + END; + RETURN v_uagent; + ELSE + RETURN NULL; + END IF; +END; +$$ +LANGUAGE 'plpgsql' STABLE; + +COMMIT; diff --git a/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_public/procedures/current_user_id.sql b/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_public/procedures/current_user_id.sql new file mode 100644 index 00000000..3754c455 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_public/procedures/current_user_id.sql @@ -0,0 +1,34 @@ +-- Deploy schemas/jwt_public/procedures/current_user_id to pg +-- Retrieves the current user's ID from JWT claims with validation + +-- requires: schemas/jwt_public/schema + +BEGIN; + +-- Returns the current user's UUID from the JWT claims +-- Includes error handling for invalid UUID values +-- Returns NULL if the claim is not set or invalid +CREATE FUNCTION jwt_public.current_user_id() + RETURNS uuid +AS $$ +DECLARE + v_identifier_id uuid; +BEGIN + IF current_setting('jwt.claims.user_id', TRUE) + IS NOT NULL THEN + BEGIN + v_identifier_id = current_setting('jwt.claims.user_id', TRUE)::uuid; + EXCEPTION + WHEN OTHERS THEN + RAISE NOTICE 'Invalid UUID value'; + RETURN NULL; + END; + RETURN v_identifier_id; + ELSE + RETURN NULL; + END IF; +END; +$$ +LANGUAGE 'plpgsql' STABLE; + +COMMIT; diff --git a/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_public/schema.sql b/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_public/schema.sql new file mode 100644 index 00000000..84caddc0 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/deploy/schemas/jwt_public/schema.sql @@ -0,0 +1,15 @@ +-- Deploy schemas/jwt_public/schema to pg + + +BEGIN; + +CREATE SCHEMA jwt_public; + +GRANT USAGE ON SCHEMA jwt_public +TO authenticated, anonymous; + +ALTER DEFAULT PRIVILEGES IN SCHEMA jwt_public +GRANT EXECUTE ON FUNCTIONS +TO authenticated; + +COMMIT; diff --git a/extensions/@pgpm/jwt-claims/package.json b/extensions/@pgpm/jwt-claims/package.json new file mode 100644 index 00000000..0ed31be4 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/package.json @@ -0,0 +1,39 @@ +{ + "name": "@pgpm/jwt-claims", + "version": "0.26.0", + "description": "JWT claim handling and validation functions", + "author": "Dan Lynch ", + "contributors": [ + "Constructive " + ], + "keywords": [ + "postgresql", + "pgpm", + "jwt", + "authentication" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "bundle": "pgpm package", + "test": "jest", + "test:watch": "jest --watch" + }, + "devDependencies": { + "pgpm": "^4.23.2" + }, + "dependencies": { + "@pgpm/types": "0.26.0", + "@pgpm/verify": "0.26.0" + }, + "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" + }, + "gitHead": "3badf1e5e2fc71deae9e194b779599d17ab28a0d" +} diff --git a/extensions/@pgpm/jwt-claims/pgpm-jwt-claims.control b/extensions/@pgpm/jwt-claims/pgpm-jwt-claims.control new file mode 100644 index 00000000..a5260b5f --- /dev/null +++ b/extensions/@pgpm/jwt-claims/pgpm-jwt-claims.control @@ -0,0 +1,8 @@ +# pgpm-jwt-claims extension +comment = 'pgpm-jwt-claims extension' +default_version = '0.15.5' +module_pathname = '$libdir/pgpm-jwt-claims' +requires = 'plpgsql,pgpm-types,pgpm-verify' +relocatable = false +superuser = false + diff --git a/extensions/@pgpm/jwt-claims/pgpm.plan b/extensions/@pgpm/jwt-claims/pgpm.plan new file mode 100644 index 00000000..e24acdc6 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/pgpm.plan @@ -0,0 +1,19 @@ +%syntax-version=1.0.0 +%project=pgpm-jwt-claims +%uri=pgpm-jwt-claims + +schemas/ctx/schema 2017-08-11T08:11:51Z skitch # add schemas/ctx/schema +schemas/ctx/procedures/ip_address [schemas/ctx/schema] 2017-08-11T08:11:51Z skitch # add schemas/ctx/procedures/ip_address +schemas/ctx/procedures/origin [schemas/ctx/schema] 2017-08-11T08:11:51Z skitch # add schemas/ctx/procedures/origin +schemas/ctx/procedures/uagent [schemas/ctx/schema] 2017-08-11T08:11:51Z skitch # add schemas/ctx/procedures/uagent +schemas/ctx/procedures/uid [schemas/ctx/schema] 2017-08-11T08:11:51Z skitch # add schemas/ctx/procedures/uid +schemas/ctx/procedures/security_definer [schemas/ctx/schema] 2021-04-20T04:04:08Z Dan Lynch # add schemas/ctx/procedures/security_definer +schemas/jwt_public/schema 2020-12-17T06:47:29Z Dan Lynch # add schemas/jwt_public/schema +schemas/jwt_public/procedures/current_user_id [schemas/jwt_public/schema] 2020-12-17T06:48:56Z Dan Lynch # add schemas/jwt_public/procedures/current_user_id +schemas/jwt_public/procedures/current_ip_address [schemas/jwt_public/schema] 2020-12-17T23:19:17Z Dan Lynch # add schemas/jwt_public/procedures/current_ip_address +schemas/jwt_public/procedures/current_user_agent [schemas/jwt_public/schema] 2020-12-17T23:20:04Z Dan Lynch # add schemas/jwt_public/procedures/current_user_agent +schemas/jwt_public/procedures/current_origin [schemas/jwt_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/jwt_public/procedures/current_origin +schemas/jwt_private/schema 2020-12-17T06:47:34Z Dan Lynch # add schemas/jwt_private/schema +schemas/jwt_private/procedures/current_database_id [schemas/jwt_private/schema] 2020-12-17T23:22:28Z Dan Lynch # add schemas/jwt_private/procedures/current_database_id +schemas/jwt_private/procedures/current_token_id [schemas/jwt_private/schema] 2017-08-11T08:11:51Z skitch # add schemas/jwt_private/procedures/current_token_id +schemas/jwt_private/procedures/current_session_id [schemas/jwt_private/schema] 2026-01-28T05:44:00Z Dan Lynch # add schemas/jwt_private/procedures/current_session_id diff --git a/extensions/@pgpm/jwt-claims/revert/schemas/ctx/procedures/ip_address.sql b/extensions/@pgpm/jwt-claims/revert/schemas/ctx/procedures/ip_address.sql new file mode 100644 index 00000000..b23230c0 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/revert/schemas/ctx/procedures/ip_address.sql @@ -0,0 +1,8 @@ +-- Revert schemas/ctx/procedures/ip_address from pg + +BEGIN; + +DROP FUNCTION ctx.ip_address; + +COMMIT; + diff --git a/extensions/@pgpm/jwt-claims/revert/schemas/ctx/procedures/origin.sql b/extensions/@pgpm/jwt-claims/revert/schemas/ctx/procedures/origin.sql new file mode 100644 index 00000000..e04aae2b --- /dev/null +++ b/extensions/@pgpm/jwt-claims/revert/schemas/ctx/procedures/origin.sql @@ -0,0 +1,8 @@ +-- Revert schemas/ctx/procedures/origin from pg + +BEGIN; + +DROP FUNCTION ctx.origin; + +COMMIT; + diff --git a/extensions/@pgpm/jwt-claims/revert/schemas/ctx/procedures/security_definer.sql b/extensions/@pgpm/jwt-claims/revert/schemas/ctx/procedures/security_definer.sql new file mode 100644 index 00000000..dbf6868d --- /dev/null +++ b/extensions/@pgpm/jwt-claims/revert/schemas/ctx/procedures/security_definer.sql @@ -0,0 +1,9 @@ +-- Revert schemas/ctx/procedures/security_definer from pg + +BEGIN; + +DROP FUNCTION ctx.security_definer; +DROP FUNCTION ctx.is_security_definer; + +COMMIT; + diff --git a/extensions/@pgpm/jwt-claims/revert/schemas/ctx/procedures/uagent.sql b/extensions/@pgpm/jwt-claims/revert/schemas/ctx/procedures/uagent.sql new file mode 100644 index 00000000..8404c504 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/revert/schemas/ctx/procedures/uagent.sql @@ -0,0 +1,8 @@ +-- Revert schemas/ctx/procedures/uagent from pg + +BEGIN; + +DROP FUNCTION ctx.uagent; + +COMMIT; + diff --git a/extensions/@pgpm/jwt-claims/revert/schemas/ctx/procedures/uid.sql b/extensions/@pgpm/jwt-claims/revert/schemas/ctx/procedures/uid.sql new file mode 100644 index 00000000..2beaa012 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/revert/schemas/ctx/procedures/uid.sql @@ -0,0 +1,8 @@ +-- Revert schemas/ctx/procedures/uid from pg + +BEGIN; + +DROP FUNCTION ctx.uid; + +COMMIT; + diff --git a/extensions/@pgpm/jwt-claims/revert/schemas/ctx/schema.sql b/extensions/@pgpm/jwt-claims/revert/schemas/ctx/schema.sql new file mode 100644 index 00000000..2244b09b --- /dev/null +++ b/extensions/@pgpm/jwt-claims/revert/schemas/ctx/schema.sql @@ -0,0 +1,8 @@ +-- Revert schemas/ctx/schema from pg + +BEGIN; + +DROP SCHEMA ctx; + +COMMIT; + diff --git a/extensions/@pgpm/jwt-claims/revert/schemas/jwt_private/procedures/current_database_id.sql b/extensions/@pgpm/jwt-claims/revert/schemas/jwt_private/procedures/current_database_id.sql new file mode 100644 index 00000000..815e6da1 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/revert/schemas/jwt_private/procedures/current_database_id.sql @@ -0,0 +1,7 @@ +-- Revert schemas/jwt_private/procedures/current_database_id from pg + +BEGIN; + +DROP FUNCTION jwt_private.current_database_id; + +COMMIT; diff --git a/extensions/@pgpm/jwt-claims/revert/schemas/jwt_private/procedures/current_session_id.sql b/extensions/@pgpm/jwt-claims/revert/schemas/jwt_private/procedures/current_session_id.sql new file mode 100644 index 00000000..fb07278e --- /dev/null +++ b/extensions/@pgpm/jwt-claims/revert/schemas/jwt_private/procedures/current_session_id.sql @@ -0,0 +1,7 @@ +-- Revert schemas/jwt_private/procedures/current_session_id from pg + +BEGIN; + +DROP FUNCTION jwt_private.current_session_id; + +COMMIT; diff --git a/extensions/@pgpm/jwt-claims/revert/schemas/jwt_private/procedures/current_token_id.sql b/extensions/@pgpm/jwt-claims/revert/schemas/jwt_private/procedures/current_token_id.sql new file mode 100644 index 00000000..f2689e67 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/revert/schemas/jwt_private/procedures/current_token_id.sql @@ -0,0 +1,8 @@ +-- Revert schemas/jwt_private/procedures/current_token_id from pg + +BEGIN; + +DROP FUNCTION jwt_private.current_token_id; + +COMMIT; + diff --git a/extensions/@pgpm/jwt-claims/revert/schemas/jwt_private/schema.sql b/extensions/@pgpm/jwt-claims/revert/schemas/jwt_private/schema.sql new file mode 100644 index 00000000..4accd2db --- /dev/null +++ b/extensions/@pgpm/jwt-claims/revert/schemas/jwt_private/schema.sql @@ -0,0 +1,7 @@ +-- Revert schemas/jwt_private/schema from pg + +BEGIN; + +DROP SCHEMA jwt_private; + +COMMIT; diff --git a/extensions/@pgpm/jwt-claims/revert/schemas/jwt_public/procedures/current_ip_address.sql b/extensions/@pgpm/jwt-claims/revert/schemas/jwt_public/procedures/current_ip_address.sql new file mode 100644 index 00000000..297da8dc --- /dev/null +++ b/extensions/@pgpm/jwt-claims/revert/schemas/jwt_public/procedures/current_ip_address.sql @@ -0,0 +1,7 @@ +-- Revert schemas/jwt_public/procedures/current_ip_address from pg + +BEGIN; + +DROP FUNCTION jwt_public.current_ip_address; + +COMMIT; diff --git a/extensions/@pgpm/jwt-claims/revert/schemas/jwt_public/procedures/current_origin.sql b/extensions/@pgpm/jwt-claims/revert/schemas/jwt_public/procedures/current_origin.sql new file mode 100644 index 00000000..98f18faa --- /dev/null +++ b/extensions/@pgpm/jwt-claims/revert/schemas/jwt_public/procedures/current_origin.sql @@ -0,0 +1,8 @@ +-- Revert schemas/jwt_public/procedures/current_origin from pg + +BEGIN; + +DROP FUNCTION jwt_public.current_origin; + +COMMIT; + diff --git a/extensions/@pgpm/jwt-claims/revert/schemas/jwt_public/procedures/current_user_agent.sql b/extensions/@pgpm/jwt-claims/revert/schemas/jwt_public/procedures/current_user_agent.sql new file mode 100644 index 00000000..864e219c --- /dev/null +++ b/extensions/@pgpm/jwt-claims/revert/schemas/jwt_public/procedures/current_user_agent.sql @@ -0,0 +1,7 @@ +-- Revert schemas/jwt_public/procedures/current_user_agent from pg + +BEGIN; + +DROP FUNCTION jwt_public.current_user_agent; + +COMMIT; diff --git a/extensions/@pgpm/jwt-claims/revert/schemas/jwt_public/procedures/current_user_id.sql b/extensions/@pgpm/jwt-claims/revert/schemas/jwt_public/procedures/current_user_id.sql new file mode 100644 index 00000000..42b48f9a --- /dev/null +++ b/extensions/@pgpm/jwt-claims/revert/schemas/jwt_public/procedures/current_user_id.sql @@ -0,0 +1,7 @@ +-- Revert schemas/jwt_public/procedures/current_user_id from pg + +BEGIN; + +DROP FUNCTION jwt_public.current_user_id; + +COMMIT; diff --git a/extensions/@pgpm/jwt-claims/revert/schemas/jwt_public/schema.sql b/extensions/@pgpm/jwt-claims/revert/schemas/jwt_public/schema.sql new file mode 100644 index 00000000..5a221559 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/revert/schemas/jwt_public/schema.sql @@ -0,0 +1,7 @@ +-- Revert schemas/jwt_public/schema from pg + +BEGIN; + +DROP SCHEMA jwt_public; + +COMMIT; diff --git a/extensions/@pgpm/jwt-claims/sql/pgpm-jwt-claims--0.15.5.sql b/extensions/@pgpm/jwt-claims/sql/pgpm-jwt-claims--0.15.5.sql new file mode 100644 index 00000000..fd3b5d48 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/sql/pgpm-jwt-claims--0.15.5.sql @@ -0,0 +1,147 @@ +\echo Use "CREATE EXTENSION pgpm-jwt-claims" to load this file. \quit +CREATE SCHEMA ctx; + +GRANT USAGE ON SCHEMA ctx TO authenticated, anonymous; + +ALTER DEFAULT PRIVILEGES IN SCHEMA ctx + GRANT EXECUTE ON FUNCTIONS TO authenticated; + +CREATE FUNCTION ctx.ip_address() RETURNS inet AS $EOFCODE$ + SELECT nullif(current_setting('jwt.claims.ip_address', true), '')::inet; +$EOFCODE$ LANGUAGE sql STABLE; + +CREATE FUNCTION ctx.origin() RETURNS origin AS $EOFCODE$ + SELECT nullif(current_setting('jwt.claims.origin', true), '')::origin; +$EOFCODE$ LANGUAGE sql STABLE; + +CREATE FUNCTION ctx.uagent() RETURNS text AS $EOFCODE$ + SELECT nullif(current_setting('jwt.claims.user_agent', true), ''); +$EOFCODE$ LANGUAGE sql STABLE; + +CREATE FUNCTION ctx.uid() RETURNS uuid AS $EOFCODE$ + SELECT nullif(current_setting('jwt.claims.user_id', true), '')::uuid; +$EOFCODE$ LANGUAGE sql STABLE; + +DO $EOFCODE$ + DECLARE + BEGIN + EXECUTE format('CREATE FUNCTION ctx.security_definer() returns text as $FUNC$ + SELECT ''%s''; +$FUNC$ +LANGUAGE ''sql'';', current_user); + EXECUTE format('CREATE FUNCTION ctx.is_security_definer() returns bool as $FUNC$ + SELECT ''%s'' = current_user; +$FUNC$ +LANGUAGE ''sql'';', current_user); + END; +$EOFCODE$; + +GRANT EXECUTE ON FUNCTION ctx.security_definer() TO PUBLIC; + +GRANT EXECUTE ON FUNCTION ctx.is_security_definer() TO PUBLIC; + +CREATE SCHEMA jwt_public; + +GRANT USAGE ON SCHEMA jwt_public TO authenticated, anonymous; + +ALTER DEFAULT PRIVILEGES IN SCHEMA jwt_public + GRANT EXECUTE ON FUNCTIONS TO authenticated; + +CREATE FUNCTION jwt_public.current_user_id() RETURNS uuid AS $EOFCODE$ +DECLARE + v_identifier_id uuid; +BEGIN + IF current_setting('jwt.claims.user_id', TRUE) + IS NOT NULL THEN + BEGIN + v_identifier_id = current_setting('jwt.claims.user_id', TRUE)::uuid; + EXCEPTION + WHEN OTHERS THEN + RAISE NOTICE 'Invalid UUID value'; + RETURN NULL; + END; + RETURN v_identifier_id; + ELSE + RETURN NULL; + END IF; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION jwt_public.current_ip_address() RETURNS inet AS $EOFCODE$ +DECLARE + v_ip_addr inet; +BEGIN + IF current_setting('jwt.claims.ip_address', TRUE) + IS NOT NULL THEN + BEGIN + v_ip_addr = trim(current_setting('jwt.claims.ip_address', TRUE))::inet; + EXCEPTION + WHEN OTHERS THEN + RAISE NOTICE 'Invalid IP'; + RETURN NULL; + END; + RETURN v_ip_addr; + ELSE + RETURN NULL; + END IF; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION jwt_public.current_user_agent() RETURNS text AS $EOFCODE$ +DECLARE + v_uagent text; +BEGIN + IF current_setting('jwt.claims.user_agent', TRUE) + IS NOT NULL THEN + BEGIN + v_uagent = current_setting('jwt.claims.user_agent', TRUE); + EXCEPTION + WHEN OTHERS THEN + RAISE NOTICE 'Invalid UserAgent'; + RETURN NULL; + END; + RETURN v_uagent; + ELSE + RETURN NULL; + END IF; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION jwt_public.current_origin() RETURNS origin AS $EOFCODE$ + SELECT nullif(current_setting('jwt.claims.origin', true), '')::origin; +$EOFCODE$ LANGUAGE sql STABLE; + +CREATE SCHEMA jwt_private; + +GRANT USAGE ON SCHEMA jwt_private TO authenticated, anonymous; + +ALTER DEFAULT PRIVILEGES IN SCHEMA jwt_private + GRANT EXECUTE ON FUNCTIONS TO authenticated; + +CREATE FUNCTION jwt_private.current_database_id() RETURNS uuid AS $EOFCODE$ +DECLARE + v_identifier_id uuid; +BEGIN + IF current_setting('jwt.claims.database_id', TRUE) + IS NOT NULL THEN + BEGIN + v_identifier_id = current_setting('jwt.claims.database_id', TRUE)::uuid; + EXCEPTION + WHEN OTHERS THEN + RAISE NOTICE 'Invalid UUID value'; + RETURN NULL; + END; + RETURN v_identifier_id; + ELSE + RETURN NULL; + END IF; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION jwt_private.current_token_id() RETURNS uuid AS $EOFCODE$ + SELECT nullif(current_setting('jwt.claims.token_id', true), '')::uuid; +$EOFCODE$ LANGUAGE sql STABLE; + +CREATE FUNCTION jwt_private.current_session_id() RETURNS uuid AS $EOFCODE$ + SELECT nullif(current_setting('jwt.claims.session_id', true), '')::uuid; +$EOFCODE$ LANGUAGE sql STABLE; \ No newline at end of file diff --git a/extensions/@pgpm/jwt-claims/verify/schemas/ctx/procedures/ip_address.sql b/extensions/@pgpm/jwt-claims/verify/schemas/ctx/procedures/ip_address.sql new file mode 100644 index 00000000..191af455 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/verify/schemas/ctx/procedures/ip_address.sql @@ -0,0 +1,8 @@ +-- Verify schemas/ctx/procedures/ip_address on pg + +BEGIN; + +SELECT verify_function ('ctx.ip_address'); + +ROLLBACK; + diff --git a/extensions/@pgpm/jwt-claims/verify/schemas/ctx/procedures/origin.sql b/extensions/@pgpm/jwt-claims/verify/schemas/ctx/procedures/origin.sql new file mode 100644 index 00000000..4fb286ed --- /dev/null +++ b/extensions/@pgpm/jwt-claims/verify/schemas/ctx/procedures/origin.sql @@ -0,0 +1,8 @@ +-- Verify schemas/ctx/procedures/origin on pg + +BEGIN; + +SELECT verify_function ('ctx.origin'); + +ROLLBACK; + diff --git a/extensions/@pgpm/jwt-claims/verify/schemas/ctx/procedures/security_definer.sql b/extensions/@pgpm/jwt-claims/verify/schemas/ctx/procedures/security_definer.sql new file mode 100644 index 00000000..d9aa4078 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/verify/schemas/ctx/procedures/security_definer.sql @@ -0,0 +1,9 @@ +-- Verify schemas/ctx/procedures/security_definer on pg + +BEGIN; + +SELECT verify_function ('ctx.security_definer'); +SELECT verify_function ('ctx.is_security_definer'); + +ROLLBACK; + diff --git a/extensions/@pgpm/jwt-claims/verify/schemas/ctx/procedures/uagent.sql b/extensions/@pgpm/jwt-claims/verify/schemas/ctx/procedures/uagent.sql new file mode 100644 index 00000000..917fa505 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/verify/schemas/ctx/procedures/uagent.sql @@ -0,0 +1,8 @@ +-- Verify schemas/ctx/procedures/uagent on pg + +BEGIN; + +SELECT verify_function ('ctx.uagent'); + +ROLLBACK; + diff --git a/extensions/@pgpm/jwt-claims/verify/schemas/ctx/procedures/uid.sql b/extensions/@pgpm/jwt-claims/verify/schemas/ctx/procedures/uid.sql new file mode 100644 index 00000000..82d56cde --- /dev/null +++ b/extensions/@pgpm/jwt-claims/verify/schemas/ctx/procedures/uid.sql @@ -0,0 +1,8 @@ +-- Verify schemas/ctx/procedures/uid on pg + +BEGIN; + +SELECT verify_function ('ctx.uid'); + +ROLLBACK; + diff --git a/extensions/@pgpm/jwt-claims/verify/schemas/ctx/schema.sql b/extensions/@pgpm/jwt-claims/verify/schemas/ctx/schema.sql new file mode 100644 index 00000000..297e87c3 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/verify/schemas/ctx/schema.sql @@ -0,0 +1,8 @@ +-- Verify schemas/ctx/schema on pg + +BEGIN; + +SELECT verify_schema ('ctx'); + +ROLLBACK; + diff --git a/extensions/@pgpm/jwt-claims/verify/schemas/jwt_private/procedures/current_database_id.sql b/extensions/@pgpm/jwt-claims/verify/schemas/jwt_private/procedures/current_database_id.sql new file mode 100644 index 00000000..33311a61 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/verify/schemas/jwt_private/procedures/current_database_id.sql @@ -0,0 +1,7 @@ +-- Verify schemas/jwt_private/procedures/current_database_id on pg + +BEGIN; + +SELECT verify_function ('jwt_private.current_database_id'); + +ROLLBACK; diff --git a/extensions/@pgpm/jwt-claims/verify/schemas/jwt_private/procedures/current_session_id.sql b/extensions/@pgpm/jwt-claims/verify/schemas/jwt_private/procedures/current_session_id.sql new file mode 100644 index 00000000..8eebd834 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/verify/schemas/jwt_private/procedures/current_session_id.sql @@ -0,0 +1,7 @@ +-- Verify schemas/jwt_private/procedures/current_session_id on pg + +BEGIN; + +SELECT verify_function ('jwt_private.current_session_id'); + +ROLLBACK; diff --git a/extensions/@pgpm/jwt-claims/verify/schemas/jwt_private/procedures/current_token_id.sql b/extensions/@pgpm/jwt-claims/verify/schemas/jwt_private/procedures/current_token_id.sql new file mode 100644 index 00000000..756b4b54 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/verify/schemas/jwt_private/procedures/current_token_id.sql @@ -0,0 +1,8 @@ +-- Verify schemas/jwt_private/procedures/current_token_id on pg + +BEGIN; + +SELECT verify_function ('jwt_private.current_token_id'); + +ROLLBACK; + diff --git a/extensions/@pgpm/jwt-claims/verify/schemas/jwt_private/schema.sql b/extensions/@pgpm/jwt-claims/verify/schemas/jwt_private/schema.sql new file mode 100644 index 00000000..8dc051d1 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/verify/schemas/jwt_private/schema.sql @@ -0,0 +1,7 @@ +-- Verify schemas/jwt_private/schema on pg + +BEGIN; + +SELECT verify_schema ('jwt_private'); + +ROLLBACK; diff --git a/extensions/@pgpm/jwt-claims/verify/schemas/jwt_public/procedures/current_ip_address.sql b/extensions/@pgpm/jwt-claims/verify/schemas/jwt_public/procedures/current_ip_address.sql new file mode 100644 index 00000000..bce457e2 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/verify/schemas/jwt_public/procedures/current_ip_address.sql @@ -0,0 +1,7 @@ +-- Verify schemas/jwt_public/procedures/current_ip_address on pg + +BEGIN; + +SELECT verify_function ('jwt_public.current_ip_address'); + +ROLLBACK; diff --git a/extensions/@pgpm/jwt-claims/verify/schemas/jwt_public/procedures/current_origin.sql b/extensions/@pgpm/jwt-claims/verify/schemas/jwt_public/procedures/current_origin.sql new file mode 100644 index 00000000..d1605a9a --- /dev/null +++ b/extensions/@pgpm/jwt-claims/verify/schemas/jwt_public/procedures/current_origin.sql @@ -0,0 +1,8 @@ +-- Verify schemas/jwt_public/procedures/current_origin on pg + +BEGIN; + +SELECT verify_function ('jwt_public.current_origin'); + +ROLLBACK; + diff --git a/extensions/@pgpm/jwt-claims/verify/schemas/jwt_public/procedures/current_user_agent.sql b/extensions/@pgpm/jwt-claims/verify/schemas/jwt_public/procedures/current_user_agent.sql new file mode 100644 index 00000000..e807ae30 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/verify/schemas/jwt_public/procedures/current_user_agent.sql @@ -0,0 +1,7 @@ +-- Verify schemas/jwt_public/procedures/current_user_agent on pg + +BEGIN; + +SELECT verify_function ('jwt_public.current_user_agent'); + +ROLLBACK; diff --git a/extensions/@pgpm/jwt-claims/verify/schemas/jwt_public/procedures/current_user_id.sql b/extensions/@pgpm/jwt-claims/verify/schemas/jwt_public/procedures/current_user_id.sql new file mode 100644 index 00000000..82a757bf --- /dev/null +++ b/extensions/@pgpm/jwt-claims/verify/schemas/jwt_public/procedures/current_user_id.sql @@ -0,0 +1,7 @@ +-- Verify schemas/jwt_public/procedures/current_user_id on pg + +BEGIN; + +SELECT verify_function ('jwt_public.current_user_id'); + +ROLLBACK; diff --git a/extensions/@pgpm/jwt-claims/verify/schemas/jwt_public/schema.sql b/extensions/@pgpm/jwt-claims/verify/schemas/jwt_public/schema.sql new file mode 100644 index 00000000..1a9c5105 --- /dev/null +++ b/extensions/@pgpm/jwt-claims/verify/schemas/jwt_public/schema.sql @@ -0,0 +1,7 @@ +-- Verify schemas/jwt_public/schema on pg + +BEGIN; + +SELECT verify_schema ('jwt_public'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/LICENSE b/extensions/@pgpm/metaschema-modules/LICENSE new file mode 100644 index 00000000..7b18c918 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2025 Dan Lynch +Copyright (c) 2025 Constructive + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/@pgpm/metaschema-modules/Makefile b/extensions/@pgpm/metaschema-modules/Makefile new file mode 100644 index 00000000..f8bc992d --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/Makefile @@ -0,0 +1,6 @@ +EXTENSION = metaschema-modules +DATA = sql/metaschema-modules--0.26.3.sql + +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) diff --git a/extensions/@pgpm/metaschema-modules/README.md b/extensions/@pgpm/metaschema-modules/README.md new file mode 100644 index 00000000..535f67c8 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/README.md @@ -0,0 +1,238 @@ +# @pgpm/db-meta-modules + +

+ +

+ +

+ + + + + +

+ +Module metadata handling and dependency tracking. + +## Overview + +`@pgpm/db-meta-modules` extends the `@pgpm/db-meta-schema` package with module-specific metadata tables. This package provides tables for tracking various pgpm modules including authentication, permissions, memberships, encrypted secrets, and more. It enables configuration and metadata storage for modular application features. + +## Features + +- **Module Metadata Tables**: Store configuration for various application modules +- **Authentication Modules**: Track user authentication, connected accounts, and crypto auth +- **Permission System**: Store permissions and membership configurations +- **Security Modules**: Track encrypted secrets and tokens +- **User Management**: Store user and membership module configurations +- **Field Modules**: Track custom field configurations +- **API Configuration**: Store API and RLS module settings + +## Installation + +If you have `pgpm` installed: + +```bash +pgpm install @pgpm/db-meta-modules +pgpm deploy +``` + +This is a quick way to get started. The sections below provide more detailed installation options. + +### Prerequisites + +```bash +# Install pgpm CLI +npm install -g pgpm + +# Start local Postgres (via Docker) and export env vars +pgpm docker start +eval "$(pgpm env)" +``` + +> **Tip:** Already running Postgres? Skip the Docker step and just export your `PG*` environment variables. + +### **Add to an Existing Package** + +```bash +# 1. Install the package +pgpm install @pgpm/db-meta-modules + +# 2. Deploy locally +pgpm deploy +``` + +### **Add to a New Project** + +```bash +# 1. Create a workspace +pgpm init workspace + +# 2. Create your first module +cd my-workspace +pgpm init + +# 3. Install a package +cd packages/my-module +pgpm install @pgpm/db-meta-modules + +# 4. Deploy everything +pgpm deploy --createdb --database mydb1 +``` + +## Module Tables + +The package provides metadata tables for the following modules: + +### Authentication & Users +- **users_module**: User management configuration +- **user_auth_module**: User authentication settings +- **connected_accounts_module**: Connected account configurations +- **crypto_auth_module**: Cryptocurrency authentication settings +- **crypto_addresses_module**: Crypto address management + +### Permissions & Memberships +- **permissions_module**: Permission system configuration +- **memberships_module**: Membership management settings +- **membership_types_module**: Membership type definitions +- **events_module**: User level configurations + +### Security +- **encrypted_secrets_module**: Encrypted secrets configuration +- **secrets_module**: Secret management settings +- **tokens_module**: Token management configuration + +### Communication +- **emails_module**: Email module configuration +- **phone_numbers_module**: Phone number management settings +- **invites_module**: Invitation system configuration + +### Other Modules +- **default_ids_module**: Default ID generation settings +- **limits_module**: Rate limiting and quota configurations +- **rls_module**: Row-level security configurations +- **denormalized_table_field**: Denormalized field tracking + +### Application Structure +- **apis**: API configurations +- **sites**: Site definitions + +## Usage + +### Storing Module Configuration + +```sql +-- Configure users module +INSERT INTO metaschema_modules_public.users_module ( + database_id, + api_id, + enabled, + settings +) VALUES ( + 'database-uuid', + 'api-uuid', + true, + '{"require_email_verification": true}'::jsonb +); + +-- Configure permissions module +INSERT INTO metaschema_modules_public.permissions_module ( + database_id, + api_id, + enabled, + settings +) VALUES ( + 'database-uuid', + 'api-uuid', + true, + '{"default_role": "user"}'::jsonb +); + +-- Configure encrypted secrets module +INSERT INTO metaschema_modules_public.encrypted_secrets_module ( + database_id, + api_id, + enabled, + encryption_key_id +) VALUES ( + 'database-uuid', + 'api-uuid', + true, + 'key-uuid' +); +``` + +### Querying Module Configuration + +```sql +-- Get all enabled modules for a database +SELECT + 'users' as module_name, enabled +FROM metaschema_modules_public.users_module +WHERE database_id = 'database-uuid' +UNION ALL +SELECT + 'permissions' as module_name, enabled +FROM metaschema_modules_public.permissions_module +WHERE database_id = 'database-uuid' +UNION ALL +SELECT + 'encrypted_secrets' as module_name, enabled +FROM metaschema_modules_public.encrypted_secrets_module +WHERE database_id = 'database-uuid'; + +-- Get RLS module configuration +SELECT * FROM metaschema_modules_public.rls_module +WHERE api_id = 'api-uuid'; +``` + +## Use Cases + +### Modular Application Configuration + +Store and manage configuration for optional application features: +- Enable/disable modules per database or API +- Store module-specific settings +- Track module dependencies +- Configure module behavior + +### Multi-Tenant Applications + +Manage module configurations per tenant: +- Different modules enabled per tenant +- Tenant-specific module settings +- Isolated module configurations + +### Dynamic Feature Flags + +Use module tables as feature flags: +- Enable/disable features at runtime +- A/B testing configurations +- Gradual feature rollouts + +## Dependencies + +- `@pgpm/db-meta-schema`: Core metadata management +- `@pgpm/verify`: Verification utilities + +## Testing + +```bash +pnpm test +``` + +## Related Tooling + +* [pgpm](https://github.com/constructive-io/constructive/tree/main/packages/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages. +* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/packages/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation. +* [supabase-test](https://github.com/constructive-io/constructive/tree/main/packages/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready. +* [graphile-test](https://github.com/constructive-io/constructive/tree/main/packages/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts. +* [pgsql-parser](https://github.com/constructive-io/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax. +* [libpg-query-node](https://github.com/constructive-io/libpg-query-node): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees. +* [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. + +## Disclaimer + +AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND. + +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. diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/schema.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/schema.sql new file mode 100644 index 00000000..a80c249e --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/schema.sql @@ -0,0 +1,16 @@ +-- Deploy schemas/metaschema_modules_public/schema to pg + +BEGIN; + +CREATE SCHEMA metaschema_modules_public; + +GRANT USAGE ON SCHEMA metaschema_modules_public TO authenticated; +GRANT USAGE ON SCHEMA metaschema_modules_public TO administrator; +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_modules_public GRANT ALL ON TABLES TO authenticated; +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_modules_public GRANT ALL ON SEQUENCES TO authenticated; +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_modules_public GRANT ALL ON FUNCTIONS TO authenticated; +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_modules_public GRANT ALL ON TABLES TO administrator; +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_modules_public GRANT ALL ON SEQUENCES TO administrator; +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_modules_public GRANT ALL ON FUNCTIONS TO administrator; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/agent_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/agent_module/table.sql new file mode 100644 index 00000000..777deca2 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/agent_module/table.sql @@ -0,0 +1,94 @@ +-- Deploy schemas/metaschema_modules_public/tables/agent_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.agent_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- Schema references (if uuid_nil, resolved from schema name or default) + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Generated table IDs (populated by the generator) + thread_table_id uuid NOT NULL DEFAULT uuid_nil(), + message_table_id uuid NOT NULL DEFAULT uuid_nil(), + task_table_id uuid NOT NULL DEFAULT uuid_nil(), + prompts_table_id uuid NOT NULL DEFAULT uuid_nil(), + plan_table_id uuid DEFAULT NULL, + agent_table_id uuid DEFAULT NULL, + persona_table_id uuid DEFAULT NULL, + resource_table_id uuid DEFAULT NULL, + + -- Table names (input to the generator) + thread_table_name text NOT NULL DEFAULT 'agent_thread', + message_table_name text NOT NULL DEFAULT 'agent_message', + task_table_name text NOT NULL DEFAULT 'agent_task', + prompts_table_name text NOT NULL DEFAULT 'agent_prompt', + plan_table_name text NOT NULL DEFAULT 'agent_plan', + agent_table_name text NOT NULL DEFAULT 'agent', + persona_table_name text NOT NULL DEFAULT 'agent_persona', + resource_table_name text NOT NULL DEFAULT 'agent_resource', + + -- Feature flags + has_plans boolean NOT NULL DEFAULT false, + has_resources boolean NOT NULL DEFAULT false, + has_agents boolean NOT NULL DEFAULT false, + shared boolean NOT NULL DEFAULT false, + + -- API routing (configurable per-module) + api_name text DEFAULT 'agent', + private_api_name text DEFAULT NULL, + + -- Scope: determines the security level for this module instance. + -- Resolved to a membership_type integer at trigger time via membership_types table. + scope text NOT NULL DEFAULT 'app', + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + -- Override to create multiple module instances at the same scope. + prefix text NOT NULL DEFAULT '', + + -- Entity table for RLS (NULL for app-level, entity table for entity-scoped) + entity_table_id uuid NULL, + + -- Configurable security policies (NULL = use defaults based on scope) + policies jsonb NULL, + + -- Resource configuration array (dimensions, chunk_size, chunk_strategy, etc.) + -- NULL = use sensible defaults (768d, 1000 chunk_size, paragraph strategy) + -- Example: [{"dimensions": 1536, "chunk_size": 500, "chunk_strategy": "sentence"}] + resources jsonb NULL, + + -- Per-table provisions overrides from blueprint config. + -- Keys are table keys (thread, message, task, prompt, knowledge). + -- When a key is present, the module trigger skips default security for that table; + -- secure_table_provision applies the custom grants/policies instead. + provisions jsonb NULL, + + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + + -- Constraints + CONSTRAINT agent_module_db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT agent_module_schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT agent_module_private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT agent_module_thread_table_fkey FOREIGN KEY (thread_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT agent_module_message_table_fkey FOREIGN KEY (message_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT agent_module_task_table_fkey FOREIGN KEY (task_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT agent_module_prompts_table_fkey FOREIGN KEY (prompts_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT agent_module_plan_table_fkey FOREIGN KEY (plan_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT agent_module_agent_table_fkey FOREIGN KEY (agent_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT agent_module_persona_table_fkey FOREIGN KEY (persona_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT agent_module_resource_table_fkey FOREIGN KEY (resource_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT agent_module_entity_table_fkey FOREIGN KEY (entity_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX agent_module_database_id_idx ON metaschema_modules_public.agent_module ( database_id ); + +-- Unique constraint: one agent module per database per scope per prefix. +CREATE UNIQUE INDEX agent_module_unique_scope ON metaschema_modules_public.agent_module ( database_id, scope, prefix ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/billing_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/billing_module/table.sql new file mode 100644 index 00000000..4d2aa0b5 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/billing_module/table.sql @@ -0,0 +1,70 @@ +-- Deploy schemas/metaschema_modules_public/tables/billing_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.billing_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Meters table: defines what you track (quota, boolean, credit_pool) + meters_table_id uuid NOT NULL DEFAULT uuid_nil(), + meters_table_name text NOT NULL DEFAULT '', + + -- Plan subscriptions table: assigns plans to entities with lifecycle + plan_subscriptions_table_id uuid NOT NULL DEFAULT uuid_nil(), + plan_subscriptions_table_name text NOT NULL DEFAULT '', + + -- Ledger table: append-only event log + ledger_table_id uuid NOT NULL DEFAULT uuid_nil(), + ledger_table_name text NOT NULL DEFAULT '', + + -- Balances SPRT: denormalized current state (RLS-exempt fast lookups) + balances_table_id uuid NOT NULL DEFAULT uuid_nil(), + balances_table_name text NOT NULL DEFAULT '', + + -- Meter credits table: append-only credit grants for billing meters + meter_credits_table_id uuid NOT NULL DEFAULT uuid_nil(), + meter_credits_table_name text NOT NULL DEFAULT '', + + -- Meter sources table: maps billing meters to typed daily summary table columns + meter_sources_table_id uuid NOT NULL DEFAULT uuid_nil(), + meter_sources_table_name text NOT NULL DEFAULT '', + + -- Meter defaults table: app-scope default meter catalog seeded at provision time + meter_defaults_table_id uuid NOT NULL DEFAULT uuid_nil(), + meter_defaults_table_name text NOT NULL DEFAULT '', + + -- Generated functions + record_usage_function text NOT NULL DEFAULT '', + + prefix text NULL, + + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + + -- API routing (configurable per-module) + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT meters_table_fkey FOREIGN KEY (meters_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT plan_subscriptions_table_fkey FOREIGN KEY (plan_subscriptions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT ledger_table_fkey FOREIGN KEY (ledger_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT balances_table_fkey FOREIGN KEY (balances_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT meter_credits_table_fkey FOREIGN KEY (meter_credits_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT meter_sources_table_fkey FOREIGN KEY (meter_sources_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT meter_defaults_table_fkey FOREIGN KEY (meter_defaults_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT billing_module_database_id_unique UNIQUE (database_id) +); + +CREATE INDEX billing_module_database_id_idx ON metaschema_modules_public.billing_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/billing_provider_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/billing_provider_module/table.sql new file mode 100644 index 00000000..22adc393 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/billing_provider_module/table.sql @@ -0,0 +1,65 @@ +-- Deploy schemas/metaschema_modules_public/tables/billing_provider_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.billing_provider_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Provider: which billing provider (stripe, paddle, lemon_squeezy, etc.) + provider text NOT NULL DEFAULT 'stripe', + + -- Parameterized FK targets: pass in your own tables + -- For SaaS: plans, plan_pricing, plan_subscriptions + -- For e-commerce: products, product_variants, orders + products_table_id uuid NULL, + prices_table_id uuid NULL, + subscriptions_table_id uuid NULL, + + -- Created mapping tables + billing_customers_table_id uuid NOT NULL DEFAULT uuid_nil(), + billing_customers_table_name text NOT NULL DEFAULT '', + + billing_products_table_id uuid NOT NULL DEFAULT uuid_nil(), + billing_products_table_name text NOT NULL DEFAULT '', + + billing_prices_table_id uuid NOT NULL DEFAULT uuid_nil(), + billing_prices_table_name text NOT NULL DEFAULT '', + + billing_subscriptions_table_id uuid NOT NULL DEFAULT uuid_nil(), + billing_subscriptions_table_name text NOT NULL DEFAULT '', + + billing_webhook_events_table_id uuid NOT NULL DEFAULT uuid_nil(), + billing_webhook_events_table_name text NOT NULL DEFAULT '', + + -- Generated functions + process_billing_event_function text NOT NULL DEFAULT '', + + prefix text NULL, + + -- API routing (configurable per-module) + api_name text DEFAULT NULL, + private_api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT billing_customers_table_fkey FOREIGN KEY (billing_customers_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT billing_products_table_fkey FOREIGN KEY (billing_products_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT billing_prices_table_fkey FOREIGN KEY (billing_prices_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT billing_subscriptions_table_fkey FOREIGN KEY (billing_subscriptions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT billing_webhook_events_table_fkey FOREIGN KEY (billing_webhook_events_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT products_table_fkey FOREIGN KEY (products_table_id) REFERENCES metaschema_public.table (id) ON DELETE SET NULL, + CONSTRAINT prices_table_fkey FOREIGN KEY (prices_table_id) REFERENCES metaschema_public.table (id) ON DELETE SET NULL, + CONSTRAINT subscriptions_table_fkey FOREIGN KEY (subscriptions_table_id) REFERENCES metaschema_public.table (id) ON DELETE SET NULL, + CONSTRAINT billing_provider_module_database_id_unique UNIQUE (database_id) +); + +CREATE INDEX billing_provider_module_database_id_idx ON metaschema_modules_public.billing_provider_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/blueprint/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/blueprint/table.sql new file mode 100644 index 00000000..7dc5a609 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/blueprint/table.sql @@ -0,0 +1,89 @@ +-- Deploy schemas/metaschema_modules_public/tables/blueprint/table to pg + +-- requires: schemas/metaschema_modules_public/schema +-- requires: schemas/metaschema_modules_public/tables/blueprint_template/table + +BEGIN; + +CREATE TABLE metaschema_modules_public.blueprint ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + + -- Ownership + scoping + owner_id uuid NOT NULL, + + database_id uuid NOT NULL, + + -- Identity + name text NOT NULL, + + display_name text NOT NULL, + + description text, + + -- The blueprint definition (tables with nodes[] and policies[], relations with $type) + -- This is a mutable copy — the owner can customize before executing + definition jsonb NOT NULL, + + -- Lineage: where did this come from? + template_id uuid DEFAULT NULL, + + -- Content-addressable Merkle hashes (backend-computed via trigger) + definition_hash uuid, + + table_hashes jsonb, + + created_at timestamptz NOT NULL DEFAULT now(), + + updated_at timestamptz NOT NULL DEFAULT now(), + + CONSTRAINT blueprint_unique_database_name UNIQUE (database_id, name), + CONSTRAINT blueprint_db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT blueprint_template_fkey FOREIGN KEY (template_id) REFERENCES metaschema_modules_public.blueprint_template (id) +); + +COMMENT ON TABLE metaschema_modules_public.blueprint IS + 'An owned, editable blueprint scoped to a specific database. Created by copying from a blueprint_template via copy_template_to_blueprint() or built from scratch. The owner can customize the definition at any time. Execute it with construct_blueprint() which creates a separate blueprint_construction record to track the build.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.id IS + 'Unique identifier for this blueprint.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.owner_id IS + 'The user who owns this blueprint.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.database_id IS + 'The database this blueprint is scoped to. Tables created by construct_blueprint() are provisioned in this database.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.name IS + 'Machine-readable name for the blueprint. Must be unique per database.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.display_name IS + 'Human-readable display name for the blueprint.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.description IS + 'Optional description of the blueprint.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.definition IS + 'The blueprint definition as a JSONB document. Contains tables[] (each with table_name, optional schema_name, nodes[] for data behaviors, fields[], grants[], and policies[] using $type), relations[] (using $type with source_table/target_table and optional source_schema/target_schema), indexes[] (using table_name + column), and full_text_searches[] (using table_name + field + sources[]). Everything is name-based — no UUIDs in the definition.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.template_id IS + 'If this blueprint was created by copying a template, the ID of the source template. NULL if built from scratch.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.created_at IS + 'Timestamp when this blueprint was created.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.definition_hash IS + 'UUIDv5 Merkle root hash of the definition. Computed automatically via trigger from the ordered table_hashes. Used for content-addressable deduplication and provenance tracking. Backend-computed — clients should never set this directly.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.table_hashes IS + 'JSONB map of table names to their individual UUIDv5 content hashes. Each table hash is computed from the canonical jsonb::text of the table entry. Enables structural comparison at the table level across blueprints and templates. Backend-computed via trigger.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.updated_at IS + 'Timestamp when this blueprint was last modified.'; + + +CREATE INDEX blueprint_owner_id_idx ON metaschema_modules_public.blueprint (owner_id); +CREATE INDEX blueprint_database_id_idx ON metaschema_modules_public.blueprint (database_id); +CREATE INDEX blueprint_template_id_idx ON metaschema_modules_public.blueprint (template_id); +CREATE INDEX blueprint_definition_hash_idx ON metaschema_modules_public.blueprint (definition_hash); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/blueprint_construction/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/blueprint_construction/table.sql new file mode 100644 index 00000000..f28d49df --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/blueprint_construction/table.sql @@ -0,0 +1,84 @@ +-- Deploy schemas/metaschema_modules_public/tables/blueprint_construction/table to pg + +-- requires: schemas/metaschema_modules_public/schema +-- requires: schemas/metaschema_modules_public/tables/blueprint/table + +BEGIN; + +CREATE TABLE metaschema_modules_public.blueprint_construction ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + + -- What was constructed + blueprint_id uuid NOT NULL, + + database_id uuid NOT NULL, + + -- The schema used as the default for tables without an explicit schema_name + schema_id uuid, + + -- Execution state + status text NOT NULL DEFAULT 'pending' + CHECK (status IN ('pending', 'constructing', 'constructed', 'failed')), + + error_details text, + + -- Output: mapping of table names to created table IDs (populated after construct) + table_map jsonb NOT NULL DEFAULT '{}', + + -- Snapshot of the definition at construct-time (immutable record of what was actually executed) + constructed_definition jsonb, + + constructed_at timestamptz, + + created_at timestamptz NOT NULL DEFAULT now(), + + updated_at timestamptz NOT NULL DEFAULT now(), + + CONSTRAINT blueprint_construction_blueprint_fkey + FOREIGN KEY (blueprint_id) REFERENCES metaschema_modules_public.blueprint(id) ON DELETE CASCADE, + CONSTRAINT blueprint_construction_db_fkey + FOREIGN KEY (database_id) REFERENCES metaschema_public.database(id) ON DELETE CASCADE +); + +COMMENT ON TABLE metaschema_modules_public.blueprint_construction IS + 'Tracks individual construction attempts of a blueprint. Each time construct_blueprint() is called, a new record is created here. This separates the editable blueprint definition from its build history, allowing blueprints to be re-executed, constructed into multiple databases, and maintain an audit trail of all construction attempts.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.id IS + 'Unique identifier for this construction attempt.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.blueprint_id IS + 'The blueprint that was constructed.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.database_id IS + 'The database the blueprint was constructed into.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.schema_id IS + 'The default schema used for tables that did not specify an explicit schema_name. NULL if not yet resolved.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.status IS + 'Execution state of this construction attempt. pending: created but not yet started. constructing: currently executing. constructed: successfully completed. failed: execution failed (see error_details).'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.error_details IS + 'Error message from a failed construction attempt. NULL unless status is failed.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.table_map IS + 'Mapping of table names to created table UUIDs, populated after successful construction. Format: {"products": "uuid", "categories": "uuid", ...}. Defaults to empty object.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.constructed_definition IS + 'Immutable snapshot of the definition at construct-time. Preserved so the exact definition that was executed is recorded even if the user later modifies the blueprint definition.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.constructed_at IS + 'Timestamp when construction successfully completed. NULL until constructed.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.created_at IS + 'Timestamp when this construction attempt was created.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.updated_at IS + 'Timestamp when this construction attempt was last modified.'; + + +CREATE INDEX blueprint_construction_blueprint_id_idx ON metaschema_modules_public.blueprint_construction (blueprint_id); +CREATE INDEX blueprint_construction_database_id_idx ON metaschema_modules_public.blueprint_construction (database_id); +CREATE INDEX blueprint_construction_status_idx ON metaschema_modules_public.blueprint_construction (status); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/blueprint_template/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/blueprint_template/table.sql new file mode 100644 index 00000000..d634212e --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/blueprint_template/table.sql @@ -0,0 +1,137 @@ +-- Deploy schemas/metaschema_modules_public/tables/blueprint_template/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.blueprint_template ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + + -- Identity + name text NOT NULL, + + version text NOT NULL DEFAULT '1.0.0', + + display_name text NOT NULL, + + description text, + + -- Ownership + owner_id uuid NOT NULL, + + -- Visibility + visibility text NOT NULL DEFAULT 'private' + CHECK (visibility IN ('private', 'public')), + + -- Categorization + categories text[] NOT NULL DEFAULT '{}', + + tags text[] NOT NULL DEFAULT '{}', + + -- The blueprint definition (tables with nodes[] and policies[], relations with $type) + definition jsonb NOT NULL, + + -- Schema for validating definition structure + definition_schema_version text NOT NULL DEFAULT '1', + + -- Provenance + source text NOT NULL DEFAULT 'user' + CHECK (source IN ('user', 'system', 'agent')), + + -- Complexity indicator + complexity text DEFAULT NULL + CHECK (complexity IS NULL OR complexity IN ('simple', 'moderate', 'complex')), + + -- Marketplace stats (denormalized for query perf) + copy_count integer NOT NULL DEFAULT 0, + + fork_count integer NOT NULL DEFAULT 0, + + -- If this template was forked from another + forked_from_id uuid DEFAULT NULL, + + -- Content-addressable Merkle hashes (backend-computed via trigger) + definition_hash uuid, + + table_hashes jsonb, + + created_at timestamptz NOT NULL DEFAULT now(), + + updated_at timestamptz NOT NULL DEFAULT now(), + + CONSTRAINT blueprint_template_unique_owner_name_version UNIQUE (owner_id, name, version), + CONSTRAINT blueprint_template_forked_from_fkey FOREIGN KEY (forked_from_id) REFERENCES metaschema_modules_public.blueprint_template(id) +); + +COMMENT ON TABLE metaschema_modules_public.blueprint_template IS + 'A shareable, versioned schema recipe for the blueprint marketplace. Templates define arrays of secure_table_provision + relation_provision inputs that together describe a complete domain schema (e.g. e-commerce, telemedicine, habit tracker). Templates are never executed directly — they are copied into a blueprint first via copy_template_to_blueprint(). Can be private (owner-only) or public (marketplace-visible).'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.id IS + 'Unique identifier for this template.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.name IS + 'Machine-readable name for the template (e.g. e_commerce_basic). Must be unique per owner + version.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.version IS + 'Semantic version string. Defaults to 1.0.0.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.display_name IS + 'Human-readable display name for the template (e.g. E-Commerce Basic).'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.description IS + 'Optional description of what the template provisions.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.owner_id IS + 'The user who created or published this template.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.visibility IS + 'Access control for the template. private: only the owner can see and copy. public: anyone can browse and copy from the marketplace. Defaults to private.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.categories IS + 'Domain categories for marketplace browsing (e.g. e-commerce, healthcare, social). Defaults to empty array.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.tags IS + 'Freeform tags for search and discovery (e.g. products, orders, payments). Defaults to empty array.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.definition IS + 'The blueprint definition as a JSONB document. Contains tables[] (each with nodes[] for data behaviors via string shorthand or {"$type": "...", "data": {...}} objects, fields[], grants[], and policies[] using {"$type": "...", "data": {...}}), and relations[] (using $type for relation_type with junction config in data). This is the core payload that gets copied into a blueprint for execution.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.definition_schema_version IS + 'Version of the definition format schema. Used for forward-compatible parsing. Defaults to 1.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.source IS + 'Provenance of the template. user: manually created by a human. system: official curated template from the Constructive team. agent: AI-generated. Defaults to user.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.complexity IS + 'Complexity indicator for marketplace filtering. simple: 3-5 tables. moderate: 6-12 tables. complex: 13+ tables. NULL if not categorized.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.copy_count IS + 'Denormalized count of how many blueprints have been created from this template via copy_template_to_blueprint(). Incremented automatically. Defaults to 0.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.fork_count IS + 'Denormalized count of how many derivative templates have been forked from this template. Defaults to 0.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.forked_from_id IS + 'If this template was forked from another template, the ID of the parent. NULL for original templates.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.created_at IS + 'Timestamp when this template was created.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.definition_hash IS + 'UUIDv5 Merkle root hash of the definition. Computed automatically via trigger from the ordered table_hashes. Used for content-addressable deduplication, provenance tracking, and cross-blueprint structural comparison. NULL columns are backend-computed — clients should never set this directly.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.table_hashes IS + 'JSONB map of table ref names to their individual UUIDv5 content hashes (e.g. {"products": "uuid", "categories": "uuid"}). Each table hash is computed from the canonical jsonb::text of the table entry. Enables structural comparison at the table level across different blueprints. Backend-computed via trigger.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.updated_at IS + 'Timestamp when this template was last modified.'; + + +CREATE INDEX blueprint_template_owner_id_idx ON metaschema_modules_public.blueprint_template (owner_id); +CREATE INDEX blueprint_template_visibility_idx ON metaschema_modules_public.blueprint_template (visibility); +CREATE INDEX blueprint_template_forked_from_id_idx ON metaschema_modules_public.blueprint_template (forked_from_id); +CREATE INDEX blueprint_template_categories_idx ON metaschema_modules_public.blueprint_template USING gin (categories); +CREATE INDEX blueprint_template_tags_idx ON metaschema_modules_public.blueprint_template USING gin (tags); +CREATE INDEX blueprint_template_definition_hash_idx ON metaschema_modules_public.blueprint_template (definition_hash); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/compute_log_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/compute_log_module/table.sql new file mode 100644 index 00000000..cda421e2 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/compute_log_module/table.sql @@ -0,0 +1,49 @@ +-- Deploy schemas/metaschema_modules_public/tables/compute_log_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.compute_log_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Compute log table (partitioned by completed_at) + compute_log_table_id uuid NOT NULL DEFAULT uuid_nil(), + compute_log_table_name text NOT NULL DEFAULT '', + + -- Pre-aggregated daily rollup table + usage_daily_table_id uuid NOT NULL DEFAULT uuid_nil(), + usage_daily_table_name text NOT NULL DEFAULT '', + + -- Partition lifecycle configuration + "interval" text NOT NULL DEFAULT '1 month', + retention text NOT NULL DEFAULT '12 months', + premake int NOT NULL DEFAULT 2, + + -- Scope configuration: 'app' = per-app usage (actor_id RLS) + scope text NOT NULL DEFAULT 'app', + actor_fk_table_id uuid NULL, + entity_fk_table_id uuid NULL, + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- API routing (configurable per-module) + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT compute_log_table_fkey FOREIGN KEY (compute_log_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT usage_daily_table_fkey FOREIGN KEY (usage_daily_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT compute_log_module_database_id_prefix_unique UNIQUE NULLS NOT DISTINCT (database_id, prefix) +); + +CREATE INDEX compute_log_module_database_id_idx ON metaschema_modules_public.compute_log_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_module/table.sql new file mode 100644 index 00000000..9d320395 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_module/table.sql @@ -0,0 +1,82 @@ +-- Deploy schemas/metaschema_modules_public/tables/config_secrets_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.config_secrets_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- Schema references (resolved by BEFORE INSERT trigger when uuid_nil) + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Generated table IDs (populated by the generator) + table_id uuid NOT NULL DEFAULT uuid_nil(), + config_definitions_table_id uuid NULL DEFAULT NULL, + + -- Table name (input — bare name without scope prefix). + -- The trigger prepends the scope prefix automatically. + table_name text NOT NULL DEFAULT 'secrets', + + -- API routing (get-or-create: if set, schema is added to this API) + api_name text DEFAULT 'config', + private_api_name text DEFAULT NULL, + + -- Scope: determines the security level for this module instance. + -- Resolved to a membership_type integer at trigger time via membership_types table. + -- 'app' = app-level (AuthzAppMembership, admin-only secrets + config) + -- 'org' = org-scoped (AuthzEntityMembership, per-org secrets with manage_secrets permission) + -- custom entity type names for entity-scoped secrets + -- Note: user-scoped credentials are handled by user_credentials_module (separate module) + scope text NOT NULL DEFAULT 'app', + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + -- Override to create multiple module instances at the same scope. + prefix text NOT NULL DEFAULT '', + + -- Entity table for RLS (NULL for app-level, entity table for entity-scoped) + entity_table_id uuid NULL, + + -- Configurable security policies (NULL = use defaults based on scope). + -- When provided, replaces the default policy set in the security function. + -- Accepts a JSON array of policy objects: + -- {"$type": "AuthzEntityMembership", "privileges": ["select", "update"], "data": {...}} + policies jsonb NULL, + + -- Per-table provisions overrides from blueprint config. + -- Keys are table keys (secrets, config_definitions). + -- When a key is present, the module trigger skips default security for that table; + -- secure_table_provision applies the custom grants/policies instead. + provisions jsonb NULL, + + -- Feature flags + + -- When true, also creates a plaintext config table ({prefix}_config) and + -- a config definitions registry table ({prefix}_config_definitions). + -- Only meaningful for app-level scope (scope = 'app'). + has_config boolean NOT NULL DEFAULT false, + + -- Constraints + CONSTRAINT config_secrets_module_db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT config_secrets_module_schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT config_secrets_module_private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT config_secrets_module_table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT config_secrets_module_config_defs_table_fkey FOREIGN KEY (config_definitions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT config_secrets_module_entity_table_fkey FOREIGN KEY (entity_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX config_secrets_module_database_id_idx ON metaschema_modules_public.config_secrets_module ( database_id ); +CREATE INDEX config_secrets_module_schema_id_idx ON metaschema_modules_public.config_secrets_module ( schema_id ); +CREATE INDEX config_secrets_module_table_id_idx ON metaschema_modules_public.config_secrets_module ( table_id ); + +-- Unique constraint: one config_secrets module per database per scope per prefix. +CREATE UNIQUE INDEX config_secrets_module_unique_scope ON metaschema_modules_public.config_secrets_module ( database_id, scope, prefix ); + +COMMENT ON TABLE metaschema_modules_public.config_secrets_module IS + 'Entity-aware PGP-encrypted key-value config/secrets module. Supports app-level (admin-only) + and org-scoped (per-org secrets with manage_secrets permission) via the scope column. + User-scoped bcrypt credentials are handled by user_credentials_module.'; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql new file mode 100644 index 00000000..0a9465b3 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql @@ -0,0 +1,32 @@ +-- Deploy schemas/metaschema_modules_public/tables/config_secrets_org_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.config_secrets_org_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- + schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT 'org_secrets', + -- + + -- API routing (configurable per-module) + api_name text DEFAULT 'config', + private_api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX config_secrets_org_module_database_id_idx ON metaschema_modules_public.config_secrets_org_module ( database_id ); +CREATE INDEX config_secrets_org_module_schema_id_idx ON metaschema_modules_public.config_secrets_org_module ( schema_id ); +CREATE INDEX config_secrets_org_module_table_id_idx ON metaschema_modules_public.config_secrets_org_module ( table_id ); + +COMMENT ON TABLE metaschema_modules_public.config_secrets_org_module IS 'Config row for the config_secrets_org_module, which provisions an organization-scoped encrypted key-value secrets store with manage_secrets permission and entity-membership RLS.'; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql new file mode 100644 index 00000000..e0e6c5a2 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql @@ -0,0 +1,32 @@ +-- Deploy schemas/metaschema_modules_public/tables/config_secrets_user_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.config_secrets_user_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- + schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT 'user_secrets', + + -- Config definitions table ID (populated by the generator) + config_definitions_table_id uuid NOT NULL DEFAULT uuid_nil(), + -- + + -- API routing (configurable per-module) + api_name text DEFAULT 'config', + private_api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT config_defs_table_fkey FOREIGN KEY (config_definitions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX config_secrets_user_module_database_id_idx ON metaschema_modules_public.config_secrets_user_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/connected_accounts_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/connected_accounts_module/table.sql new file mode 100644 index 00000000..6f831d86 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/connected_accounts_module/table.sql @@ -0,0 +1,33 @@ +-- Deploy schemas/metaschema_modules_public/tables/connected_accounts_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.connected_accounts_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + table_id uuid NOT NULL DEFAULT uuid_nil(), + owner_table_id uuid NOT NULL DEFAULT uuid_nil(), + + table_name text NOT NULL, + + -- API routing (configurable per-module) + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + + -- + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT owner_table_fkey FOREIGN KEY (owner_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE +); + +CREATE INDEX connected_accounts_module_database_id_idx ON metaschema_modules_public.connected_accounts_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/crypto_addresses_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/crypto_addresses_module/table.sql new file mode 100644 index 00000000..7e3747fd --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/crypto_addresses_module/table.sql @@ -0,0 +1,34 @@ +-- Deploy schemas/metaschema_modules_public/tables/crypto_addresses_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.crypto_addresses_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + table_id uuid NOT NULL DEFAULT uuid_nil(), + owner_table_id uuid NOT NULL DEFAULT uuid_nil(), + + table_name text NOT NULL, + crypto_network text NOT NULL DEFAULT 'BTC', + + -- API routing (configurable per-module) + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + + -- + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT owner_table_fkey FOREIGN KEY (owner_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE +); + +CREATE INDEX crypto_addresses_module_database_id_idx ON metaschema_modules_public.crypto_addresses_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/crypto_auth_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/crypto_auth_module/table.sql new file mode 100644 index 00000000..639475bd --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/crypto_auth_module/table.sql @@ -0,0 +1,41 @@ +-- Deploy schemas/metaschema_modules_public/tables/crypto_auth_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.crypto_auth_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + + users_table_id uuid NOT NULL DEFAULT uuid_nil(), + -- TOKENS_REMOVAL: tokens_table_id removed - crypto auth now uses sessions_module + secrets_table_id uuid NOT NULL DEFAULT uuid_nil(), + sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), + session_credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), + addresses_table_id uuid NOT NULL DEFAULT uuid_nil(), + + user_field text NOT NULL, + + crypto_network text NOT NULL DEFAULT 'BTC', + sign_in_request_challenge text NOT NULL DEFAULT 'sign_in_request_challenge', + sign_in_record_failure text NOT NULL DEFAULT 'sign_in_record_failure', + sign_up_with_key text NOT NULL DEFAULT 'sign_up_with_key', + sign_in_with_challenge text NOT NULL DEFAULT 'sign_in_with_challenge', + + -- + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT secrets_table_fkey FOREIGN KEY (secrets_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT users_table_fkey FOREIGN KEY (users_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + -- TOKENS_REMOVAL: tokens_table_fkey removed - crypto auth now uses sessions_module + CONSTRAINT sessions_table_fkey FOREIGN KEY (sessions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT session_credentials_table_fkey FOREIGN KEY (session_credentials_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE +); + +-- TOKENS_REMOVAL: tokens_table_fkey comment removed +CREATE INDEX crypto_auth_module_database_id_idx ON metaschema_modules_public.crypto_auth_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/db_usage_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/db_usage_module/table.sql new file mode 100644 index 00000000..9a667fef --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/db_usage_module/table.sql @@ -0,0 +1,61 @@ +-- Deploy schemas/metaschema_modules_public/tables/db_usage_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.db_usage_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- DB table stats log (partitioned — per-table reads/writes/size from pg_stat_user_tables) + table_stats_log_table_id uuid NOT NULL DEFAULT uuid_nil(), + table_stats_log_table_name text NOT NULL DEFAULT '', + + -- DB table stats daily rollup + table_stats_daily_table_id uuid NOT NULL DEFAULT uuid_nil(), + table_stats_daily_table_name text NOT NULL DEFAULT '', + + -- DB query stats log (partitioned — query execution time from pg_stat_statements) + query_stats_log_table_id uuid NOT NULL DEFAULT uuid_nil(), + query_stats_log_table_name text NOT NULL DEFAULT '', + + -- DB query stats daily rollup + query_stats_daily_table_id uuid NOT NULL DEFAULT uuid_nil(), + query_stats_daily_table_name text NOT NULL DEFAULT '', + + -- Partition lifecycle configuration + "interval" text NOT NULL DEFAULT '1 month', + retention text NOT NULL DEFAULT '12 months', + premake int NOT NULL DEFAULT 2, + + -- Scope configuration: 'app' = per-app usage + scope text NOT NULL DEFAULT 'app', + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + + -- API routing (configurable per-module) + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT table_stats_log_table_fkey FOREIGN KEY (table_stats_log_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT table_stats_daily_table_fkey FOREIGN KEY (table_stats_daily_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT query_stats_log_table_fkey FOREIGN KEY (query_stats_log_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT query_stats_daily_table_fkey FOREIGN KEY (query_stats_daily_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT db_usage_module_database_id_prefix_unique UNIQUE NULLS NOT DISTINCT (database_id, prefix) +); + +CREATE INDEX db_usage_module_database_id_idx ON metaschema_modules_public.db_usage_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/default_ids_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/default_ids_module/table.sql new file mode 100644 index 00000000..15fa40a6 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/default_ids_module/table.sql @@ -0,0 +1,18 @@ +-- Deploy schemas/metaschema_modules_public/tables/default_ids_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.default_ids_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE +); + +CREATE INDEX default_ids_module_database_id_idx ON metaschema_modules_public.default_ids_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/denormalized_table_field/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/denormalized_table_field/table.sql new file mode 100644 index 00000000..e6ea588a --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/denormalized_table_field/table.sql @@ -0,0 +1,36 @@ +-- Deploy schemas/metaschema_modules_public/tables/denormalized_table_field/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.denormalized_table_field ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + table_id uuid NOT NULL, + field_id uuid NOT NULL, + + set_ids uuid[], + + ref_table_id uuid NOT NULL, + ref_field_id uuid NOT NULL, + ref_ids uuid[], + + use_updates bool NOT NULL DEFAULT TRUE, + update_defaults bool NOT NULL DEFAULT TRUE, + + func_name text NULL, + func_order int NOT NULL DEFAULT 0, + + -- + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT ref_table_fkey FOREIGN KEY (ref_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT field_fkey FOREIGN KEY (field_id) REFERENCES metaschema_public.field (id) ON DELETE CASCADE, + CONSTRAINT ref_field_fkey FOREIGN KEY (ref_field_id) REFERENCES metaschema_public.field (id) ON DELETE CASCADE +); + +CREATE INDEX denormalized_table_field_database_id_idx ON metaschema_modules_public.denormalized_table_field ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/devices_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/devices_module/table.sql new file mode 100644 index 00000000..b6c5837e --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/devices_module/table.sql @@ -0,0 +1,36 @@ +-- Deploy schemas/metaschema_modules_public/tables/devices_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.devices_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- + schema_id uuid NOT NULL DEFAULT uuid_nil(), + user_devices_table_id uuid NOT NULL DEFAULT uuid_nil(), + device_settings_table_id uuid NOT NULL DEFAULT uuid_nil(), + + user_devices_table text NOT NULL DEFAULT 'auth_user_devices', + device_settings_table text NOT NULL DEFAULT 'app_settings_device', + -- + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT user_devices_table_fkey FOREIGN KEY (user_devices_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT device_settings_table_fkey FOREIGN KEY (device_settings_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + -- + CONSTRAINT devices_module_database_id_uniq UNIQUE(database_id) +); + +CREATE INDEX devices_module_database_id_idx ON metaschema_modules_public.devices_module ( database_id ); + +COMMENT ON CONSTRAINT user_devices_table_fkey + ON metaschema_modules_public.devices_module IS E'@fieldName userDevicesTableByUserDevicesTableId'; +COMMENT ON CONSTRAINT device_settings_table_fkey + ON metaschema_modules_public.devices_module IS E'@fieldName deviceSettingsTableByDeviceSettingsTableId'; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/emails_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/emails_module/table.sql new file mode 100644 index 00000000..ee831229 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/emails_module/table.sql @@ -0,0 +1,33 @@ +-- Deploy schemas/metaschema_modules_public/tables/emails_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.emails_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + table_id uuid NOT NULL DEFAULT uuid_nil(), + owner_table_id uuid NOT NULL DEFAULT uuid_nil(), + + table_name text NOT NULL, + + -- API routing (configurable per-module) + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + + -- + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT owner_table_fkey FOREIGN KEY (owner_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE +); + +CREATE INDEX emails_module_database_id_idx ON metaschema_modules_public.emails_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/entity_type_provision/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/entity_type_provision/table.sql new file mode 100644 index 00000000..d5285446 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/entity_type_provision/table.sql @@ -0,0 +1,421 @@ +-- Deploy schemas/metaschema_modules_public/tables/entity_type_provision/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.entity_type_provision ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + + database_id uuid NOT NULL, + + -- ========================================================================= + -- Identity: what this membership type is called + -- ========================================================================= + + name text NOT NULL, + + prefix text NOT NULL, + + description text NOT NULL DEFAULT '', + + -- ========================================================================= + -- Parentage: which entity type is the parent (resolved by trigger) + -- ========================================================================= + + parent_entity text NOT NULL DEFAULT 'org', + + -- ========================================================================= + -- Entity table name override + -- ========================================================================= + + table_name text DEFAULT NULL, + + -- ========================================================================= + -- Visibility: can parent members see child entities? + -- ========================================================================= + + is_visible boolean NOT NULL DEFAULT true, + + -- ========================================================================= + -- Optional modules + -- ========================================================================= + + has_limits boolean NOT NULL DEFAULT false, + + has_profiles boolean NOT NULL DEFAULT false, + + has_levels boolean NOT NULL DEFAULT false, + + has_invites boolean NOT NULL DEFAULT false, + + has_invite_achievements boolean NOT NULL DEFAULT false, + + -- ========================================================================= + -- Storage configuration: JSON array of storage module definitions. + -- Each element provisions a separate storage module with its own tables, + -- RLS policies, and feature flags. Presence triggers provisioning + -- (same inference model as namespaces, functions, agents). + -- NULL = do not provision. '[{}]' = provision one default storage module. + -- ========================================================================= + + storage jsonb DEFAULT NULL, + + -- ========================================================================= + -- Module configuration arrays: presence triggers provisioning. + -- Each is a JSON array of module definitions (like storage). + -- NULL = do not provision. '[{}]' = provision one default instance. + -- Each element may include "key" (discriminator) and "policies" (override). + -- ========================================================================= + + namespaces jsonb DEFAULT NULL, + + functions jsonb DEFAULT NULL, + + graphs jsonb DEFAULT NULL, + + agents jsonb DEFAULT NULL, + + -- ========================================================================= + -- Escape hatch: skip default entity table RLS policies + -- ========================================================================= + + skip_entity_policies boolean NOT NULL DEFAULT false, + + -- ========================================================================= + -- Table provisioning override: single jsonb object describing the full + -- security setup to apply to the entity table, using the same vocabulary + -- as metaschema_modules_public.provision_table() and blueprint tables[] + -- entries (policies[], nodes[], fields[], grants[], use_rls). + -- + -- Semantics: + -- - NULL (default) -> apply the 5 default entity-table policies + -- (gated by is_visible / skip_entity_policies) + -- - non-NULL object -> fan table_provision.policies[] into N + -- secure_table_provision rows; the 5 defaults + -- are implicitly skipped, and is_visible is + -- a no-op on this path. + -- ========================================================================= + + table_provision jsonb DEFAULT NULL, + + -- ========================================================================= + -- Output columns (populated by the trigger, not set by callers) + -- ========================================================================= + + out_membership_type int DEFAULT NULL, + + out_entity_table_id uuid DEFAULT NULL, + + out_entity_table_name text DEFAULT NULL, + + out_installed_modules text[] DEFAULT NULL, + + out_storage_module_id uuid DEFAULT NULL, + + out_buckets_table_id uuid DEFAULT NULL, + + out_files_table_id uuid DEFAULT NULL, + + out_path_shares_table_id uuid DEFAULT NULL, + + out_invites_module_id uuid DEFAULT NULL, + + out_namespace_module_id uuid DEFAULT NULL, + + out_namespaces_table_id uuid DEFAULT NULL, + + out_namespace_events_table_id uuid DEFAULT NULL, + + out_function_module_id uuid DEFAULT NULL, + + out_definitions_table_id uuid DEFAULT NULL, + + out_invocations_table_id uuid DEFAULT NULL, + + out_execution_logs_table_id uuid DEFAULT NULL, + + out_secret_definitions_table_id uuid DEFAULT NULL, + + out_graph_module_id uuid DEFAULT NULL, + + out_graphs_table_id uuid DEFAULT NULL, + + out_agent_module_id uuid DEFAULT NULL, + + -- ========================================================================= + -- Constraints + -- ========================================================================= + + CONSTRAINT entity_type_provision_unique_prefix UNIQUE (database_id, prefix), + CONSTRAINT entity_type_provision_db_fkey FOREIGN KEY (database_id) + REFERENCES metaschema_public.database (id) ON DELETE CASCADE +); + +-- ============================================================================= +-- Table-level comment +-- ============================================================================= + +COMMENT ON TABLE metaschema_modules_public.entity_type_provision IS + 'Provisions a new membership entity type. Each INSERT creates an entity table, registers a membership type, + and installs the required modules (permissions, memberships, limits) plus optional modules (profiles, levels, invites). + Uses provision_membership_table() internally. Graceful: duplicate (database_id, prefix) pairs are silently skipped + via the unique constraint (use INSERT ... ON CONFLICT DO NOTHING). + Policy behavior: by default the five entity-table RLS policies are applied (gated by is_visible). + Set table_provision to a single jsonb object (using the same shape as provision_table() / + blueprint tables[] entries) to replace the defaults with your own; set skip_entity_policies=true + as an escape hatch to apply zero policies.'; + +-- ============================================================================= +-- Identity columns +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.id IS + 'Unique identifier for this provision row.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.database_id IS + 'The database to provision this entity type in. Required.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.name IS + 'Human-readable name for this entity type, e.g. ''Data Room'', ''Team Channel''. Required. + Stored in the entity_types registry table.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.prefix IS + 'SQL prefix used for table and module naming, e.g. ''data_room'', ''team_channel''. Required. + Drives entity table name (prefix || ''s'' by default), module labels (permissions_module:prefix), + and membership table names (prefix_memberships, prefix_members, etc.). + Must be unique per database — the (database_id, prefix) constraint ensures graceful ON CONFLICT DO NOTHING.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.description IS + 'Description of this entity type. Stored in the entity_types registry table. Defaults to empty string.'; + +-- ============================================================================= +-- Parentage +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.parent_entity IS + 'Prefix of the parent entity type. The trigger resolves this to a membership_type integer + by looking up memberships_module WHERE prefix = parent_entity. + Defaults to ''org'' (the organization-level type). For nested types, set to the parent''s prefix + (e.g. ''data_room'' for a team_channel nested under data_room). + The parent type must already be provisioned before this INSERT.'; + +-- ============================================================================= +-- Entity table name override +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.table_name IS + 'Override the entity table name. When NULL (default), the table name is derived as prefix || ''s'' + (e.g. prefix ''data_room'' produces table ''data_rooms''). + Set this when the pluralization rule doesn''t apply (e.g. prefix ''staff'' should produce ''staff'' not ''staffs'').'; + +-- ============================================================================= +-- Visibility +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.is_visible IS + 'Whether members of the parent entity can see child entities. Defaults to true. + When true: a SELECT policy allows parent members to list child entities (e.g. org members can see all data rooms). + When false: only direct members of the entity itself can see it (private entity mode). + Controls whether the parent_member SELECT policy is created on the entity table. + Only meaningful on the defaults path — ignored (no-op) when table_provision is non-NULL or + skip_entity_policies=true, since no default policies are being applied in those cases.'; + +-- ============================================================================= +-- Optional modules +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.has_limits IS + 'Whether to apply limits_module security for this type. Defaults to false. + The limits_module table structure is always created (memberships_module requires it), + but when false, no RLS policies are applied to the limits tables. + Set to true if this entity type needs configurable resource limits per membership.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.has_profiles IS + 'Whether to provision profiles_module for this type. Defaults to false. + Profiles provide named permission roles (e.g. ''Editor'', ''Viewer'') with pre-configured permission bitmasks. + When true, creates profile tables and applies profiles security.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.has_levels IS + 'Whether to provision events_module for this type. Defaults to false. + Levels provide gamification/achievement tracking for members. + When true, creates level steps, achievements, and level tables with security.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.has_invites IS + 'Whether to provision invites_module for this type. Defaults to false. + When true, the trigger inserts a row into invites_module which in turn + (via insert_invites_module BEFORE INSERT) creates {prefix}_invites and + {prefix}_claimed_invites tables plus the submit_{prefix}_invite_code() function. + Re-provisioning is idempotent: the UNIQUE (database_id, membership_type) constraint + on invites_module combined with ON CONFLICT DO NOTHING in the fan-out makes + repeated INSERTs safe.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.has_invite_achievements IS + 'Whether to auto-attach an EventTracker to the claimed_invites table for invite-based + achievements. Defaults to false. Requires has_invites=true AND has_levels=true. + When true, the trigger calls event_tracker() on the claimed_invites table with + event_name=''invite_claimed'', actor_field=''sender_id'', events=[''INSERT''], + crediting the SENDER (inviter) when someone claims their invite code. + Developers can then define achievements in the blueprint achievements[] section + that reference the ''invite_claimed'' event (e.g., "Invite 5 friends" = count: 5).'; + +-- ============================================================================= +-- Escape hatch +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.skip_entity_policies IS + 'Escape hatch: when true, apply zero RLS policies to the entity table. Defaults to false. + Use this only when you want the entity table provisioned with zero policies (e.g. because you + plan to insert secure_table_provision rows yourself later). In most cases, prefer leaving this + false and either accepting the five defaults (table_provision=NULL) or overriding them via + table_provision. + Defaults (applied when table_provision IS NULL and skip_entity_policies=false): + - SELECT (parent_member): parent entity members can see child entities (only when is_visible=true) + - SELECT (self_member): direct members of the entity can see it + - INSERT: create_entity permission on the parent entity + - UPDATE: admin_entity permission on the entity itself + - DELETE: owner of the entity can delete it'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.table_provision IS + 'Single jsonb object describing the full security setup to apply to the entity table. + Uses the same vocabulary as metaschema_modules_public.provision_table() and blueprint tables[] + entries, so an entity table is configured the same way an ordinary blueprint table is. + Defaults to NULL; when non-NULL, the five default policies are implicitly replaced by + table_provision.policies[] (is_visible becomes a no-op on this path). + Recognized keys (all optional): + - use_rls (boolean, default true) + - nodes (jsonb array of {"$type","data"} Data* module entries) + - fields (jsonb array of field objects: name,type,is_required,default,min,max,regexp,index) + - grants (jsonb array of grant objects; each with roles[] and privileges[]) + - policies (jsonb array of policy objects; each with $type, privileges, data, name, role, permissive) + The trigger forwards all setup (nodes/fields/grants/policies) as a single secure_table_provision row + against the newly created entity table. + Example — override with two SELECT policies: + table_provision := jsonb_build_object( + ''policies'', jsonb_build_array( + jsonb_build_object( + ''$type'', ''AuthzEntityMembership'', + ''privileges'', jsonb_build_array(''select''), + ''data'', jsonb_build_object(''entity_field'', ''id'', ''membership_type'', 3), + ''name'', ''self_member'' + ), + jsonb_build_object( + ''$type'', ''AuthzDirectOwner'', + ''privileges'', jsonb_build_array(''select'', ''update''), + ''data'', jsonb_build_object(''owner_field'', ''owner_id'') + ) + ) + )'; + +-- ============================================================================= +-- Output columns +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_membership_type IS + 'Output: the auto-assigned integer membership type ID. Populated by the trigger after successful provisioning. + This is the ID used in entity_types, memberships_module, and all module tables.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_entity_table_id IS + 'Output: the UUID of the created entity table. Populated by the trigger. + Use this to reference the entity table in subsequent relation_provision or secure_table_provision rows.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_entity_table_name IS + 'Output: the name of the created entity table (e.g. ''data_rooms''). Populated by the trigger.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_installed_modules IS + 'Output: array of installed module labels (e.g. ARRAY[''permissions_module:data_room'', ''memberships_module:data_room'', ''invites_module:data_room'']). + Populated by the trigger. Useful for verifying which modules were provisioned.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.storage IS + 'Optional JSON array of storage module definitions. Presence triggers provisioning + (same inference model as namespaces, functions, agents). + Each element provisions a separate storage module with its own tables + ({prefix}_{key}_buckets/files), RLS policies, and feature flags. + NULL = do not provision storage. ''[{}]'' = provision one default storage module. + Each array element recognizes (all optional): + - key (text) module discriminator, max 16 chars, lowercase snake_case. + Defaults to ''default'' (omitted from table names). + Non-default keys become infixes: {prefix}_{key}_buckets. + (storage_key accepted for backward compat) + - upload_url_expiry_seconds (integer) presigned PUT URL expiry override + - download_url_expiry_seconds (integer) presigned GET URL expiry override + - default_max_file_size (bigint) global max file size in bytes for this module + - allowed_origins (text[]) default CORS origins for all buckets in this module + - restrict_reads (boolean) require read_files permission for SELECT on files + - has_path_shares (boolean) enable virtual filesystem + path share policies + - has_versioning (boolean) enable file version chains + - has_content_hash (boolean) enable content hash for dedup + - has_custom_keys (boolean) allow client-provided S3 keys + - has_audit_log (boolean) enable file events audit table + - has_confirm_upload (boolean) enable HeadObject confirmation flow + - confirm_upload_delay (interval) delay before first confirmation attempt + - buckets (jsonb[]) array of initial bucket definitions to seed. + Each bucket: { name (required), description, is_public, allowed_mime_types, max_file_size, allowed_origins } + - provisions (jsonb object) per-table customization keyed by "files" or "buckets". + Each value: { nodes, fields, grants, use_rls, policies }. + Example (single module, backward compat): + storage := ''[{"buckets": [{"name": "documents"}]}]''::jsonb + Example (multi-module): + storage := ''[{"has_path_shares": true, "buckets": [{"name": "documents"}]}, {"key": "fn", "has_custom_keys": true, "buckets": [{"name": "functions"}]}]''::jsonb'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_storage_module_id IS + 'Output: the UUID of the storage_module row created for this entity type. Populated by the trigger when storage is non-NULL and non-empty.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_buckets_table_id IS + 'Output: the UUID of the generated buckets table (e.g. data_room_buckets). Populated by the trigger when storage is non-NULL and non-empty.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_files_table_id IS + 'Output: the UUID of the generated files table (e.g. data_room_files). Populated by the trigger when storage is non-NULL and non-empty.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_invites_module_id IS + 'Output: the UUID of the invites_module row created for this entity type. Populated by the trigger when has_invites=true. + NULL when has_invites=false, or when re-provisioning hits ON CONFLICT DO NOTHING + (i.e. the invites_module row was created in a previous run).'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.namespaces IS + 'Optional JSON array of namespace module definitions. Presence triggers provisioning. + NULL = do not provision namespaces. ''[{}]'' = provision one default namespace module. + Each element recognizes (all optional): + - key (text) module discriminator. Defaults to ''default''. + - policies (jsonb array) RLS policy overrides. NULL = apply defaults from apply_namespace_security(). + Creates {prefix}_namespaces (or {prefix}_{key}_namespaces for non-default keys) + with entity-scoped RLS (AuthzEntityMembership) and a rename proxy trigger. + Registers manage_namespaces permission bit on first provision. + Example: namespaces := ''[{}]''::jsonb'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.functions IS + 'Optional JSON array of function module definitions. Presence triggers provisioning. + NULL = do not provision functions. ''[{}]'' = provision one default function module. + Each element recognizes (all optional): + - key (text) module discriminator. Defaults to ''default''. + - policies (jsonb array) RLS policy overrides. NULL = apply defaults from apply_function_security(). + Creates {prefix}_function_definitions (or {prefix}_{key}_function_definitions for non-default keys) + with entity-scoped RLS and a job trigger dispatching function:provision tasks. + Registers manage_functions + invoke_functions permission bits on first provision. + Example: functions := ''[{}]''::jsonb'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.graphs IS + 'Optional JSON array of graph module definitions. Presence triggers provisioning. + NULL = do not provision graphs. ''[{}]'' = provision one default graph module. + Each element recognizes (all optional): + - key (text) module discriminator. Defaults to ''default''. + - policies (jsonb array) RLS policy overrides. NULL = apply defaults from apply_graph_security(). + Registers manage_graphs + execute_graphs permission bits on first provision. + Graph module requires a merkle_store_module_id dependency, so entity_type_provision + only registers permissions here. The graph module itself must be provisioned + separately with the merkle store dependency resolved. + Example: graphs := ''[{}]''::jsonb'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_namespace_module_id IS + 'Output: the UUID of the namespace_module row created (or found) for this entity type. + Populated by the trigger when namespaces is non-NULL. NULL otherwise.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_namespaces_table_id IS + 'Output: the UUID of the generated namespaces table (e.g. data_room_namespaces). + Populated by the trigger when namespaces is non-NULL. NULL otherwise.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_namespace_events_table_id IS + 'Output: the UUID of the generated namespace_events partitioned table (e.g. data_room_namespace_events). + Monthly partitioned, 12-month retention. Populated by the trigger when namespaces is non-NULL. NULL otherwise.'; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/events_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/events_module/table.sql new file mode 100644 index 00000000..d363250c --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/events_module/table.sql @@ -0,0 +1,95 @@ +-- Deploy schemas/metaschema_modules_public/tables/events_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.events_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + -- + + events_table_id uuid NOT NULL DEFAULT uuid_nil(), + events_table_name text NOT NULL DEFAULT '', + + event_aggregates_table_id uuid NOT NULL DEFAULT uuid_nil(), + event_aggregates_table_name text NOT NULL DEFAULT '', + + event_types_table_id uuid NOT NULL DEFAULT uuid_nil(), + event_types_table_name text NOT NULL DEFAULT '', + + levels_table_id uuid NOT NULL DEFAULT uuid_nil(), + levels_table_name text NOT NULL DEFAULT '', + + level_requirements_table_id uuid NOT NULL DEFAULT uuid_nil(), + level_requirements_table_name text NOT NULL DEFAULT '', + + level_grants_table_id uuid NOT NULL DEFAULT uuid_nil(), + level_grants_table_name text NOT NULL DEFAULT '', + + achievement_rewards_table_id uuid NOT NULL DEFAULT uuid_nil(), + achievement_rewards_table_name text NOT NULL DEFAULT '', + + record_event text NOT NULL DEFAULT '', + remove_event text NOT NULL DEFAULT '', + tg_event text NOT NULL DEFAULT '', + tg_event_toggle text NOT NULL DEFAULT '', + tg_event_toggle_bool text NOT NULL DEFAULT '', + tg_event_bool text NOT NULL DEFAULT '', + upsert_aggregate text NOT NULL DEFAULT '', + tg_update_aggregates text NOT NULL DEFAULT '', + prune_events text NOT NULL DEFAULT '', + steps_required text NOT NULL DEFAULT '', + level_achieved text NOT NULL DEFAULT '', + tg_check_achievements text NOT NULL DEFAULT '', + grant_achievement text NOT NULL DEFAULT '', + tg_achievement_reward text NOT NULL DEFAULT '', + + -- Partition lifecycle configuration for events table + "interval" text NOT NULL DEFAULT '1 month', + retention text DEFAULT '12 months', + premake int NOT NULL DEFAULT 2, + + -- Scope: determines the security level for this module instance. + scope text NOT NULL DEFAULT 'app', + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- Entity table for RLS (NULL for app-level, entity table for entity-scoped) + entity_table_id uuid NULL, + + -- required tables + actor_table_id uuid NOT NULL DEFAULT uuid_nil(), + + + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + + -- API routing (configurable per-module) + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + + CONSTRAINT events_table_fkey FOREIGN KEY (events_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT event_aggregates_table_fkey FOREIGN KEY (event_aggregates_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT event_types_table_fkey FOREIGN KEY (event_types_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT levels_table_fkey FOREIGN KEY (levels_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT level_requirements_table_fkey FOREIGN KEY (level_requirements_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT level_grants_table_fkey FOREIGN KEY (level_grants_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT achievement_rewards_table_fkey FOREIGN KEY (achievement_rewards_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT entity_table_fkey FOREIGN KEY (entity_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT actor_table_fkey FOREIGN KEY (actor_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX events_module_database_id_idx ON metaschema_modules_public.events_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/function_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/function_module/table.sql new file mode 100644 index 00000000..841d5345 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/function_module/table.sql @@ -0,0 +1,79 @@ +-- Deploy schemas/metaschema_modules_public/tables/function_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.function_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- Schema references (if uuid_nil, resolved from schema name or default) + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Optional schema name overrides (used when schema IDs are not provided) + public_schema_name text, + private_schema_name text, + + -- Generated table IDs (populated by the generator) + definitions_table_id uuid NOT NULL DEFAULT uuid_nil(), + invocations_table_id uuid NOT NULL DEFAULT uuid_nil(), + execution_logs_table_id uuid NOT NULL DEFAULT uuid_nil(), + secret_definitions_table_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Table names (input to the generator — bare names without scope prefix). + -- The trigger prepends the scope prefix automatically. + definitions_table_name text NOT NULL DEFAULT 'function_definitions', + invocations_table_name text NOT NULL DEFAULT 'function_invocations', + execution_logs_table_name text NOT NULL DEFAULT 'function_execution_logs', + secret_definitions_table_name text NOT NULL DEFAULT 'secret_definitions', + + -- API routing (get-or-create: if set, schema is added to this API; if NULL, no API is added) + api_name text, + private_api_name text, + + -- Scope: determines the security level for this module instance. + -- Resolved to a membership_type integer at trigger time via membership_types table. + scope text NOT NULL DEFAULT 'app', + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + -- Override to create multiple module instances at the same scope. + prefix text NOT NULL DEFAULT '', + + -- Entity table for RLS (NULL for app-level functions, entity table for entity-scoped functions) + entity_table_id uuid NULL, + + -- Configurable security policies (NULL = use defaults based on scope). + -- When provided, replaces the default policy set in apply_function_security. + -- Accepts a JSON array of policy objects: + -- {"$type": "AuthzEntityMembership", "privileges": ["select", "update"], "data": {...}} + policies jsonb NULL, + + -- Per-table provisions overrides from blueprint config. + -- Keys are table keys (definitions, invocations, execution_logs, secret_definitions). + -- When a key is present, the module trigger skips default security for that table; + -- secure_table_provision applies the custom grants/policies instead. + provisions jsonb NULL, + + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + + -- Constraints + CONSTRAINT function_module_db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT function_module_schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT function_module_private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT function_module_definitions_table_fkey FOREIGN KEY (definitions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT function_module_invocations_table_fkey FOREIGN KEY (invocations_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT function_module_execution_logs_table_fkey FOREIGN KEY (execution_logs_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT function_module_secret_defs_table_fkey FOREIGN KEY (secret_definitions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT function_module_entity_table_fkey FOREIGN KEY (entity_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX function_module_database_id_idx ON metaschema_modules_public.function_module ( database_id ); + +-- Unique constraint: one function module per database per scope per prefix. +CREATE UNIQUE INDEX function_module_unique_scope ON metaschema_modules_public.function_module ( database_id, scope, prefix ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/graph_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/graph_module/table.sql new file mode 100644 index 00000000..1eaf0750 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/graph_module/table.sql @@ -0,0 +1,75 @@ +-- Deploy schemas/metaschema_modules_public/tables/graph_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema +-- requires: schemas/metaschema_modules_public/tables/merkle_store_module/table + +BEGIN; + +CREATE TABLE metaschema_modules_public.graph_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- Schema references (if uuid_nil, resolved from schema name or default) + public_schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Optional schema name overrides (used when schema IDs are not provided) + public_schema_name text, + private_schema_name text, + + -- Scope: determines the security level for this module instance. + scope text NOT NULL DEFAULT 'app', + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- Reference to the Merkle store this graph module depends on + merkle_store_module_id uuid NOT NULL, + + -- Generated table IDs (populated by BEFORE INSERT trigger) + graphs_table_id uuid NOT NULL DEFAULT uuid_nil(), + executions_table_id uuid NOT NULL DEFAULT uuid_nil(), + outputs_table_id uuid NOT NULL DEFAULT uuid_nil(), + + -- API routing (get-or-create: if set, schema is added to this API; if NULL, no API is added) + api_name text, + private_api_name text, + + -- Entity table for RLS (NULL for app-level, entity table for entity-scoped) + entity_table_id uuid NULL, + + -- Configurable security policies (NULL = use defaults). + -- Accepts a JSON array of policy objects: + -- {"$type": "AuthzEntityMembership", "privileges": ["select", "update"], "data": {...}} + policies jsonb NULL, + + -- Per-table provisions overrides from blueprint config. + -- Keys are table keys (graphs, executions, outputs). + -- When a key is present, the module trigger skips default security for that table; + -- secure_table_provision applies the custom grants/policies instead. + provisions jsonb NULL, + + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + + -- Timestamps + created_at timestamptz NOT NULL DEFAULT now(), + + -- Constraints + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT public_schema_fkey FOREIGN KEY (public_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT merkle_store_fkey FOREIGN KEY (merkle_store_module_id) REFERENCES metaschema_modules_public.merkle_store_module (id) ON DELETE CASCADE, + CONSTRAINT graphs_table_fkey FOREIGN KEY (graphs_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT executions_table_fkey FOREIGN KEY (executions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT outputs_table_fkey FOREIGN KEY (outputs_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT graph_module_entity_table_fkey FOREIGN KEY (entity_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + -- Only one graph module per database + merkle store combination + CONSTRAINT graph_module_database_merkle_unique UNIQUE (database_id, merkle_store_module_id) +); + +CREATE INDEX graph_module_database_id_idx ON metaschema_modules_public.graph_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/hierarchy_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/hierarchy_module/table.sql new file mode 100644 index 00000000..d85ad586 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/hierarchy_module/table.sql @@ -0,0 +1,68 @@ +-- Deploy schemas/metaschema_modules_public/tables/hierarchy_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.hierarchy_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- Schema references + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Table references for created tables + chart_edges_table_id uuid NOT NULL DEFAULT uuid_nil(), + chart_edges_table_name text NOT NULL DEFAULT '', + + hierarchy_sprt_table_id uuid NOT NULL DEFAULT uuid_nil(), + hierarchy_sprt_table_name text NOT NULL DEFAULT '', + + chart_edge_grants_table_id uuid NOT NULL DEFAULT uuid_nil(), + chart_edge_grants_table_name text NOT NULL DEFAULT '', + + -- Required external table references + entity_table_id uuid NOT NULL, -- Organizations table (entity-scoped) + users_table_id uuid NOT NULL, -- Users table + + -- Scope: determines the security level for this module instance. + scope text NOT NULL DEFAULT 'org', + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- Resolved names for RLS parser lookups + private_schema_name text NOT NULL DEFAULT '', + sprt_table_name text NOT NULL DEFAULT '', + + -- Function names for helper functions + rebuild_hierarchy_function text NOT NULL DEFAULT '', + get_subordinates_function text NOT NULL DEFAULT '', + get_managers_function text NOT NULL DEFAULT '', + is_manager_of_function text NOT NULL DEFAULT '', + + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + + -- Timestamps + created_at timestamptz NOT NULL DEFAULT now(), + + -- Constraints + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT chart_edges_table_fkey FOREIGN KEY (chart_edges_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT hierarchy_sprt_table_fkey FOREIGN KEY (hierarchy_sprt_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT chart_edge_grants_table_fkey FOREIGN KEY (chart_edge_grants_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT entity_table_fkey FOREIGN KEY (entity_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT users_table_fkey FOREIGN KEY (users_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + -- Only one hierarchy module per database + CONSTRAINT hierarchy_module_database_unique UNIQUE (database_id) +); + +CREATE INDEX hierarchy_module_database_id_idx ON metaschema_modules_public.hierarchy_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/i18n_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/i18n_module/table.sql new file mode 100644 index 00000000..df567812 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/i18n_module/table.sql @@ -0,0 +1,31 @@ +-- Deploy schemas/metaschema_modules_public/tables/i18n_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.i18n_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- Schema references (populated by the insert trigger) + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Settings table (populated by the generator) + settings_table_id uuid NOT NULL DEFAULT uuid_nil(), + + -- API routing (configurable per-module) + api_name text DEFAULT NULL, + private_api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT settings_table_fkey FOREIGN KEY (settings_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX i18n_module_database_id_idx ON metaschema_modules_public.i18n_module ( database_id ); +CREATE UNIQUE INDEX i18n_module_unique_per_db ON metaschema_modules_public.i18n_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/identity_providers_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/identity_providers_module/table.sql new file mode 100644 index 00000000..ae017f4d --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/identity_providers_module/table.sql @@ -0,0 +1,65 @@ +-- Deploy schemas/metaschema_modules_public/tables/identity_providers_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.identity_providers_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + table_id uuid NOT NULL DEFAULT uuid_nil(), + + table_name text NOT NULL DEFAULT 'identity_providers', + + -- API routing (configurable per-module) + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + + -- Entity-aware scope: determines which config_secrets_module table + -- the rotate_identity_provider_{prefix}_secret proc targets. + -- 'app' = app_secrets (AuthzAppMembership, admin-only) + -- 'org' = org_secrets (AuthzEntityMembership, per-org) + -- Future entity types are also supported via the membership_types table. + scope text NOT NULL DEFAULT 'app', + + -- Table name prefix for the generated rotate proc. + -- Auto-derived from scope by the trigger when empty. + -- e.g. scope='app' → prefix='app' → rotate_identity_provider_app_secret + prefix text NOT NULL DEFAULT '', + + -- Entity table for RLS (NULL for app-level, entity table for entity-scoped) + entity_table_id uuid NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT entity_table_fkey FOREIGN KEY (entity_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX identity_providers_module_database_id_idx ON metaschema_modules_public.identity_providers_module ( database_id ); +CREATE INDEX identity_providers_module_schema_id_idx ON metaschema_modules_public.identity_providers_module ( schema_id ); +CREATE INDEX identity_providers_module_private_schema_id_idx ON metaschema_modules_public.identity_providers_module ( private_schema_id ); +CREATE INDEX identity_providers_module_table_id_idx ON metaschema_modules_public.identity_providers_module ( table_id ); + +-- One install per database per scope +CREATE UNIQUE INDEX identity_providers_module_unique_scope ON metaschema_modules_public.identity_providers_module ( database_id, scope ); + +COMMENT ON TABLE metaschema_modules_public.identity_providers_module IS + 'Entity-aware config row for the identity_providers_module, which provisions a per-database + identity_providers table holding OAuth2 / OIDC (and future SAML) provider definitions. + The scope column determines which config_secrets_module table the rotate proc targets + (app_secrets for app scope, org_secrets for org scope). When scope = platform, + the secrets table gets a database_id column and platform-level RLS via + AuthzRelatedEntityMembership through database.owner_id. + Scoping matrix: + scope=app → per-database flat, in-app admin manages + scope=platform → per-database, platform admin manages (generate:constructive) + scope=org → per-org tenant, org admin manages'; +COMMENT ON COLUMN metaschema_modules_public.identity_providers_module.private_schema_id IS 'Private schema that hosts SECURITY DEFINER admin helpers which write to identity_providers (create / update / enable / disable / rotate-secret / delete) and the per-app quota check.'; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/inference_log_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/inference_log_module/table.sql new file mode 100644 index 00000000..8c94221b --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/inference_log_module/table.sql @@ -0,0 +1,49 @@ +-- Deploy schemas/metaschema_modules_public/tables/inference_log_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.inference_log_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Inference log table (partitioned by created_at) + inference_log_table_id uuid NOT NULL DEFAULT uuid_nil(), + inference_log_table_name text NOT NULL DEFAULT '', + + -- Pre-aggregated daily rollup table + usage_daily_table_id uuid NOT NULL DEFAULT uuid_nil(), + usage_daily_table_name text NOT NULL DEFAULT '', + + -- Partition lifecycle configuration + "interval" text NOT NULL DEFAULT '1 month', + retention text NOT NULL DEFAULT '12 months', + premake int NOT NULL DEFAULT 2, + + -- Scope configuration: 'app' = per-app usage (actor_id RLS) + scope text NOT NULL DEFAULT 'app', + actor_fk_table_id uuid NULL, + entity_fk_table_id uuid NULL, + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- API routing (configurable per-module) + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT inference_log_table_fkey FOREIGN KEY (inference_log_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT usage_daily_table_fkey FOREIGN KEY (usage_daily_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT inference_log_module_database_id_prefix_unique UNIQUE NULLS NOT DISTINCT (database_id, prefix) +); + +CREATE INDEX inference_log_module_database_id_idx ON metaschema_modules_public.inference_log_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/invites_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/invites_module/table.sql new file mode 100644 index 00000000..8a1a1297 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/invites_module/table.sql @@ -0,0 +1,54 @@ +-- Deploy schemas/metaschema_modules_public/tables/invites_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.invites_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + emails_table_id uuid NOT NULL DEFAULT uuid_nil(), + users_table_id uuid NOT NULL DEFAULT uuid_nil(), + + invites_table_id uuid NOT NULL DEFAULT uuid_nil(), + claimed_invites_table_id uuid NOT NULL DEFAULT uuid_nil(), + + invites_table_name text NOT NULL DEFAULT '', + claimed_invites_table_name text NOT NULL DEFAULT '', + submit_invite_code_function text NOT NULL DEFAULT '', + + -- Scope: determines the security level for this module instance. + scope text NOT NULL DEFAULT 'app', + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- Entity table for RLS (NULL for app-level, entity table for entity-scoped) + entity_table_id uuid NULL, + + -- API routing (configurable per-module) + api_name text DEFAULT 'admin', + private_api_name text DEFAULT NULL, + + -- + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT invites_table_fkey FOREIGN KEY (invites_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT emails_table_fkey FOREIGN KEY (emails_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT users_table_fkey FOREIGN KEY (users_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT entity_table_fkey FOREIGN KEY (entity_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT claimed_invites_table_fkey FOREIGN KEY (claimed_invites_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT pschema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE +); + +CREATE INDEX invites_module_database_id_idx ON metaschema_modules_public.invites_module ( database_id ); + +-- Unique constraint: one invites module per database per scope per prefix. +CREATE UNIQUE INDEX invites_module_unique_scope + ON metaschema_modules_public.invites_module (database_id, scope, prefix); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/limits_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/limits_module/table.sql new file mode 100644 index 00000000..bde28379 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/limits_module/table.sql @@ -0,0 +1,102 @@ +-- Deploy schemas/metaschema_modules_public/tables/limits_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.limits_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + -- + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + --- + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT '', + + default_table_id uuid NOT NULL DEFAULT uuid_nil(), + default_table_name text NOT NULL DEFAULT '', + -- + + limit_increment_function text NOT NULL DEFAULT '', + limit_decrement_function text NOT NULL DEFAULT '', + limit_increment_trigger text NOT NULL DEFAULT '', + limit_decrement_trigger text NOT NULL DEFAULT '', + limit_update_trigger text NOT NULL DEFAULT '', + limit_check_function text NOT NULL DEFAULT '', + + -- Credit grants ledger table + limit_credits_table_id uuid NULL, + + -- Events audit trail table + events_table_id uuid NULL, + + -- Credit codes table (app-level only, database-wide) + credit_codes_table_id uuid NULL, + + -- Credit code items table (app-level only, database-wide) + credit_code_items_table_id uuid NULL, + + -- Credit redemptions table (app-level only, database-wide) + credit_redemptions_table_id uuid NULL, + + -- Aggregate entity limits (org-level caps, no actor_id) + aggregate_table_id uuid NULL, + + -- Cap tables (static config limits, no metering) + limit_caps_table_id uuid NULL, + limit_caps_defaults_table_id uuid NULL, + + -- Cap check trigger function (gates inserts behind cap/feature flag values) + cap_check_trigger text NOT NULL DEFAULT '', + + -- Resolve cap function (COALESCE lookup: per-entity → default → 0) + resolve_cap_function text NOT NULL DEFAULT '', + + -- Warning tables for soft-limit notifications + limit_warnings_table_id uuid NULL, + limit_warning_state_table_id uuid NULL, + + -- Soft limit check functions + limit_check_soft_function text NOT NULL DEFAULT '', + limit_aggregate_check_soft_function text NOT NULL DEFAULT '', + + -- Scope: determines the security level for this module instance. + scope text NOT NULL DEFAULT 'app', + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- Entity table for RLS (NULL for app-level, entity table for entity-scoped) + entity_table_id uuid NULL, + + -- required tables + actor_table_id uuid NOT NULL DEFAULT uuid_nil(), + + -- API routing (configurable per-module) + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT default_table_fkey FOREIGN KEY (default_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT entity_table_fkey FOREIGN KEY (entity_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT actor_table_fkey FOREIGN KEY (actor_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT aggregate_table_fkey FOREIGN KEY (aggregate_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT limit_credits_table_fkey FOREIGN KEY (limit_credits_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT events_table_fkey FOREIGN KEY (events_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT credit_codes_table_fkey FOREIGN KEY (credit_codes_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT credit_code_items_table_fkey FOREIGN KEY (credit_code_items_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT credit_redemptions_table_fkey FOREIGN KEY (credit_redemptions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT limit_caps_table_fkey FOREIGN KEY (limit_caps_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT limit_caps_defaults_table_fkey FOREIGN KEY (limit_caps_defaults_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT limit_warnings_table_fkey FOREIGN KEY (limit_warnings_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT limit_warning_state_table_fkey FOREIGN KEY (limit_warning_state_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE + +); + +CREATE INDEX limits_module_database_id_idx ON metaschema_modules_public.limits_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/membership_types_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/membership_types_module/table.sql new file mode 100644 index 00000000..dc5c8a6f --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/membership_types_module/table.sql @@ -0,0 +1,25 @@ +-- Deploy schemas/metaschema_modules_public/tables/membership_types_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.membership_types_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + -- + schema_id uuid NOT NULL DEFAULT uuid_nil(), + + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT 'membership_types', + + -- + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX membership_types_module_database_id_idx ON metaschema_modules_public.membership_types_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/memberships_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/memberships_module/table.sql new file mode 100644 index 00000000..fa07cdfb --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/memberships_module/table.sql @@ -0,0 +1,98 @@ +-- Deploy schemas/metaschema_modules_public/tables/memberships_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.memberships_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + -- + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + memberships_table_id uuid NOT NULL DEFAULT uuid_nil(), + memberships_table_name text NOT NULL DEFAULT '', + + members_table_id uuid NOT NULL DEFAULT uuid_nil(), + members_table_name text NOT NULL DEFAULT '', + + membership_defaults_table_id uuid NOT NULL DEFAULT uuid_nil(), + membership_defaults_table_name text NOT NULL DEFAULT '', + + -- Nullable: only created when entity_table_id IS NOT NULL (entity-scoped membership types) + membership_settings_table_id uuid NULL, + membership_settings_table_name text NOT NULL DEFAULT '', + + grants_table_id uuid NOT NULL DEFAULT uuid_nil(), + grants_table_name text NOT NULL DEFAULT '', + + -- required tables + actor_table_id uuid NOT NULL DEFAULT uuid_nil(), + limits_table_id uuid NOT NULL DEFAULT uuid_nil(), + default_limits_table_id uuid NOT NULL DEFAULT uuid_nil(), + permissions_table_id uuid NOT NULL DEFAULT uuid_nil(), + default_permissions_table_id uuid NOT NULL DEFAULT uuid_nil(), + sprt_table_id uuid NOT NULL DEFAULT uuid_nil(), + + admin_grants_table_id uuid NOT NULL DEFAULT uuid_nil(), + admin_grants_table_name text NOT NULL DEFAULT '', + + owner_grants_table_id uuid NOT NULL DEFAULT uuid_nil(), + owner_grants_table_name text NOT NULL DEFAULT '', + + -- Scope: determines the security level for this module instance. + scope text NOT NULL DEFAULT 'app', + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- Entity table for RLS (NULL for app-level, entity table for entity-scoped) + entity_table_id uuid NULL, + entity_table_owner_id uuid NULL, + + -- Populated by memberships_module generator when get_organization_id is created + get_org_fn text NULL, + + -- + + actor_mask_check text NOT NULL DEFAULT '', + actor_perm_check text NOT NULL DEFAULT '', + entity_ids_by_mask text NULL, + entity_ids_by_perm text NULL, + entity_ids_function text NULL, + + member_profiles_table_id uuid NULL, + + -- + + -- API routing (configurable per-module) + api_name text DEFAULT 'admin', + private_api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + + CONSTRAINT memberships_table_fkey FOREIGN KEY (memberships_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT membership_defaults_table_fkey FOREIGN KEY (membership_defaults_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT membership_settings_table_fkey FOREIGN KEY (membership_settings_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT members_table_fkey FOREIGN KEY (members_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT grants_table_fkey FOREIGN KEY (grants_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT sprt_table_fkey FOREIGN KEY (sprt_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + CONSTRAINT entity_table_fkey FOREIGN KEY (entity_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT entity_table_owner_fkey FOREIGN KEY (entity_table_owner_id) REFERENCES metaschema_public.field (id) ON DELETE CASCADE, + CONSTRAINT actor_table_fkey FOREIGN KEY (actor_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT limits_table_fkey FOREIGN KEY (limits_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT default_limits_table_fkey FOREIGN KEY (default_limits_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + CONSTRAINT permissions_table_fkey FOREIGN KEY (permissions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT default_permissions_table_fkey FOREIGN KEY (default_permissions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + CONSTRAINT memberships_module_unique UNIQUE (database_id, scope, prefix) +); + +CREATE INDEX memberships_module_database_id_idx ON metaschema_modules_public.memberships_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/merkle_store_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/merkle_store_module/table.sql new file mode 100644 index 00000000..744c5f66 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/merkle_store_module/table.sql @@ -0,0 +1,56 @@ +-- Deploy schemas/metaschema_modules_public/tables/merkle_store_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.merkle_store_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- Schema references (if uuid_nil, resolved from schema name or default) + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Optional schema name overrides (used when schema IDs are not provided) + public_schema_name text, + private_schema_name text, + + -- Generated table IDs (populated by BEFORE INSERT trigger) + object_table_id uuid NOT NULL DEFAULT uuid_nil(), + store_table_id uuid NOT NULL DEFAULT uuid_nil(), + commit_table_id uuid NOT NULL DEFAULT uuid_nil(), + ref_table_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Table/function prefix (e.g., 'graph' -> graph_object, graph_store, ...) + -- Stored normalized (no trailing underscore); underscore added at generation time + prefix text NOT NULL DEFAULT '', + + -- API routing (get-or-create: if set, schema is added to this API; if NULL, no API is added) + api_name text, + private_api_name text, + + -- Scope: 'app' for app-level, 'platform' for database-scoped with + -- RLS through metaschema_public.database ownership. + scope text NOT NULL DEFAULT 'app', + + -- Timestamps + created_at timestamptz NOT NULL DEFAULT now(), + + -- Constraints + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT object_table_fkey FOREIGN KEY (object_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT store_table_fkey FOREIGN KEY (store_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT commit_table_fkey FOREIGN KEY (commit_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT ref_table_fkey FOREIGN KEY (ref_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + -- Only one merkle store module per database + prefix combination + CONSTRAINT merkle_store_module_database_prefix_unique UNIQUE (database_id, prefix) +); + +CREATE INDEX merkle_store_module_database_id_idx ON metaschema_modules_public.merkle_store_module ( database_id ); +CREATE INDEX merkle_store_module_private_schema_id_idx ON metaschema_modules_public.merkle_store_module ( private_schema_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/namespace_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/namespace_module/table.sql new file mode 100644 index 00000000..2015322a --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/namespace_module/table.sql @@ -0,0 +1,72 @@ +-- Deploy schemas/metaschema_modules_public/tables/namespace_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.namespace_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- Schema references (if uuid_nil, resolved from schema name or default) + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Optional schema name overrides (used when schema IDs are not provided) + public_schema_name text, + private_schema_name text, + + -- Generated table IDs (populated by the generator) + namespaces_table_id uuid NOT NULL DEFAULT uuid_nil(), + namespace_events_table_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Table names (input to the generator) + namespaces_table_name text NOT NULL DEFAULT 'namespaces', + namespace_events_table_name text NOT NULL DEFAULT 'namespace_events', + + -- API routing (get-or-create: if set, schema is added to this API; if NULL, no API is added) + api_name text, + private_api_name text, + + -- Scope: determines the security level for this module instance. + -- Resolved to a membership_type integer at trigger time via membership_types table. + scope text NOT NULL DEFAULT 'app', + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + -- Override to create multiple module instances at the same scope. + prefix text NOT NULL DEFAULT '', + + -- Entity table for RLS (NULL for app-level namespaces, entity table for entity-scoped namespaces) + entity_table_id uuid NULL, + + -- Configurable security policies (NULL = use defaults based on scope). + -- When provided, replaces the default policy set in apply_namespace_security. + -- Accepts a JSON array of policy objects: + -- {"$type": "AuthzEntityMembership", "privileges": ["select", "update"], "data": {...}} + policies jsonb NULL, + + -- Per-table provisions overrides from blueprint config. + -- Keys are table keys (namespaces, namespace_events). + -- When a key is present, the module trigger skips default security for that table; + -- secure_table_provision applies the custom grants/policies instead. + provisions jsonb NULL, + + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + + -- Constraints + CONSTRAINT namespace_module_db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT namespace_module_schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT namespace_module_private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT namespace_module_namespaces_table_fkey FOREIGN KEY (namespaces_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT namespace_module_events_table_fkey FOREIGN KEY (namespace_events_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT namespace_module_entity_table_fkey FOREIGN KEY (entity_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX namespace_module_database_id_idx ON metaschema_modules_public.namespace_module ( database_id ); + +-- Unique constraint: one namespace module per database per scope per prefix. +CREATE UNIQUE INDEX namespace_module_unique_scope ON metaschema_modules_public.namespace_module ( database_id, scope, prefix ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/notifications_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/notifications_module/table.sql new file mode 100644 index 00000000..0acd075b --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/notifications_module/table.sql @@ -0,0 +1,74 @@ +-- Deploy schemas/metaschema_modules_public/tables/notifications_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.notifications_module ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4 (), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + notifications_table_id uuid NOT NULL DEFAULT uuid_nil(), + read_state_table_id uuid NOT NULL DEFAULT uuid_nil(), + -- Feature-gated tables: NULL when the corresponding has_* flag is off. + -- The generator returns NULL for tables it skipped; the insert trigger + -- forwards that through unchanged. + preferences_table_id uuid, + channels_table_id uuid, + delivery_log_table_id uuid, + + owner_table_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Optional: table_template_module rows to extend with notifs_* columns. + -- Resolved at install time; NULL disables the corresponding extension. + user_settings_table_id uuid, + organization_settings_table_id uuid, + + -- Sub-feature toggles. Resolved at module-generation time so the generated + -- schema has zero dead code. Validated by the BEFORE INSERT trigger; see + -- insert_notifications_module for dependency rules. + has_channels boolean NOT NULL DEFAULT true, + has_preferences boolean NOT NULL DEFAULT true, + has_settings_extension boolean NOT NULL DEFAULT false, + has_digest_metadata boolean NOT NULL DEFAULT false, + has_subscriptions boolean NOT NULL DEFAULT false, + + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + + -- API routing (configurable per-module) + api_name text DEFAULT 'notifications', + private_api_name text DEFAULT NULL, + + -- + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT notifications_table_fkey FOREIGN KEY (notifications_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT read_state_table_fkey FOREIGN KEY (read_state_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT preferences_table_fkey FOREIGN KEY (preferences_table_id) REFERENCES metaschema_public.table (id) ON DELETE SET NULL, + CONSTRAINT channels_table_fkey FOREIGN KEY (channels_table_id) REFERENCES metaschema_public.table (id) ON DELETE SET NULL, + CONSTRAINT delivery_log_table_fkey FOREIGN KEY (delivery_log_table_id) REFERENCES metaschema_public.table (id) ON DELETE SET NULL, + CONSTRAINT owner_table_fkey FOREIGN KEY (owner_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT user_settings_table_fkey FOREIGN KEY (user_settings_table_id) REFERENCES metaschema_public.table (id) ON DELETE SET NULL, + CONSTRAINT organization_settings_table_fkey FOREIGN KEY (organization_settings_table_id) REFERENCES metaschema_public.table (id) ON DELETE SET NULL, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE +); + +COMMENT ON CONSTRAINT schema_fkey ON metaschema_modules_public.notifications_module IS E'@omit manyToMany'; +COMMENT ON CONSTRAINT private_schema_fkey ON metaschema_modules_public.notifications_module IS E'@omit manyToMany'; +COMMENT ON CONSTRAINT notifications_table_fkey ON metaschema_modules_public.notifications_module IS E'@fieldName notificationsTableByNotificationsTableId\n@omit manyToMany'; +COMMENT ON CONSTRAINT read_state_table_fkey ON metaschema_modules_public.notifications_module IS E'@fieldName readStateTableByReadStateTableId\n@omit manyToMany'; +COMMENT ON CONSTRAINT preferences_table_fkey ON metaschema_modules_public.notifications_module IS E'@fieldName preferencesTableByPreferencesTableId\n@omit manyToMany'; +COMMENT ON CONSTRAINT channels_table_fkey ON metaschema_modules_public.notifications_module IS E'@fieldName channelsTableByChannelsTableId\n@omit manyToMany'; +COMMENT ON CONSTRAINT delivery_log_table_fkey ON metaschema_modules_public.notifications_module IS E'@fieldName deliveryLogTableByDeliveryLogTableId\n@omit manyToMany'; +COMMENT ON CONSTRAINT owner_table_fkey ON metaschema_modules_public.notifications_module IS E'@omit manyToMany'; +COMMENT ON CONSTRAINT user_settings_table_fkey ON metaschema_modules_public.notifications_module IS E'@fieldName userSettingsTableByUserSettingsTableId\n@omit manyToMany'; +COMMENT ON CONSTRAINT organization_settings_table_fkey ON metaschema_modules_public.notifications_module IS E'@fieldName organizationSettingsTableByOrganizationSettingsTableId\n@omit manyToMany'; +COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.notifications_module IS E'@omit manyToMany'; +CREATE INDEX notifications_module_database_id_idx ON metaschema_modules_public.notifications_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/permissions_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/permissions_module/table.sql new file mode 100644 index 00000000..9017fe42 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/permissions_module/table.sql @@ -0,0 +1,82 @@ +-- Deploy schemas/metaschema_modules_public/tables/permissions_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.permissions_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + -- + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT '', + -- + + -- + default_table_id uuid NOT NULL DEFAULT uuid_nil(), + default_table_name text NOT NULL DEFAULT '', + -- + + -- Default bit-width of the permission mask for this module. + -- + -- Chosen to maximize permission headroom without costing extra storage or + -- compute. PostgreSQL lays out heap tuples to MAXALIGN (8 bytes on x86_64), + -- so the row-size bucket that holds bit(24) already extends up to bit(64): + -- + -- bitlen | row bytes | heap (1M rows) | btree idx (1M rows) + -- -------+-----------+----------------+-------------------- + -- 24 | 67 | 74 MB | 47 MB + -- 48 | 70 | 74 MB | 47 MB + -- 64 | 72 | 74 MB | 47 MB <-- same bucket + -- 65 | 73 | 81 MB | 47 MB <-- next bucket + -- + -- Bitwise AND/OR on bit(<=64) fits in a single 64-bit machine word, so + -- permission checks at 64 cost the same as at 24. Raising the default from + -- 24 to 64 gives new modules 6.4x more permission slots before anyone has + -- to think about running update_bitlen_permissions, at identical storage + -- and compute cost. Do not raise past 64 casually -- bit(65+) jumps to the + -- next 8-byte tuple bucket (+~10% heap) and pays on every write. + -- + -- Existing databases are unaffected; this only changes the default for + -- newly inserted permissions_module rows. + bitlen int NOT NULL DEFAULT 64, + + -- Scope: determines the security level for this module instance. + scope text NOT NULL DEFAULT 'app', + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- Entity table for RLS (NULL for app-level, entity table for entity-scoped) + entity_table_id uuid NULL, + + -- required tables + actor_table_id uuid NOT NULL DEFAULT uuid_nil(), + + -- + + get_padded_mask text NOT NULL DEFAULT '', + get_mask text NOT NULL DEFAULT '', + get_by_mask text NOT NULL DEFAULT '', + get_mask_by_name text NOT NULL DEFAULT '', + + -- + + -- API routing (configurable per-module) + api_name text DEFAULT 'admin', + private_api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT default_table_fkey FOREIGN KEY (default_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT entity_table_fkey FOREIGN KEY (entity_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT actor_table_fkey FOREIGN KEY (actor_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX permissions_module_database_id_idx ON metaschema_modules_public.permissions_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/phone_numbers_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/phone_numbers_module/table.sql new file mode 100644 index 00000000..35db3eb8 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/phone_numbers_module/table.sql @@ -0,0 +1,33 @@ +-- Deploy schemas/metaschema_modules_public/tables/phone_numbers_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.phone_numbers_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + table_id uuid NOT NULL DEFAULT uuid_nil(), + owner_table_id uuid NOT NULL DEFAULT uuid_nil(), + + table_name text NOT NULL, + + -- API routing (configurable per-module) + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + + -- + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT owner_table_fkey FOREIGN KEY (owner_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE +); + +CREATE INDEX phone_numbers_module_database_id_idx ON metaschema_modules_public.phone_numbers_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/plans_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/plans_module/table.sql new file mode 100644 index 00000000..3f51ff9a --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/plans_module/table.sql @@ -0,0 +1,62 @@ +-- Deploy schemas/metaschema_modules_public/tables/plans_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.plans_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Plans table: defines plan tiers (Free, Pro, Enterprise) + plans_table_id uuid NOT NULL DEFAULT uuid_nil(), + plans_table_name text NOT NULL DEFAULT '', + + -- Plan limits table: maps plan → limit name → max value + plan_limits_table_id uuid NOT NULL DEFAULT uuid_nil(), + plan_limits_table_name text NOT NULL DEFAULT '', + + -- Plan pricing table: billing cycles, prices, discounts per plan + plan_pricing_table_id uuid NULL, + + -- Plan overrides table: per-entity custom limit overrides + plan_overrides_table_id uuid NULL, + + -- Plan meter limits table: maps plan → meter slug → billing quota + plan_meter_limits_table_id uuid NULL, + + -- Plan caps table: maps plan → cap name → cap value (feature flags) + plan_caps_table_id uuid NULL, + + -- Generated apply_plan functions (one per limits scope) + apply_plan_function text NOT NULL DEFAULT '', + apply_plan_aggregate_function text NOT NULL DEFAULT '', + + -- Generated apply functions for billing and caps (set when respective modules are installed) + apply_billing_plan_function text NULL, + apply_plan_caps_function text NULL, + + prefix text NULL, + + -- API routing (configurable per-module) + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT plans_table_fkey FOREIGN KEY (plans_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT plan_limits_table_fkey FOREIGN KEY (plan_limits_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT plan_pricing_table_fkey FOREIGN KEY (plan_pricing_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT plan_overrides_table_fkey FOREIGN KEY (plan_overrides_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT plan_meter_limits_table_fkey FOREIGN KEY (plan_meter_limits_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT plan_caps_table_fkey FOREIGN KEY (plan_caps_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT plans_module_database_id_unique UNIQUE (database_id) +); + +CREATE INDEX plans_module_database_id_idx ON metaschema_modules_public.plans_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/profiles_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/profiles_module/table.sql new file mode 100644 index 00000000..13728055 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/profiles_module/table.sql @@ -0,0 +1,71 @@ +-- Deploy schemas/metaschema_modules_public/tables/profiles_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.profiles_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + -- + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Main profiles table + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT '', + + -- Profile permissions join table (for UI) + profile_permissions_table_id uuid NOT NULL DEFAULT uuid_nil(), + profile_permissions_table_name text NOT NULL DEFAULT '', + + -- Profile grants audit table + profile_grants_table_id uuid NOT NULL DEFAULT uuid_nil(), + profile_grants_table_name text NOT NULL DEFAULT '', + + -- Profile definition grants audit table + profile_definition_grants_table_id uuid NOT NULL DEFAULT uuid_nil(), + profile_definition_grants_table_name text NOT NULL DEFAULT '', + + -- Profile templates table (for seeding profiles into new entities) + profile_templates_table_id uuid NOT NULL DEFAULT uuid_nil(), + profile_templates_table_name text NOT NULL DEFAULT '', + + -- Scope: determines the security level for this module instance. + scope text NOT NULL DEFAULT 'app', + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- Entity table for org/group scoped profiles (NULL for app-level) + entity_table_id uuid NULL, + + -- Required tables + actor_table_id uuid NOT NULL DEFAULT uuid_nil(), + permissions_table_id uuid NOT NULL DEFAULT uuid_nil(), + memberships_table_id uuid NOT NULL DEFAULT uuid_nil(), + + -- API routing (configurable per-module) + api_name text DEFAULT 'admin', + private_api_name text DEFAULT NULL, + + -- + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT profile_permissions_table_fkey FOREIGN KEY (profile_permissions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT profile_grants_table_fkey FOREIGN KEY (profile_grants_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT profile_definition_grants_table_fkey FOREIGN KEY (profile_definition_grants_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT profile_templates_table_fkey FOREIGN KEY (profile_templates_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT entity_table_fkey FOREIGN KEY (entity_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT actor_table_fkey FOREIGN KEY (actor_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT permissions_table_fkey FOREIGN KEY (permissions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT memberships_table_fkey FOREIGN KEY (memberships_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + CONSTRAINT profiles_module_unique UNIQUE (database_id, scope, prefix) +); + +CREATE INDEX profiles_module_database_id_idx ON metaschema_modules_public.profiles_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql new file mode 100644 index 00000000..984e7f47 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql @@ -0,0 +1,59 @@ +-- Deploy schemas/metaschema_modules_public/tables/rate_limit_meters_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.rate_limit_meters_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- Public schema: rate_limit_overrides table (admin-manageable via GraphQL API) + schema_id uuid NOT NULL DEFAULT uuid_nil(), + -- Private schema: rate_limit_state table, check_rate_limit function (internal) + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- State table: sliding window tracking per entity/actor/meter/window (private) + rate_limit_state_table_id uuid NOT NULL DEFAULT uuid_nil(), + rate_limit_state_table_name text NOT NULL DEFAULT '', + + -- Overrides table: per-entity and per-actor rate limit overrides (public) + rate_limit_overrides_table_id uuid NULL, + rate_limit_overrides_table_name text NOT NULL DEFAULT '', + + -- Rate window limits table: per-plan rate limit configuration (public) + rate_window_limits_table_id uuid NULL, + rate_window_limits_table_name text NOT NULL DEFAULT '', + + -- Generated check function (private) + check_rate_limit_function text NOT NULL DEFAULT '', + + prefix text NULL, + + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + + -- API routing (configurable per-module) + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT rate_limit_state_table_fkey FOREIGN KEY (rate_limit_state_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT rate_limit_overrides_table_fkey FOREIGN KEY (rate_limit_overrides_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT rate_window_limits_table_fkey FOREIGN KEY (rate_window_limits_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT rate_limit_meters_module_database_id_unique UNIQUE (database_id) +); + +CREATE INDEX rate_limit_meters_module_database_id_idx ON metaschema_modules_public.rate_limit_meters_module ( database_id ); + +COMMENT ON CONSTRAINT rate_limit_state_table_fkey + ON metaschema_modules_public.rate_limit_meters_module IS E'@fieldName rateLimitStateTableByRateLimitStateTableId'; +COMMENT ON CONSTRAINT rate_limit_overrides_table_fkey + ON metaschema_modules_public.rate_limit_meters_module IS E'@fieldName rateLimitOverridesTableByRateLimitOverridesTableId'; +COMMENT ON CONSTRAINT rate_window_limits_table_fkey + ON metaschema_modules_public.rate_limit_meters_module IS E'@fieldName rateWindowLimitsTableByRateWindowLimitsTableId'; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rate_limits_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rate_limits_module/table.sql new file mode 100644 index 00000000..b53e8927 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rate_limits_module/table.sql @@ -0,0 +1,41 @@ +-- Deploy schemas/metaschema_modules_public/tables/rate_limits_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.rate_limits_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- + schema_id uuid NOT NULL DEFAULT uuid_nil(), + rate_limit_settings_table_id uuid NOT NULL DEFAULT uuid_nil(), + ip_rate_limits_table_id uuid NOT NULL DEFAULT uuid_nil(), + rate_limits_table_id uuid NOT NULL DEFAULT uuid_nil(), + + rate_limit_settings_table text NOT NULL DEFAULT 'app_settings_rate_limit', + ip_rate_limits_table text NOT NULL DEFAULT 'auth_ip_rate_limits', + rate_limits_table text NOT NULL DEFAULT 'auth_rate_limits', + -- + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT rate_limit_settings_table_fkey FOREIGN KEY (rate_limit_settings_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT ip_rate_limits_table_fkey FOREIGN KEY (ip_rate_limits_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT rate_limits_table_fkey FOREIGN KEY (rate_limits_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + -- + CONSTRAINT rate_limits_module_database_id_uniq UNIQUE(database_id) +); + +CREATE INDEX rate_limits_module_database_id_idx ON metaschema_modules_public.rate_limits_module ( database_id ); + +COMMENT ON CONSTRAINT rate_limit_settings_table_fkey + ON metaschema_modules_public.rate_limits_module IS E'@fieldName rateLimitSettingsTableByRateLimitSettingsTableId'; +COMMENT ON CONSTRAINT ip_rate_limits_table_fkey + ON metaschema_modules_public.rate_limits_module IS E'@fieldName ipRateLimitsTableByIpRateLimitsTableId'; +COMMENT ON CONSTRAINT rate_limits_table_fkey + ON metaschema_modules_public.rate_limits_module IS E'@fieldName rateLimitsTableByRateLimitsTableId'; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/realtime_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/realtime_module/table.sql new file mode 100644 index 00000000..41973324 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/realtime_module/table.sql @@ -0,0 +1,52 @@ +-- Deploy schemas/metaschema_modules_public/tables/realtime_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.realtime_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- Schema references (populated by the insert trigger) + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + subscriptions_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Generated table IDs (populated by the generator) + change_log_table_id uuid NOT NULL DEFAULT uuid_nil(), + listener_node_table_id uuid NOT NULL DEFAULT uuid_nil(), + source_registry_table_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Partition lifecycle configuration for change_log + retention_hours integer NOT NULL DEFAULT 168, + premake int NOT NULL DEFAULT 7, + "interval" text NOT NULL DEFAULT '1 day', + + -- NOTIFY hybrid wake-up channel name (NULL = use default) + notify_channel text NULL, + + -- Constraints + -- API routing (configurable per-module) + api_name text DEFAULT 'realtime', + private_api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT subscriptions_schema_fkey FOREIGN KEY (subscriptions_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT change_log_table_fkey FOREIGN KEY (change_log_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT listener_node_table_fkey FOREIGN KEY (listener_node_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT source_registry_table_fkey FOREIGN KEY (source_registry_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX realtime_module_database_id_idx ON metaschema_modules_public.realtime_module ( database_id ); +CREATE UNIQUE INDEX realtime_module_unique_per_db ON metaschema_modules_public.realtime_module ( database_id ); +CREATE INDEX realtime_module_schema_id_idx ON metaschema_modules_public.realtime_module ( schema_id ); +CREATE INDEX realtime_module_private_schema_id_idx ON metaschema_modules_public.realtime_module ( private_schema_id ); +CREATE INDEX realtime_module_subscriptions_schema_id_idx ON metaschema_modules_public.realtime_module ( subscriptions_schema_id ); +CREATE INDEX realtime_module_change_log_table_id_idx ON metaschema_modules_public.realtime_module ( change_log_table_id ); +CREATE INDEX realtime_module_listener_node_table_id_idx ON metaschema_modules_public.realtime_module ( listener_node_table_id ); +CREATE INDEX realtime_module_source_registry_table_id_idx ON metaschema_modules_public.realtime_module ( source_registry_table_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/relation_provision/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/relation_provision/table.sql new file mode 100644 index 00000000..9ce83634 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/relation_provision/table.sql @@ -0,0 +1,279 @@ +-- Deploy schemas/metaschema_modules_public/tables/relation_provision/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.relation_provision ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + + database_id uuid NOT NULL, + + -- ========================================================================= + -- Relation type and tables + -- ========================================================================= + + relation_type text NOT NULL CHECK (relation_type IN ( + 'RelationBelongsTo', 'RelationHasOne', 'RelationHasMany', 'RelationManyToMany' + )), + + source_table_id uuid NOT NULL, + + target_table_id uuid NOT NULL, + + -- ========================================================================= + -- BelongsTo / HasOne / HasMany: FK field config + -- ========================================================================= + + field_name text DEFAULT NULL, + + delete_action text DEFAULT NULL, + + is_required boolean NOT NULL DEFAULT true, + + api_required boolean NOT NULL DEFAULT false, + + -- ========================================================================= + -- ManyToMany: junction table identity + -- ========================================================================= + + junction_table_id uuid NOT NULL DEFAULT uuid_nil(), + + junction_table_name text DEFAULT NULL, + + junction_schema_id uuid DEFAULT NULL, + + source_field_name text DEFAULT NULL, + + target_field_name text DEFAULT NULL, + + -- ========================================================================= + -- ManyToMany: junction table primary key strategy + -- ========================================================================= + + use_composite_key boolean NOT NULL DEFAULT false, + + -- ========================================================================= + -- Index creation on FK fields + -- ========================================================================= + + create_index boolean NOT NULL DEFAULT true, + + -- ========================================================================= + -- ManyToMany: API visibility (PostGraphile v5 @behavior +manyToMany) + -- ========================================================================= + + expose_in_api boolean NOT NULL DEFAULT true, + + -- ========================================================================= + -- ManyToMany: field creation (forwarded to provision_table) + -- ========================================================================= + + nodes jsonb NOT NULL DEFAULT '[]', + + -- ========================================================================= + -- ManyToMany: grants (forwarded to provision_table) + -- ========================================================================= + + grants jsonb NOT NULL DEFAULT '[]', + + -- ========================================================================= + -- ManyToMany: RLS policies (forwarded to secure_table_provision) + -- ========================================================================= + + policies jsonb NOT NULL DEFAULT '[]', + + -- ========================================================================= + -- Output columns (populated by the trigger, not set by callers) + -- ========================================================================= + + out_field_id uuid DEFAULT NULL, + + out_junction_table_id uuid DEFAULT NULL, + + out_source_field_id uuid DEFAULT NULL, + + out_target_field_id uuid DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT source_table_fkey FOREIGN KEY (source_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT target_table_fkey FOREIGN KEY (target_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +-- ============================================================================= +-- Table-level comment +-- ============================================================================= + +COMMENT ON TABLE metaschema_modules_public.relation_provision IS + 'Provisions relational structure between tables. Supports four relation types: + - RelationBelongsTo: adds a FK field on the source table referencing the target table (child perspective: "tasks belongs to projects" -> tasks.project_id). + - RelationHasMany: adds a FK field on the target table referencing the source table (parent perspective: "projects has many tasks" -> tasks.project_id). Inverse of BelongsTo. + - RelationHasOne: adds a FK field with a unique constraint on the source table referencing the target table. Also supports shared-primary-key patterns where the FK field IS the primary key (set field_name to the existing PK field name). + - RelationManyToMany: creates a junction table with FK fields to both source and target tables, delegating table creation and security to secure_table_provision. + This is a one-and-done structural provisioner. To layer additional security onto junction tables after creation, use secure_table_provision directly. + All operations are graceful: existing fields, FK constraints, and unique constraints are reused if found. + The trigger never injects values the caller did not provide. All security config is forwarded to secure_table_provision as-is.'; + +-- ============================================================================= +-- Relation type and tables +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.id IS + 'Unique identifier for this relation provision row.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.database_id IS + 'The database this relation belongs to. Required. Must match the database of both source_table_id and target_table_id.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.relation_type IS + 'The type of relation to create. Uses SuperCase naming: + - RelationBelongsTo: creates a FK field on source_table referencing target_table (e.g., tasks belongs to projects -> tasks.project_id). Field name auto-derived from target table. + - RelationHasMany: creates a FK field on target_table referencing source_table (e.g., projects has many tasks -> tasks.project_id). Field name auto-derived from source table. Inverse of BelongsTo — same FK, different perspective. + - RelationHasOne: creates a FK field + unique constraint on source_table referencing target_table (e.g., user_settings has one user -> user_settings.user_id with UNIQUE). Also supports shared-primary-key patterns (e.g., user_profiles.id = users.id) by setting field_name to the existing PK field. + - RelationManyToMany: creates a junction table with FK fields to both tables (e.g., projects and tags -> project_tags table). + Each relation type uses a different subset of columns on this table. Required.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.source_table_id IS + 'The source table in the relation. Required. + - RelationBelongsTo: the table that receives the FK field (e.g., tasks in "tasks belongs to projects"). + - RelationHasMany: the parent table being referenced (e.g., projects in "projects has many tasks"). The FK field is created on the target table. + - RelationHasOne: the table that receives the FK field + unique constraint (e.g., user_settings in "user_settings has one user"). + - RelationManyToMany: one of the two tables being joined (e.g., projects in "projects and tags"). The junction table will have a FK field referencing this table.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.target_table_id IS + 'The target table in the relation. Required. + - RelationBelongsTo: the table being referenced by the FK (e.g., projects in "tasks belongs to projects"). + - RelationHasMany: the table that receives the FK field (e.g., tasks in "projects has many tasks"). + - RelationHasOne: the table being referenced by the FK (e.g., users in "user_settings has one user"). + - RelationManyToMany: the other table being joined (e.g., tags in "projects and tags"). The junction table will have a FK field referencing this table.'; + +-- ============================================================================= +-- BelongsTo / HasOne / HasMany: FK field config +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.field_name IS + 'FK field name for RelationBelongsTo, RelationHasOne, and RelationHasMany. + - RelationBelongsTo/RelationHasOne: if NULL, auto-derived from the target table name (e.g., target "projects" derives "project_id"). + - RelationHasMany: if NULL, auto-derived from the source table name (e.g., source "projects" derives "project_id"). + For RelationHasOne shared-primary-key patterns, set field_name to the existing PK field (e.g., "id") so the FK reuses it. + Ignored for RelationManyToMany — use source_field_name/target_field_name instead.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.delete_action IS + 'FK delete action for RelationBelongsTo, RelationHasOne, and RelationHasMany. One of: c (CASCADE), r (RESTRICT), n (SET NULL), d (SET DEFAULT), a (NO ACTION). Required — the trigger raises an error if not provided. The caller must explicitly choose the cascade behavior; there is no default. Ignored for RelationManyToMany (junction FK fields always use CASCADE).'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.is_required IS + 'Whether the FK field is NOT NULL. Defaults to true. + - RelationBelongsTo: set to false for optional associations (e.g., tasks.assignee_id that can be NULL). + - RelationHasMany: set to false if the child can exist without a parent. + - RelationHasOne: typically true. + Ignored for RelationManyToMany (junction FK fields are always required).'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.api_required IS + 'Whether the FK field should be required at the API level even though it is nullable at the database level. Defaults to false. + When true and is_required is false, the field is created as nullable (allowing SET NULL cascade) but a @requiredInput smart tag is added so PostGraphile treats it as non-null in create/update input types. + When is_required is true, api_required is ignored (the field is already required at both levels). + Ignored for RelationManyToMany (junction FK fields are always required).'; + +-- ============================================================================= +-- ManyToMany: junction table identity +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.junction_table_id IS + 'For RelationManyToMany: an existing junction table to use. Defaults to uuid_nil(). + - When uuid_nil(): the trigger creates a new junction table via secure_table_provision using junction_table_name. + - When set to a valid table UUID: the trigger skips table creation and only adds FK fields, composite key (if use_composite_key is true), and security to the existing table. + Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.junction_table_name IS + 'For RelationManyToMany: name of the junction table to create or look up. If NULL, auto-derived from source and target table names using inflection_db (e.g., "projects" + "tags" derives "project_tags"). Only used when junction_table_id is uuid_nil(). Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.junction_schema_id IS + 'For RelationManyToMany: schema for the junction table. If NULL, defaults to the source table''s schema. Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.source_field_name IS + 'For RelationManyToMany: FK field name on the junction table referencing the source table. If NULL, auto-derived from the source table name using inflection_db.get_foreign_key_field_name() (e.g., source table "projects" derives "project_id"). Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.target_field_name IS + 'For RelationManyToMany: FK field name on the junction table referencing the target table. If NULL, auto-derived from the target table name using inflection_db.get_foreign_key_field_name() (e.g., target table "tags" derives "tag_id"). Ignored for RelationBelongsTo/RelationHasOne.'; + +-- ============================================================================= +-- ManyToMany: junction table primary key strategy +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.use_composite_key IS + 'For RelationManyToMany: whether to create a composite primary key from the two FK fields (source + target) on the junction table. Defaults to false. + - When true: the trigger calls metaschema.pk() with ARRAY[source_field_id, target_field_id] to create a composite PK. No separate id column is created. This enforces uniqueness of the pair and is suitable for simple junction tables. + - When false: no primary key is created by the trigger. The caller should provide node_type=''DataId'' to create a UUID primary key, or handle the PK strategy via a separate secure_table_provision row. + use_composite_key and node_type=''DataId'' are mutually exclusive — using both would create two conflicting PKs. + Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.create_index IS + 'Whether to create a btree index on FK fields created by this relation. Defaults to true. + PostgreSQL does not automatically index foreign key columns (only the referenced PK side is indexed). + Without indexes on FK columns, JOINs, CASCADE deletes, and RLS policy lookups perform sequential scans. + - RelationBelongsTo: creates an index on the FK field on the source table. + - RelationHasMany: creates an index on the FK field on the target table. + - RelationHasOne: skipped — the unique constraint already creates an implicit index. + - RelationManyToMany: creates indexes on both FK fields on the junction table. + Set to false only for very small tables or write-heavy tables where index maintenance cost outweighs read performance.'; + +-- ============================================================================= +-- ManyToMany: API visibility +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.expose_in_api IS + 'For RelationManyToMany: whether to expose the M:N shortcut fields in the GraphQL API. Defaults to true. + When true, sets @behavior +manyToMany on the junction table smart_tags so PostGraphile generates + clean M:N connection fields (e.g., event.contacts instead of event.contactEventsByEventId). + When false (or toggled off via UPDATE), the behavior tag is removed and the M:N fields disappear from GraphQL. + Toggling is supported: UPDATE expose_in_api to true/false and the smart tag is added/removed automatically. + Ignored for RelationBelongsTo/RelationHasOne/RelationHasMany.'; + +-- ============================================================================= +-- ManyToMany: field creation (forwarded to secure_table_provision) +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.nodes IS + 'For RelationManyToMany: array of node objects to apply to the junction table. Each element is a jsonb object with a required "$type" key and an optional "data" key. Forwarded to provision_table as-is. The trigger does not interpret or validate this value. + Examples: [{"$type": "DataId"}, {"$type": "DataTimestamps"}, {"$type": "DataDirectOwner", "data": {"owner_field_name": "author_id"}}]. + Defaults to ''[]'' (no node processing beyond the FK fields and composite key if use_composite_key is true). + Ignored for RelationBelongsTo/RelationHasOne/RelationHasMany.'; + +-- ============================================================================= +-- ManyToMany: grants (forwarded to secure_table_provision) +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.grants IS + 'For RelationManyToMany: array of grant objects for the junction table. Forwarded to provision_table as-is. Each element is a jsonb object with keys: "roles" (text[], required), "privileges" (jsonb[], required — array of [privilege, columns] tuples). Example: [{"roles":["authenticated"],"privileges":[["select","*"],["insert","*"],["delete","*"]]}]. Defaults to ''[]'' (no grants). Ignored for RelationBelongsTo/RelationHasOne.'; + +-- ============================================================================= +-- ManyToMany: RLS policies (forwarded to secure_table_provision) +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.policies IS + 'For RelationManyToMany: array of policy objects for the junction table. Forwarded to provision_table as-is. Each element is a jsonb object with keys: "$type" (text, required — the Authz* policy generator type), "data" (jsonb, optional — opaque config), "privileges" (text[], optional — e.g. ["select","insert"]; if omitted, derived from grants[] privilege verbs), "policy_role" (text, optional — falls back to first role in first grants[] entry, or ''authenticated''), "permissive" (boolean, optional, defaults to true), "policy_name" (text, optional). Supports multiple policies per row. + Example: [{"$type": "AuthzEntityMembership", "data": {"entity_field": "entity_id", "membership_type": 2}, "privileges": ["select", "insert", "delete"]}]. + Defaults to ''[]'' (no policies — the junction table will have RLS enabled but no policies unless added separately). + Ignored for RelationBelongsTo/RelationHasOne/RelationHasMany.'; + +-- ============================================================================= +-- Output columns +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.out_field_id IS + 'Output column for RelationBelongsTo/RelationHasOne/RelationHasMany: the UUID of the FK field created (or found). For BelongsTo/HasOne this is on the source table; for HasMany this is on the target table. Populated by the trigger. NULL for RelationManyToMany. Callers should not set this directly.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.out_junction_table_id IS + 'Output column for RelationManyToMany: the UUID of the junction table created (or found). Populated by the trigger. NULL for RelationBelongsTo/RelationHasOne. Callers should not set this directly.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.out_source_field_id IS + 'Output column for RelationManyToMany: the UUID of the FK field on the junction table referencing the source table. Populated by the trigger. NULL for RelationBelongsTo/RelationHasOne. Callers should not set this directly.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.out_target_field_id IS + 'Output column for RelationManyToMany: the UUID of the FK field on the junction table referencing the target table. Populated by the trigger. NULL for RelationBelongsTo/RelationHasOne. Callers should not set this directly.'; + +CREATE INDEX relation_provision_database_id_idx ON metaschema_modules_public.relation_provision ( database_id ); +CREATE INDEX relation_provision_relation_type_idx ON metaschema_modules_public.relation_provision ( relation_type ); +CREATE INDEX relation_provision_source_table_id_idx ON metaschema_modules_public.relation_provision ( source_table_id ); +CREATE INDEX relation_provision_target_table_id_idx ON metaschema_modules_public.relation_provision ( target_table_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rls_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rls_module/table.sql new file mode 100644 index 00000000..ef6d8bf0 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rls_module/table.sql @@ -0,0 +1,48 @@ +-- Deploy schemas/metaschema_modules_public/tables/rls_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema +-- requires: schemas/services_public/tables/apis/table + +BEGIN; + +CREATE TABLE metaschema_modules_public.rls_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + session_credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), + sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), + users_table_id uuid NOT NULL DEFAULT uuid_nil(), + + -- + + authenticate text NOT NULL DEFAULT 'authenticate', + authenticate_strict text NOT NULL DEFAULT 'authenticate_strict', + "current_role" text NOT NULL DEFAULT 'current_user', + current_role_id text NOT NULL DEFAULT 'current_user_id', + + -- API routing (configurable per-module) + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + + -- + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT session_credentials_table_fkey FOREIGN KEY (session_credentials_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT sessions_table_fkey FOREIGN KEY (sessions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT users_table_fkey FOREIGN KEY (users_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT pschema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + + -- + CONSTRAINT database_id_uniq UNIQUE(database_id) +); + + +COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.rls_module IS E'@omit'; +COMMENT ON CONSTRAINT session_credentials_table_fkey ON metaschema_modules_public.rls_module IS E'@omit'; +COMMENT ON CONSTRAINT sessions_table_fkey ON metaschema_modules_public.rls_module IS E'@omit'; +COMMENT ON CONSTRAINT users_table_fkey ON metaschema_modules_public.rls_module IS E'@omit'; +CREATE INDEX rls_module_database_id_idx ON metaschema_modules_public.rls_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/secure_table_provision/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/secure_table_provision/table.sql new file mode 100644 index 00000000..3737c975 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/secure_table_provision/table.sql @@ -0,0 +1,75 @@ +-- Deploy schemas/metaschema_modules_public/tables/secure_table_provision/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.secure_table_provision ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + + table_id uuid NOT NULL DEFAULT uuid_nil(), + + table_name text DEFAULT NULL, + + nodes jsonb NOT NULL DEFAULT '[]', + + use_rls boolean NOT NULL DEFAULT true, + + fields jsonb[] NOT NULL DEFAULT '{}', + + grants jsonb NOT NULL DEFAULT '[]', + + policies jsonb NOT NULL DEFAULT '[]', + + out_fields uuid[] DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE +); + +COMMENT ON TABLE metaschema_modules_public.secure_table_provision IS + 'Provisions security, fields, grants, and policies onto a table. Each row can independently: (1) create fields via nodes[] array (supporting multiple Data* modules per row), (2) grant privileges via grants[] array (supporting per-role privilege targeting), (3) create RLS policies via policies[] array (supporting multiple Authz* policies per row). Multiple rows can target the same table to compose different concerns. All three concerns are optional and independent.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.id IS + 'Unique identifier for this provision row.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.database_id IS + 'The database this provision belongs to. Required.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.schema_id IS + 'Target schema for the table. Defaults to uuid_nil(); the trigger resolves this to the app_public schema if not explicitly provided.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.table_id IS + 'Target table to provision. Defaults to uuid_nil(); the trigger creates or resolves the table via table_name if not explicitly provided.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.table_name IS + 'Name of the target table. Used to create or look up the table when table_id is not provided. If omitted, it is backfilled from the resolved table.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.nodes IS + 'Array of node objects to apply to the table. Each element is a jsonb object with a required "$type" key (one of: DataId, DataDirectOwner, DataEntityMembership, DataOwnershipInEntity, DataTimestamps, DataPeoplestamps, DataPublishable, DataSoftDelete, DataEmbedding, DataFullTextSearch, DataSlug, etc.) and an optional "data" key containing generator-specific configuration. Supports multiple nodes per row, matching the blueprint definition format. Example: [{"$type": "DataId"}, {"$type": "DataTimestamps"}, {"$type": "DataDirectOwner", "data": {"owner_field_name": "author_id"}}]. Defaults to ''[]'' (no node processing).'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.use_rls IS + 'If true and Row Level Security is not yet enabled on the target table, enable it. Automatically set to true by the trigger when policies[] is non-empty. Defaults to true.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.fields IS + 'PostgreSQL array of jsonb field definition objects to create on the target table. Each object has keys: "name" (text, required), "type" (text, required), "default" (text, optional), "is_required" (boolean, optional, defaults to false), "min" (float, optional), "max" (float, optional), "regexp" (text, optional), "index" (boolean, optional, defaults to false — creates a btree index on the field). min/max generate CHECK constraints: for text/citext they constrain character_length, for integer/float types they constrain the value. regexp generates a CHECK (col ~ pattern) constraint for text/citext. Fields are created via metaschema.create_field() after any node_type generator runs, and their IDs are appended to out_fields. Example: ARRAY[''{"name":"username","type":"citext","max":256,"regexp":"^[a-z0-9_]+$"}''::jsonb, ''{"name":"score","type":"integer","min":0,"max":100}''::jsonb]. Defaults to ''{}'' (no additional fields).'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.grants IS + 'Array of grant objects defining table privileges. Each element is a jsonb object with keys: "roles" (text[], required — database roles to grant to, e.g. ["authenticated","admin"]), "privileges" (jsonb[], required — array of [privilege, columns] tuples, e.g. [["select","*"],["insert","*"]]). "*" means all columns; an array means column-level grant. Supports per-role privilege targeting: different grant entries can target different roles with different privileges. Example: [{"roles":["authenticated"],"privileges":[["select","*"]]},{"roles":["admin"],"privileges":[["insert","*"],["update","*"],["delete","*"]]}]. Defaults to ''[]'' (no grants). When policies[] omit explicit privileges/policy_role, they fall back to the verbs and first role from grants[].'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.policies IS + 'Array of policy objects to create on the target table. Each element is a jsonb object with keys: "$type" (text, required — the Authz* policy generator type, e.g. AuthzEntityMembership, AuthzMembership, AuthzDirectOwner, AuthzPublishable, AuthzAllowAll), "data" (jsonb, optional — opaque configuration passed to metaschema.create_policy(), structure varies by type), "privileges" (text[], optional — privileges the policy applies to, e.g. ["select","insert"]; if omitted, derived from grants[] privilege verbs), "policy_role" (text, optional — role the policy targets; if omitted, falls back to first role in first grants[] entry, or ''authenticated'' if no grants), "permissive" (boolean, optional — PERMISSIVE or RESTRICTIVE; defaults to true), "policy_name" (text, optional — custom suffix for the generated policy name; if omitted, auto-derived from $type by stripping Authz prefix). Supports multiple policies per row. Example: [{"$type": "AuthzEntityMembership", "data": {"entity_field": "owner_id", "membership_type": 3}, "privileges": ["select", "insert"]}, {"$type": "AuthzDirectOwner", "data": {"entity_field": "actor_id"}, "privileges": ["update", "delete"]}]. Defaults to ''[]'' (no policies created). When non-empty, the trigger automatically enables RLS.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.out_fields IS + 'Output column populated by the trigger after field creation. Contains the UUIDs of the metaschema fields created on the target table by this provision row''s nodes. NULL when nodes is empty or before the trigger runs. Callers should not set this directly.'; + + +CREATE INDEX secure_table_provision_database_id_idx ON metaschema_modules_public.secure_table_provision ( database_id ); +CREATE INDEX secure_table_provision_table_id_idx ON metaschema_modules_public.secure_table_provision ( table_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/session_secrets_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/session_secrets_module/table.sql new file mode 100644 index 00000000..fac26eba --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/session_secrets_module/table.sql @@ -0,0 +1,31 @@ +-- Deploy schemas/metaschema_modules_public/tables/session_secrets_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.session_secrets_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + -- + schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT 'session_secrets', + -- + sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), + -- + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT sessions_table_fkey FOREIGN KEY (sessions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX session_secrets_module_database_id_idx ON metaschema_modules_public.session_secrets_module ( database_id ); +CREATE INDEX session_secrets_module_schema_id_idx ON metaschema_modules_public.session_secrets_module ( schema_id ); +CREATE INDEX session_secrets_module_table_id_idx ON metaschema_modules_public.session_secrets_module ( table_id ); +CREATE INDEX session_secrets_module_sessions_table_id_idx ON metaschema_modules_public.session_secrets_module ( sessions_table_id ); + +COMMENT ON TABLE metaschema_modules_public.session_secrets_module IS 'Config row for the session_secrets_module, which provisions a DB-private, session-scoped ephemeral key-value store for challenges, nonces, and one-time tokens that must never be readable by end users.'; +COMMENT ON COLUMN metaschema_modules_public.session_secrets_module.sessions_table_id IS 'Resolved reference to sessions_module.sessions_table, used to FK session_secrets.session_id with ON DELETE CASCADE.'; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/sessions_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/sessions_module/table.sql new file mode 100644 index 00000000..c1ffb797 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/sessions_module/table.sql @@ -0,0 +1,41 @@ +-- Deploy schemas/metaschema_modules_public/tables/sessions_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.sessions_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- + schema_id uuid NOT NULL DEFAULT uuid_nil(), + sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), + session_credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), + auth_settings_table_id uuid NOT NULL DEFAULT uuid_nil(), + users_table_id uuid NOT NULL DEFAULT uuid_nil(), + + sessions_default_expiration interval NOT NULL DEFAULT '30 days'::interval, + sessions_table text NOT NULL DEFAULT 'sessions', + session_credentials_table text NOT NULL DEFAULT 'session_credentials', + auth_settings_table text NOT NULL DEFAULT 'app_settings_auth', + -- + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT sessions_table_fkey FOREIGN KEY (sessions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT session_credentials_table_fkey FOREIGN KEY (session_credentials_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT auth_settings_table_fkey FOREIGN KEY (auth_settings_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT users_table_fkey FOREIGN KEY (users_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX sessions_module_database_id_idx ON metaschema_modules_public.sessions_module ( database_id ); + +COMMENT ON CONSTRAINT sessions_table_fkey + ON metaschema_modules_public.sessions_module IS E'@fieldName sessionsTableBySessionsTableId'; +COMMENT ON CONSTRAINT session_credentials_table_fkey + ON metaschema_modules_public.sessions_module IS E'@fieldName sessionCredentialsTableBySessionCredentialsTableId'; +COMMENT ON CONSTRAINT auth_settings_table_fkey + ON metaschema_modules_public.sessions_module IS E'@fieldName authSettingsTableByAuthSettingsTableId'; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/storage_log_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/storage_log_module/table.sql new file mode 100644 index 00000000..e11ecd67 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/storage_log_module/table.sql @@ -0,0 +1,49 @@ +-- Deploy schemas/metaschema_modules_public/tables/storage_log_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.storage_log_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Storage log table (partitioned by snapshot_at) + storage_log_table_id uuid NOT NULL DEFAULT uuid_nil(), + storage_log_table_name text NOT NULL DEFAULT '', + + -- Pre-aggregated daily rollup table + usage_daily_table_id uuid NOT NULL DEFAULT uuid_nil(), + usage_daily_table_name text NOT NULL DEFAULT '', + + -- Partition lifecycle configuration + "interval" text NOT NULL DEFAULT '1 month', + retention text NOT NULL DEFAULT '12 months', + premake int NOT NULL DEFAULT 2, + + -- Scope configuration: 'app' = per-app usage (actor_id RLS) + scope text NOT NULL DEFAULT 'app', + actor_fk_table_id uuid NULL, + entity_fk_table_id uuid NULL, + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- API routing (configurable per-module) + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT storage_log_table_fkey FOREIGN KEY (storage_log_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT usage_daily_table_fkey FOREIGN KEY (usage_daily_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT storage_log_module_database_id_prefix_unique UNIQUE NULLS NOT DISTINCT (database_id, prefix) +); + +CREATE INDEX storage_log_module_database_id_idx ON metaschema_modules_public.storage_log_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/storage_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/storage_module/table.sql new file mode 100644 index 00000000..52c907ca --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/storage_module/table.sql @@ -0,0 +1,113 @@ +-- Deploy schemas/metaschema_modules_public/tables/storage_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.storage_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- Schema references + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Generated table IDs (populated by the generator) + buckets_table_id uuid NOT NULL DEFAULT uuid_nil(), + files_table_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Table names (input to the generator) + buckets_table_name text NOT NULL DEFAULT 'buckets', + files_table_name text NOT NULL DEFAULT 'files', + + -- Scope: determines the security level for this module instance. + -- Resolved to a membership_type integer at trigger time via membership_types table. + scope text NOT NULL DEFAULT 'app', + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + -- Override to create multiple module instances at the same scope. + prefix text NOT NULL DEFAULT '', + + -- Configurable security policies (NULL = use defaults based on scope). + -- When provided, replaces the default policy set in apply_storage_security. + -- Accepts a JSON array of policy objects: + -- {"$type": "AuthzEntityMembership", "privileges": ["select", "update"], "data": {...}} + policies jsonb NULL, + + -- Per-table provisions overrides from blueprint config. + -- Keys are table keys (files, buckets). + -- When a key is present, the module trigger skips default security for that table; + -- secure_table_provision applies the custom grants/policies instead. + provisions jsonb NULL, + + -- Entity table for RLS (NULL for app-level storage, entity table for entity-scoped storage) + entity_table_id uuid NULL, + + -- S3 connection config (NULL = use global env/plugin defaults) + endpoint text NULL, -- S3-compatible API endpoint URL (MinIO, R2, DO Spaces, GCS, etc.) + public_url_prefix text NULL, -- Public URL prefix for generating download URLs (e.g., CDN domain) + provider text NULL, -- Storage provider type: 'minio', 's3', 'gcs', etc. + + -- CORS configuration (NULL = use plugin defaults) + allowed_origins text[] NULL, -- Default CORS origins for all buckets in this database (e.g., ARRAY['https://app.example.com']). ['*'] = open/CDN mode. + + -- Storage permissions: when true, SELECT on files requires read_files permission + -- (opt-in restrictive mode for sensitive entity types like data rooms with confidential docs). + -- When false (default), any entity member can read all files (baseline = membership). + restrict_reads boolean NOT NULL DEFAULT false, + + -- Virtual filesystem + path shares: when true, generates the ltree path column + -- on files, the file_path_shares table, and path share RLS policies. + -- Enables folder hierarchy, per-folder/file sharing, and version chains. + has_path_shares boolean NOT NULL DEFAULT false, + + -- Generated table ID for file_path_shares (populated by the generator when has_path_shares=true) + path_shares_table_id uuid NULL DEFAULT NULL, + + -- Per-database configurable settings (NULL = use plugin defaults) + upload_url_expiry_seconds integer NULL, -- Presigned PUT URL expiry (default: 900 = 15 min) + download_url_expiry_seconds integer NULL, -- Presigned GET URL expiry (default: 3600 = 1 hour) + default_max_file_size bigint NULL, -- Global max file size in bytes (default: 200MB). Bucket-level overrides this. + max_filename_length integer NULL, -- Max filename length in chars (default: 1024) + cache_ttl_seconds integer NULL, -- LRU cache TTL for this config (default: 300 dev / 3600 prod) + + -- Bulk upload limits (NULL = use plugin defaults) + max_bulk_files integer NULL, -- Max files per requestBulkUploadUrls batch (default: 100) + max_bulk_total_size bigint NULL, -- Max total size per batch in bytes (default: 1GB = 1073741824) + + -- Feature flags: toggleable storage capabilities (all default false for minimal footprint) + has_versioning boolean NOT NULL DEFAULT false, -- Version chains: previous_version_id, is_latest, version_history() + has_content_hash boolean NOT NULL DEFAULT false, -- Content hash column for dedup + integrity verification + has_custom_keys boolean NOT NULL DEFAULT false, -- allow_custom_keys on buckets (implies has_versioning + has_content_hash) + has_audit_log boolean NOT NULL DEFAULT false, -- File events audit table: upload, delete, move, rename, download, share events + has_confirm_upload boolean NOT NULL DEFAULT false, -- Deferred HeadObject confirmation: enqueues storage:confirm_upload job on INSERT, creates status transition functions + confirm_upload_delay interval NOT NULL DEFAULT '30 seconds', -- Delay before first confirmation attempt (only used when has_confirm_upload = true) + + -- Generated table ID for file_events (populated by the generator when has_audit_log=true) + file_events_table_id uuid NULL DEFAULT NULL, + + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + + -- Constraints + -- API routing (configurable per-module) + api_name text DEFAULT 'admin', + private_api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT buckets_table_fkey FOREIGN KEY (buckets_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT files_table_fkey FOREIGN KEY (files_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT entity_table_fkey FOREIGN KEY (entity_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT path_shares_table_fkey FOREIGN KEY (path_shares_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT file_events_table_fkey FOREIGN KEY (file_events_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX storage_module_database_id_idx ON metaschema_modules_public.storage_module ( database_id ); + +-- Unique constraint: one storage module per database per scope per prefix. +CREATE UNIQUE INDEX storage_module_unique_scope ON metaschema_modules_public.storage_module ( database_id, scope, prefix ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/transfer_log_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/transfer_log_module/table.sql new file mode 100644 index 00000000..05d1a9c7 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/transfer_log_module/table.sql @@ -0,0 +1,49 @@ +-- Deploy schemas/metaschema_modules_public/tables/transfer_log_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.transfer_log_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Transfer log table (partitioned by created_at) + transfer_log_table_id uuid NOT NULL DEFAULT uuid_nil(), + transfer_log_table_name text NOT NULL DEFAULT '', + + -- Pre-aggregated daily rollup table + usage_daily_table_id uuid NOT NULL DEFAULT uuid_nil(), + usage_daily_table_name text NOT NULL DEFAULT '', + + -- Partition lifecycle configuration + "interval" text NOT NULL DEFAULT '1 month', + retention text NOT NULL DEFAULT '12 months', + premake int NOT NULL DEFAULT 2, + + -- Scope configuration: 'app' = per-app usage (actor_id RLS) + scope text NOT NULL DEFAULT 'app', + actor_fk_table_id uuid NULL, + entity_fk_table_id uuid NULL, + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- API routing (configurable per-module) + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT transfer_log_table_fkey FOREIGN KEY (transfer_log_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT usage_daily_table_fkey FOREIGN KEY (usage_daily_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT transfer_log_module_database_id_prefix_unique UNIQUE NULLS NOT DISTINCT (database_id, prefix) +); + +CREATE INDEX transfer_log_module_database_id_idx ON metaschema_modules_public.transfer_log_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_auth_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_auth_module/table.sql new file mode 100644 index 00000000..3d49988d --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_auth_module/table.sql @@ -0,0 +1,80 @@ +-- Deploy schemas/metaschema_modules_public/tables/user_auth_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.user_auth_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + emails_table_id uuid NOT NULL DEFAULT uuid_nil(), + users_table_id uuid NOT NULL DEFAULT uuid_nil(), + secrets_table_id uuid NOT NULL DEFAULT uuid_nil(), + encrypted_table_id uuid NOT NULL DEFAULT uuid_nil(), + -- TOKENS_REMOVAL: tokens_table_id removed - all auth now uses sessions_module + -- SESSION_MIGRATION: sessions and session_credentials for session-centric auth + sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), + session_credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), + + audits_table_id uuid NOT NULL DEFAULT uuid_nil(), + audits_table_name text NOT NULL DEFAULT 'audit_log_auth', + + -- api_id uuid NOT NULL REFERENCES services_public.apis (id), + + sign_in_function text NOT NULL DEFAULT 'sign_in', + sign_up_function text NOT NULL DEFAULT 'sign_up', + sign_out_function text NOT NULL DEFAULT 'sign_out', + set_password_function text NOT NULL DEFAULT 'set_password', + reset_password_function text NOT NULL DEFAULT 'reset_password', + forgot_password_function text NOT NULL DEFAULT 'forgot_password', + send_verification_email_function text NOT NULL DEFAULT 'send_verification_email', + verify_email_function text NOT NULL DEFAULT 'verify_email', + + verify_password_function text NOT NULL DEFAULT 'verify_password', + check_password_function text NOT NULL DEFAULT 'check_password', + + send_account_deletion_email_function text NOT NULL DEFAULT 'send_account_deletion_email', + delete_account_function text NOT NULL DEFAULT 'confirm_delete_account', + + sign_in_cross_origin_function text NOT NULL DEFAULT 'sign_in_cross_origin', + request_cross_origin_token_function text NOT NULL DEFAULT 'request_cross_origin_token', + extend_token_expires text NOT NULL DEFAULT 'extend_token_expires', + + -- UNIQUE(api_id), + + -- API routing (configurable per-module) + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT email_table_fkey FOREIGN KEY (emails_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT users_table_fkey FOREIGN KEY (users_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT secrets_table_fkey FOREIGN KEY (secrets_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT encrypted_table_fkey FOREIGN KEY (encrypted_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + -- TOKENS_REMOVAL: tokens_table_fkey removed - all auth now uses sessions_module + -- SESSION_MIGRATION: foreign keys for sessions and session_credentials + CONSTRAINT sessions_table_fkey FOREIGN KEY (sessions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT session_credentials_table_fkey FOREIGN KEY (session_credentials_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX user_auth_module_database_id_idx ON metaschema_modules_public.user_auth_module ( database_id ); + +COMMENT ON CONSTRAINT email_table_fkey + ON metaschema_modules_public.user_auth_module IS E'@omit'; +COMMENT ON CONSTRAINT users_table_fkey + ON metaschema_modules_public.user_auth_module IS E'@omit'; +COMMENT ON CONSTRAINT secrets_table_fkey + ON metaschema_modules_public.user_auth_module IS E'@omit'; +COMMENT ON CONSTRAINT encrypted_table_fkey + ON metaschema_modules_public.user_auth_module IS E'@omit'; +-- TOKENS_REMOVAL: tokens_table_fkey comment removed +-- SESSION_MIGRATION: omit comments for sessions and session_credentials foreign keys +COMMENT ON CONSTRAINT sessions_table_fkey + ON metaschema_modules_public.user_auth_module IS E'@omit'; +COMMENT ON CONSTRAINT session_credentials_table_fkey + ON metaschema_modules_public.user_auth_module IS E'@omit'; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql new file mode 100644 index 00000000..d5bf3533 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql @@ -0,0 +1,42 @@ +-- Deploy schemas/metaschema_modules_public/tables/user_credentials_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.user_credentials_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- Schema references (resolved by BEFORE INSERT trigger when uuid_nil) + schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Generated table ID (populated by the generator) + table_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Table name (input — defaults to 'user_secrets') + table_name text NOT NULL DEFAULT 'user_secrets', + + -- API routing (get-or-create: if set, schema is added to this API) + api_name text DEFAULT 'config', + private_api_name text DEFAULT NULL, + + -- Constraints + CONSTRAINT user_credentials_module_db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT user_credentials_module_schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT user_credentials_module_table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX user_credentials_module_database_id_idx ON metaschema_modules_public.user_credentials_module ( database_id ); +CREATE INDEX user_credentials_module_schema_id_idx ON metaschema_modules_public.user_credentials_module ( schema_id ); +CREATE INDEX user_credentials_module_table_id_idx ON metaschema_modules_public.user_credentials_module ( table_id ); + +-- One user_credentials_module per database. +CREATE UNIQUE INDEX user_credentials_module_unique ON metaschema_modules_public.user_credentials_module ( database_id ); + +COMMENT ON TABLE metaschema_modules_public.user_credentials_module IS + 'Per-user bcrypt credential store (password hashes, API key hashes). + Always user-scoped with AuthzDirectOwner RLS. Consumed by user_auth_module, + identity_providers_module, and bootstrap procedures.'; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_settings_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_settings_module/table.sql new file mode 100644 index 00000000..5a50044b --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_settings_module/table.sql @@ -0,0 +1,34 @@ +-- Deploy schemas/metaschema_modules_public/tables/user_settings_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.user_settings_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- Schema reference (populated by the insert trigger) + schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Table reference (populated by the generator) + table_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Owner table reference (resolved to users table by trigger) + owner_table_id uuid NOT NULL DEFAULT uuid_nil(), + + table_name text NOT NULL DEFAULT 'user_settings', + + -- API routing (configurable per-module) + api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT owner_table_fkey FOREIGN KEY (owner_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX user_settings_module_database_id_idx ON metaschema_modules_public.user_settings_module ( database_id ); +CREATE UNIQUE INDEX user_settings_module_unique_per_db ON metaschema_modules_public.user_settings_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_state_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_state_module/table.sql new file mode 100644 index 00000000..521f5dce --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_state_module/table.sql @@ -0,0 +1,23 @@ +-- Deploy schemas/metaschema_modules_public/tables/user_state_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.user_state_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + -- + schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT 'user_state', + -- + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE + +); + +CREATE INDEX user_state_module_database_id_idx ON metaschema_modules_public.user_state_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/users_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/users_module/table.sql new file mode 100644 index 00000000..8a9a727b --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/users_module/table.sql @@ -0,0 +1,33 @@ +-- Deploy schemas/metaschema_modules_public/tables/users_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.users_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + -- + schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT 'users', + -- + + -- + type_table_id uuid NOT NULL DEFAULT uuid_nil(), + type_table_name text NOT NULL DEFAULT 'role_types', + -- + + -- API routing (configurable per-module) + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT type_table_fkey FOREIGN KEY (type_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX users_module_database_id_idx ON metaschema_modules_public.users_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/webauthn_auth_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/webauthn_auth_module/table.sql new file mode 100644 index 00000000..609f3c96 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/webauthn_auth_module/table.sql @@ -0,0 +1,63 @@ +-- Deploy schemas/metaschema_modules_public/tables/webauthn_auth_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +-- WebAuthn / Passkey auth module config. +-- Sibling of crypto_auth_module. Registers a `webauthn_challenge` entry in +-- services_public.api_modules for every authenticated API in the database, +-- so the Node relying-party (using @simplewebauthn/server) knows where to +-- read and write passkey challenges + where the credentials table lives. +-- +-- RP config (rp_id, rp_name, origin_allowlist, attestation_type, +-- require_user_verification, resident_key) lives on this row (not on +-- app_auth_settings) because RP identity varies per deployment, not per +-- user choice. +-- +-- All fields default to safe passwordless-passkey values per SimpleWebAuthn's +-- consumer guidance: attestation_type='none', require_user_verification=false, +-- resident_key='required'. +CREATE TABLE metaschema_modules_public.webauthn_auth_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + + users_table_id uuid NOT NULL DEFAULT uuid_nil(), + credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), + sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), + session_credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), + session_secrets_table_id uuid NOT NULL DEFAULT uuid_nil(), + auth_settings_table_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Relying Party config. Empty defaults require the caller to populate + -- via UPDATE / ALTER on the row before the first registration. + rp_id text NOT NULL DEFAULT '', + rp_name text NOT NULL DEFAULT '', + origin_allowlist text[] NOT NULL DEFAULT '{}', + + -- Consumer defaults per SimpleWebAuthn passkey guidance. + attestation_type text NOT NULL DEFAULT 'none' + CHECK (attestation_type IN ('none', 'indirect', 'direct', 'enterprise')), + require_user_verification boolean NOT NULL DEFAULT false, + resident_key text NOT NULL DEFAULT 'required' + CHECK (resident_key IN ('discouraged', 'preferred', 'required')), + + -- Challenge TTL (mirrors mfa_challenge_expiry on app_auth_settings). + -- 5 minutes matches SimpleWebAuthn's recommended cookie-based TTL. + challenge_expiry interval NOT NULL DEFAULT '5 minutes', + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT users_table_fkey FOREIGN KEY (users_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT credentials_table_fkey FOREIGN KEY (credentials_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT sessions_table_fkey FOREIGN KEY (sessions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT session_credentials_table_fkey FOREIGN KEY (session_credentials_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT session_secrets_table_fkey FOREIGN KEY (session_secrets_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT auth_settings_table_fkey FOREIGN KEY (auth_settings_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX webauthn_auth_module_database_id_idx ON metaschema_modules_public.webauthn_auth_module ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/webauthn_credentials_module/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/webauthn_credentials_module/table.sql new file mode 100644 index 00000000..f540f4b4 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/webauthn_credentials_module/table.sql @@ -0,0 +1,36 @@ +-- Deploy schemas/metaschema_modules_public/tables/webauthn_credentials_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.webauthn_credentials_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + table_id uuid NOT NULL DEFAULT uuid_nil(), + owner_table_id uuid NOT NULL DEFAULT uuid_nil(), + + table_name text NOT NULL DEFAULT 'webauthn_credentials', + + -- API routing (configurable per-module) + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + + -- + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT owner_table_fkey FOREIGN KEY (owner_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE +); + +CREATE INDEX webauthn_credentials_module_database_id_idx ON metaschema_modules_public.webauthn_credentials_module ( database_id ); + +COMMENT ON TABLE metaschema_modules_public.webauthn_credentials_module IS 'Config row for the webauthn_credentials_module, which provisions the per-user WebAuthn/passkey credentials table (public key, counter, transports, device type, backup state) mirroring crypto_addresses_module. The sibling webauthn_auth_module holds RP config and the registration/sign-in challenge state.'; +COMMENT ON COLUMN metaschema_modules_public.webauthn_credentials_module.private_schema_id IS 'Private schema that hosts SECURITY DEFINER helpers which write to webauthn_credentials (registration / counter-bump / delete).'; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/services_private/schema.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/services_private/schema.sql new file mode 100644 index 00000000..36722a2a --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/services_private/schema.sql @@ -0,0 +1,2 @@ +-- Deploy schemas/services_private/schema to pg + diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/services_public/schema.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/services_public/schema.sql new file mode 100644 index 00000000..be8d3e9e --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/services_public/schema.sql @@ -0,0 +1,2 @@ +-- Deploy schemas/services_public/schema to pg + diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/services_public/tables/apis/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/services_public/tables/apis/table.sql new file mode 100644 index 00000000..038b8b00 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/services_public/tables/apis/table.sql @@ -0,0 +1,3 @@ +-- Deploy schemas/services_public/tables/apis/table to pg + +-- requires: schemas/services_public/schema diff --git a/extensions/@pgpm/metaschema-modules/deploy/schemas/services_public/tables/sites/table.sql b/extensions/@pgpm/metaschema-modules/deploy/schemas/services_public/tables/sites/table.sql new file mode 100644 index 00000000..52ee3dd3 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/deploy/schemas/services_public/tables/sites/table.sql @@ -0,0 +1,3 @@ +-- Deploy schemas/services_public/tables/sites/table to pg + +-- requires: schemas/services_public/schema diff --git a/extensions/@pgpm/metaschema-modules/metaschema-modules.control b/extensions/@pgpm/metaschema-modules/metaschema-modules.control new file mode 100644 index 00000000..f7dd5703 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/metaschema-modules.control @@ -0,0 +1,7 @@ +# metaschema-modules extension +comment = 'metaschema-modules extension' +default_version = '0.26.3' +module_pathname = '$libdir/metaschema-modules' +requires = 'plpgsql,uuid-ossp,metaschema-schema,services,pgpm-verify' +relocatable = false +superuser = false diff --git a/extensions/@pgpm/metaschema-modules/package.json b/extensions/@pgpm/metaschema-modules/package.json new file mode 100644 index 00000000..b34e78fe --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/package.json @@ -0,0 +1,39 @@ +{ + "name": "@pgpm/metaschema-modules", + "version": "0.26.5", + "description": "Module metadata handling and dependency tracking", + "author": "Dan Lynch ", + "contributors": [ + "Constructive " + ], + "keywords": [ + "postgresql", + "pgpm", + "metadata", + "modules" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "bundle": "pgpm package", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@pgpm/metaschema-schema": "0.26.5", + "@pgpm/verify": "0.26.0" + }, + "devDependencies": { + "pgpm": "^4.23.2" + }, + "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" + }, + "gitHead": "a496a00d89c37d874f4a7207265b9972b6f05c7d" +} diff --git a/extensions/@pgpm/metaschema-modules/pgpm.plan b/extensions/@pgpm/metaschema-modules/pgpm.plan new file mode 100644 index 00000000..ba706dd3 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/pgpm.plan @@ -0,0 +1,67 @@ +%syntax-version=1.0.0 +%project=metaschema-modules +%uri=metaschema-modules + +schemas/services_private/schema [services:schemas/services_public/tables/site_themes/table] 2017-08-11T08:11:51Z skitch # add schemas/services_private/schema +schemas/services_public/schema 2017-08-11T08:11:51Z skitch # add schemas/services_public/schema +schemas/metaschema_modules_public/schema 2026-01-04T08:28:00Z devin # add schemas/metaschema_modules_public/schema +schemas/services_public/tables/apis/table [schemas/services_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/services_public/tables/apis/table +schemas/metaschema_modules_public/tables/connected_accounts_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/connected_accounts_module/table +schemas/metaschema_modules_public/tables/crypto_addresses_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/crypto_addresses_module/table +schemas/metaschema_modules_public/tables/crypto_auth_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/crypto_auth_module/table +schemas/metaschema_modules_public/tables/default_ids_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/default_ids_module/table +schemas/metaschema_modules_public/tables/denormalized_table_field/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/denormalized_table_field/table +schemas/metaschema_modules_public/tables/emails_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/emails_module/table +schemas/metaschema_modules_public/tables/config_secrets_user_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/config_secrets_user_module/table +schemas/metaschema_modules_public/tables/invites_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/invites_module/table +schemas/metaschema_modules_public/tables/events_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/events_module/table +schemas/metaschema_modules_public/tables/limits_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/limits_module/table +schemas/metaschema_modules_public/tables/membership_types_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/membership_types_module/table +schemas/metaschema_modules_public/tables/memberships_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/memberships_module/table +schemas/metaschema_modules_public/tables/permissions_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/permissions_module/table +schemas/metaschema_modules_public/tables/phone_numbers_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/phone_numbers_module/table +schemas/metaschema_modules_public/tables/profiles_module/table [schemas/metaschema_modules_public/schema] 2026-01-01T00:00:00Z devin # add schemas/metaschema_modules_public/tables/profiles_module/table +schemas/metaschema_modules_public/tables/rls_module/table [schemas/metaschema_modules_public/schema schemas/services_public/tables/apis/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/rls_module/table +schemas/metaschema_modules_public/tables/user_state_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/user_state_module/table +schemas/services_public/tables/sites/table [schemas/services_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/services_public/tables/sites/table +schemas/metaschema_modules_public/tables/sessions_module/table [schemas/metaschema_modules_public/schema] 2026-01-24T00:00:00Z devin # add schemas/metaschema_modules_public/tables/sessions_module/table +schemas/metaschema_modules_public/tables/user_auth_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/user_auth_module/table +schemas/metaschema_modules_public/tables/users_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/users_module/table + +schemas/metaschema_modules_public/tables/hierarchy_module/table [schemas/metaschema_modules_public/schema] 2024-12-28T00:00:00Z skitch # add schemas/metaschema_modules_public/tables/hierarchy_module/table +schemas/metaschema_modules_public/tables/secure_table_provision/table [schemas/metaschema_modules_public/schema] 2026-02-25T00:00:00Z Constructive # add schemas/metaschema_modules_public/tables/secure_table_provision/table +schemas/metaschema_modules_public/tables/relation_provision/table [schemas/metaschema_modules_public/schema] 2026-02-26T00:00:00Z Constructive # add schemas/metaschema_modules_public/tables/relation_provision/table +schemas/metaschema_modules_public/tables/blueprint_template/table [schemas/metaschema_modules_public/schema] 2026-03-20T00:00:00Z Constructive # add blueprint_template table for shareable schema recipes +schemas/metaschema_modules_public/tables/blueprint/table [schemas/metaschema_modules_public/schema schemas/metaschema_modules_public/tables/blueprint_template/table] 2026-03-20T00:00:01Z Constructive # add blueprint table for owned executable blueprints +schemas/metaschema_modules_public/tables/blueprint_construction/table [schemas/metaschema_modules_public/schema schemas/metaschema_modules_public/tables/blueprint/table] 2026-03-31T00:00:00Z Constructive # add blueprint_construction table for construction state tracking +schemas/metaschema_modules_public/tables/storage_module/table [schemas/metaschema_modules_public/schema] 2026-03-24T00:00:00Z devin # add storage_module config table for files and buckets +schemas/metaschema_modules_public/tables/entity_type_provision/table [schemas/metaschema_modules_public/schema] 2026-04-11T00:00:00Z Constructive # add entity_type_provision table for dynamic membership type provisioning + +schemas/metaschema_modules_public/tables/rate_limits_module/table [schemas/metaschema_modules_public/schema] 2026-04-15T00:00:00Z devin # add rate_limits_module for centralized throttle configuration +schemas/metaschema_modules_public/tables/devices_module/table [schemas/metaschema_modules_public/schema] 2026-04-16T00:00:00Z devin # add devices_module for trusted device tracking and recognition +schemas/metaschema_modules_public/tables/session_secrets_module/table [schemas/metaschema_modules_public/schema] 2026-04-17T00:00:00Z devin # add session_secrets_module config table for DB-private session-scoped ephemeral store +schemas/metaschema_modules_public/tables/webauthn_credentials_module/table [schemas/metaschema_modules_public/schema] 2026-04-18T00:00:00Z devin # add webauthn_credentials_module config table for WebAuthn/passkey credentials (phase 11a) +schemas/metaschema_modules_public/tables/webauthn_auth_module/table [schemas/metaschema_modules_public/schema] 2026-04-18T01:00:00Z devin # add webauthn_auth_module config table for WebAuthn/passkey RP config + api_modules publishing (phase 11b) +schemas/metaschema_modules_public/tables/identity_providers_module/table [schemas/metaschema_modules_public/schema] 2026-04-19T00:00:00Z devin # add identity_providers_module config table for OAuth2/OIDC custom identity providers (phase 12a) +schemas/metaschema_modules_public/tables/notifications_module/table [schemas/metaschema_modules_public/schema] 2026-04-19T10:00:00Z devin # add notifications_module config table for v2 notifications (events + scoped inbox) +schemas/metaschema_modules_public/tables/plans_module/table [schemas/metaschema_modules_public/schema] 2026-05-02T23:30:00Z devin # add plans_module config table for plan tiers and plan_limits +schemas/metaschema_modules_public/tables/billing_module/table [schemas/metaschema_modules_public/schema] 2026-05-02T23:45:00Z devin # add billing_module config table for meters, plan_subscriptions, ledger, and balances +schemas/metaschema_modules_public/tables/billing_provider_module/table [schemas/metaschema_modules_public/schema] 2026-05-03T01:00:00Z devin # add billing_provider_module config table for external billing provider integration +schemas/metaschema_modules_public/tables/realtime_module/table [schemas/metaschema_modules_public/schema] 2026-05-09T10:00:00Z devin # add realtime_module config table for real-time subscription infrastructure +schemas/metaschema_modules_public/tables/rate_limit_meters_module/table [schemas/metaschema_modules_public/schema] 2026-05-16T00:00:00Z devin # add rate_limit_meters_module for rolling window abuse protection (standalone rate limiting) +schemas/metaschema_modules_public/tables/config_secrets_org_module/table [schemas/metaschema_modules_public/schema] 2026-05-18T00:00:00Z devin # add config_secrets_org_module config table for org-scoped encrypted secrets +schemas/metaschema_modules_public/tables/inference_log_module/table [schemas/metaschema_modules_public/schema] 2026-05-12T23:00:00Z devin # add inference_log_module config table for partitioned LLM inference logging +schemas/metaschema_modules_public/tables/compute_log_module/table [schemas/metaschema_modules_public/schema] 2026-05-18T20:00:00Z devin # add compute_log_module config table for partitioned compute usage logging +schemas/metaschema_modules_public/tables/transfer_log_module/table [schemas/metaschema_modules_public/schema] 2026-05-18T21:00:00Z devin # add transfer_log_module config table for partitioned transfer/bandwidth logging +schemas/metaschema_modules_public/tables/storage_log_module/table [schemas/metaschema_modules_public/schema] 2026-05-18T21:00:01Z devin # add storage_log_module config table for partitioned object storage usage logging +schemas/metaschema_modules_public/tables/db_usage_module/table [schemas/metaschema_modules_public/schema] 2026-05-18T21:00:02Z devin # add db_usage_module config table for partitioned database-level usage metrics +schemas/metaschema_modules_public/tables/agent_module/table [schemas/metaschema_modules_public/schema] 2026-05-12T23:01:00Z devin # add agent_module config table for AI agent conversation threads, messages, and tasks +schemas/metaschema_modules_public/tables/merkle_store_module/table [schemas/metaschema_modules_public/schema] 2026-05-21T00:00:00Z devin # add merkle_store_module config table for content-addressed Merkle object stores +schemas/metaschema_modules_public/tables/graph_module/table [schemas/metaschema_modules_public/schema schemas/metaschema_modules_public/tables/merkle_store_module/table] 2026-05-21T01:00:00Z devin # add graph_module config table for FBP graph utilities on top of merkle store +schemas/metaschema_modules_public/tables/namespace_module/table [schemas/metaschema_modules_public/schema] 2026-05-21T02:00:00Z devin # add namespace_module config table for entity-aware namespace provisioning +schemas/metaschema_modules_public/tables/function_module/table [schemas/metaschema_modules_public/schema] 2026-05-21T03:00:00Z devin # add function_module config table for entity-aware function definitions +schemas/metaschema_modules_public/tables/config_secrets_module/table [schemas/metaschema_modules_public/schema] 2026-05-29T00:00:00Z devin # add entity-aware config_secrets_module (replaces config_secrets_user_module + config_secrets_org_module) +schemas/metaschema_modules_public/tables/user_credentials_module/table [schemas/metaschema_modules_public/schema] 2026-05-30T00:00:00Z devin # add user_credentials_module for per-user bcrypt credential store (split from config_secrets_module) +schemas/metaschema_modules_public/tables/user_settings_module/table [schemas/metaschema_modules_public/schema] 2026-05-28T00:00:00Z devin # add user_settings_module for extensible per-user preferences (1:1 with users) + +schemas/metaschema_modules_public/tables/i18n_module/table [schemas/metaschema_modules_public/schema] 2026-05-28T00:00:00Z devin # add i18n_module config table for internationalization settings diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/schema.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/schema.sql new file mode 100644 index 00000000..3e540bc2 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/schema.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/schema from pg + +BEGIN; + +DROP SCHEMA metaschema_modules_public; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/agent_chat_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/agent_chat_module/table.sql new file mode 100644 index 00000000..c5a40cd9 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/agent_chat_module/table.sql @@ -0,0 +1,3 @@ +-- Revert schemas/metaschema_modules_public/tables/agent_module/table from pg + +DROP TABLE IF EXISTS metaschema_modules_public.agent_module; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/agent_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/agent_module/table.sql new file mode 100644 index 00000000..28c79525 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/agent_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/agent_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.agent_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/billing_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/billing_module/table.sql new file mode 100644 index 00000000..37fce82a --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/billing_module/table.sql @@ -0,0 +1,3 @@ +-- Revert schemas/metaschema_modules_public/tables/billing_module/table from pg + +DROP TABLE IF EXISTS metaschema_modules_public.billing_module; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/billing_provider_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/billing_provider_module/table.sql new file mode 100644 index 00000000..d9a5ed89 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/billing_provider_module/table.sql @@ -0,0 +1,3 @@ +-- Revert schemas/metaschema_modules_public/tables/billing_provider_module/table from pg + +DROP TABLE IF EXISTS metaschema_modules_public.billing_provider_module; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/blueprint/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/blueprint/table.sql new file mode 100644 index 00000000..2c916fd8 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/blueprint/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/blueprint/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_modules_public.blueprint; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/blueprint_construction/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/blueprint_construction/table.sql new file mode 100644 index 00000000..f920d1a5 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/blueprint_construction/table.sql @@ -0,0 +1,3 @@ +-- Revert schemas/metaschema_modules_public/tables/blueprint_construction/table + +DROP TABLE IF EXISTS metaschema_modules_public.blueprint_construction; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/blueprint_template/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/blueprint_template/table.sql new file mode 100644 index 00000000..29c2dd41 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/blueprint_template/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/blueprint_template/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_modules_public.blueprint_template; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/compute_log_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/compute_log_module/table.sql new file mode 100644 index 00000000..2ed455e0 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/compute_log_module/table.sql @@ -0,0 +1,3 @@ +-- Revert schemas/metaschema_modules_public/tables/compute_log_module/table from pg + +DROP TABLE IF EXISTS metaschema_modules_public.compute_log_module; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/config_secrets_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/config_secrets_module/table.sql new file mode 100644 index 00000000..4a00859e --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/config_secrets_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/config_secrets_module/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_modules_public.config_secrets_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql new file mode 100644 index 00000000..d16a1759 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/config_secrets_org_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.config_secrets_org_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql new file mode 100644 index 00000000..fcc2139c --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/config_secrets_user_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.config_secrets_user_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/connected_accounts_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/connected_accounts_module/table.sql new file mode 100644 index 00000000..978964cd --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/connected_accounts_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/connected_accounts_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.connected_accounts_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/crypto_addresses_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/crypto_addresses_module/table.sql new file mode 100644 index 00000000..4504c875 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/crypto_addresses_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/crypto_addresses_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.crypto_addresses_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/crypto_auth_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/crypto_auth_module/table.sql new file mode 100644 index 00000000..34ac69de --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/crypto_auth_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/crypto_auth_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.crypto_auth_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/db_usage_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/db_usage_module/table.sql new file mode 100644 index 00000000..f29203aa --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/db_usage_module/table.sql @@ -0,0 +1,3 @@ +-- Revert schemas/metaschema_modules_public/tables/db_usage_module/table from pg + +DROP TABLE IF EXISTS metaschema_modules_public.db_usage_module; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/default_ids_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/default_ids_module/table.sql new file mode 100644 index 00000000..039f78ba --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/default_ids_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/default_ids_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.default_ids_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/denormalized_table_field/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/denormalized_table_field/table.sql new file mode 100644 index 00000000..c63082a2 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/denormalized_table_field/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/denormalized_table_field/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.denormalized_table_field; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/devices_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/devices_module/table.sql new file mode 100644 index 00000000..5fb0bebf --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/devices_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/devices_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.devices_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/emails_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/emails_module/table.sql new file mode 100644 index 00000000..e88362ba --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/emails_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/emails_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.emails_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/entity_type_provision/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/entity_type_provision/table.sql new file mode 100644 index 00000000..4af43127 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/entity_type_provision/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/entity_type_provision/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.entity_type_provision; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/events_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/events_module/table.sql new file mode 100644 index 00000000..3363a2dd --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/events_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/events_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.events_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/function_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/function_module/table.sql new file mode 100644 index 00000000..7d3f3dee --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/function_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/function_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.function_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/graph_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/graph_module/table.sql new file mode 100644 index 00000000..735fc8f5 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/graph_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/graph_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.graph_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/hierarchy_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/hierarchy_module/table.sql new file mode 100644 index 00000000..07be3976 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/hierarchy_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/hierarchy_module/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_modules_public.hierarchy_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/i18n_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/i18n_module/table.sql new file mode 100644 index 00000000..63e7cdfe --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/i18n_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/i18n_module/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_modules_public.i18n_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/identity_providers_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/identity_providers_module/table.sql new file mode 100644 index 00000000..3f1f3b16 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/identity_providers_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/identity_providers_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.identity_providers_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/inference_log_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/inference_log_module/table.sql new file mode 100644 index 00000000..7824e3c7 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/inference_log_module/table.sql @@ -0,0 +1,3 @@ +-- Revert schemas/metaschema_modules_public/tables/inference_log_module/table from pg + +DROP TABLE IF EXISTS metaschema_modules_public.inference_log_module; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/invites_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/invites_module/table.sql new file mode 100644 index 00000000..9e72640d --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/invites_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/invites_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.invites_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/limits_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/limits_module/table.sql new file mode 100644 index 00000000..cd3d4fcb --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/limits_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/limits_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.limits_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/membership_types_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/membership_types_module/table.sql new file mode 100644 index 00000000..bc50b5b9 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/membership_types_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/membership_types_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.membership_types_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/memberships_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/memberships_module/table.sql new file mode 100644 index 00000000..3f005d09 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/memberships_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/memberships_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.memberships_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/merkle_store_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/merkle_store_module/table.sql new file mode 100644 index 00000000..34afce6f --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/merkle_store_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/merkle_store_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.merkle_store_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/namespace_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/namespace_module/table.sql new file mode 100644 index 00000000..babff77c --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/namespace_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/namespace_module/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_modules_public.namespace_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/notifications_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/notifications_module/table.sql new file mode 100644 index 00000000..0e4167b4 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/notifications_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/notifications_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.notifications_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/permissions_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/permissions_module/table.sql new file mode 100644 index 00000000..42ace72b --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/permissions_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/permissions_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.permissions_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/phone_numbers_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/phone_numbers_module/table.sql new file mode 100644 index 00000000..5c2d6a6c --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/phone_numbers_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/phone_numbers_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.phone_numbers_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/plans_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/plans_module/table.sql new file mode 100644 index 00000000..ce7986a4 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/plans_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/plans_module/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_modules_public.plans_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/profiles_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/profiles_module/table.sql new file mode 100644 index 00000000..6efa3aa9 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/profiles_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/profiles_module/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_modules_public.profiles_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql new file mode 100644 index 00000000..b8d2b674 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/rate_limit_meters_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.rate_limit_meters_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/rate_limits_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/rate_limits_module/table.sql new file mode 100644 index 00000000..9b9015d7 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/rate_limits_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/rate_limits_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.rate_limits_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/realtime_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/realtime_module/table.sql new file mode 100644 index 00000000..b6c11f1a --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/realtime_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/realtime_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.realtime_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/relation_provision/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/relation_provision/table.sql new file mode 100644 index 00000000..d85cc87d --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/relation_provision/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/relation_provision/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_modules_public.relation_provision; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/rls_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/rls_module/table.sql new file mode 100644 index 00000000..0075e359 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/rls_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/rls_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.rls_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/secure_table_provision/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/secure_table_provision/table.sql new file mode 100644 index 00000000..3582dd74 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/secure_table_provision/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/secure_table_provision/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_modules_public.secure_table_provision; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/session_secrets_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/session_secrets_module/table.sql new file mode 100644 index 00000000..fbb4553b --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/session_secrets_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/session_secrets_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.session_secrets_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/sessions_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/sessions_module/table.sql new file mode 100644 index 00000000..7a945fd5 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/sessions_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/sessions_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.sessions_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/storage_log_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/storage_log_module/table.sql new file mode 100644 index 00000000..174bd954 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/storage_log_module/table.sql @@ -0,0 +1,3 @@ +-- Revert schemas/metaschema_modules_public/tables/storage_log_module/table from pg + +DROP TABLE IF EXISTS metaschema_modules_public.storage_log_module; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/storage_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/storage_module/table.sql new file mode 100644 index 00000000..e0b6d0f3 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/storage_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/storage_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.storage_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/transfer_log_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/transfer_log_module/table.sql new file mode 100644 index 00000000..d0299682 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/transfer_log_module/table.sql @@ -0,0 +1,3 @@ +-- Revert schemas/metaschema_modules_public/tables/transfer_log_module/table from pg + +DROP TABLE IF EXISTS metaschema_modules_public.transfer_log_module; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_auth_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_auth_module/table.sql new file mode 100644 index 00000000..aa22c602 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_auth_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/user_auth_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.user_auth_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql new file mode 100644 index 00000000..bca115ea --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/user_credentials_module/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_modules_public.user_credentials_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_settings_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_settings_module/table.sql new file mode 100644 index 00000000..95138ae6 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_settings_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/user_settings_module/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_modules_public.user_settings_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_state_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_state_module/table.sql new file mode 100644 index 00000000..78077172 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_state_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/user_state_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.user_state_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/users_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/users_module/table.sql new file mode 100644 index 00000000..0233bfe8 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/users_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/users_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.users_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/webauthn_auth_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/webauthn_auth_module/table.sql new file mode 100644 index 00000000..562524f7 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/webauthn_auth_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/webauthn_auth_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.webauthn_auth_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/webauthn_credentials_module/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/webauthn_credentials_module/table.sql new file mode 100644 index 00000000..983fd1df --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/metaschema_modules_public/tables/webauthn_credentials_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/webauthn_credentials_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.webauthn_credentials_module; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/services_private/schema.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/services_private/schema.sql new file mode 100644 index 00000000..44a09520 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/services_private/schema.sql @@ -0,0 +1 @@ +-- Revert schemas/services_private/schema from pg diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/services_public/schema.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/services_public/schema.sql new file mode 100644 index 00000000..5858afc2 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/services_public/schema.sql @@ -0,0 +1 @@ +-- Revert schemas/services_public/schema from pg diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/services_public/tables/apis/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/services_public/tables/apis/table.sql new file mode 100644 index 00000000..2dc0ce44 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/services_public/tables/apis/table.sql @@ -0,0 +1 @@ +-- Revert schemas/services_public/tables/apis/table from pg diff --git a/extensions/@pgpm/metaschema-modules/revert/schemas/services_public/tables/sites/table.sql b/extensions/@pgpm/metaschema-modules/revert/schemas/services_public/tables/sites/table.sql new file mode 100644 index 00000000..12270657 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/revert/schemas/services_public/tables/sites/table.sql @@ -0,0 +1 @@ +-- Revert schemas/services_public/tables/sites/table from pg diff --git a/extensions/@pgpm/metaschema-modules/sql/metaschema-modules--0.26.3.sql b/extensions/@pgpm/metaschema-modules/sql/metaschema-modules--0.26.3.sql new file mode 100644 index 00000000..91453238 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/sql/metaschema-modules--0.26.3.sql @@ -0,0 +1,3209 @@ +\echo Use "CREATE EXTENSION metaschema-modules" to load this file. \quit +CREATE SCHEMA metaschema_modules_public; + +GRANT USAGE ON SCHEMA metaschema_modules_public TO authenticated; + +GRANT USAGE ON SCHEMA metaschema_modules_public TO administrator; + +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_modules_public + GRANT ALL ON TABLES TO authenticated; + +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_modules_public + GRANT ALL ON SEQUENCES TO authenticated; + +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_modules_public + GRANT ALL ON FUNCTIONS TO authenticated; + +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_modules_public + GRANT ALL ON TABLES TO administrator; + +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_modules_public + GRANT ALL ON SEQUENCES TO administrator; + +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_modules_public + GRANT ALL ON FUNCTIONS TO administrator; + +CREATE TABLE metaschema_modules_public.connected_accounts_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + owner_table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL, + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT owner_table_fkey + FOREIGN KEY(owner_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE +); + +CREATE INDEX connected_accounts_module_database_id_idx ON metaschema_modules_public.connected_accounts_module (database_id); + +CREATE TABLE metaschema_modules_public.crypto_addresses_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + owner_table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL, + crypto_network text NOT NULL DEFAULT 'BTC', + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT owner_table_fkey + FOREIGN KEY(owner_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE +); + +CREATE INDEX crypto_addresses_module_database_id_idx ON metaschema_modules_public.crypto_addresses_module (database_id); + +CREATE TABLE metaschema_modules_public.crypto_auth_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + users_table_id uuid NOT NULL DEFAULT uuid_nil(), + secrets_table_id uuid NOT NULL DEFAULT uuid_nil(), + sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), + session_credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), + addresses_table_id uuid NOT NULL DEFAULT uuid_nil(), + user_field text NOT NULL, + crypto_network text NOT NULL DEFAULT 'BTC', + sign_in_request_challenge text NOT NULL DEFAULT 'sign_in_request_challenge', + sign_in_record_failure text NOT NULL DEFAULT 'sign_in_record_failure', + sign_up_with_key text NOT NULL DEFAULT 'sign_up_with_key', + sign_in_with_challenge text NOT NULL DEFAULT 'sign_in_with_challenge', + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT secrets_table_fkey + FOREIGN KEY(secrets_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT users_table_fkey + FOREIGN KEY(users_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT sessions_table_fkey + FOREIGN KEY(sessions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT session_credentials_table_fkey + FOREIGN KEY(session_credentials_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE +); + +CREATE INDEX crypto_auth_module_database_id_idx ON metaschema_modules_public.crypto_auth_module (database_id); + +CREATE TABLE metaschema_modules_public.default_ids_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE +); + +CREATE INDEX default_ids_module_database_id_idx ON metaschema_modules_public.default_ids_module (database_id); + +CREATE TABLE metaschema_modules_public.denormalized_table_field ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + table_id uuid NOT NULL, + field_id uuid NOT NULL, + set_ids uuid[], + ref_table_id uuid NOT NULL, + ref_field_id uuid NOT NULL, + ref_ids uuid[], + use_updates bool NOT NULL DEFAULT true, + update_defaults bool NOT NULL DEFAULT true, + func_name text NULL, + func_order int NOT NULL DEFAULT 0, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT ref_table_fkey + FOREIGN KEY(ref_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT field_fkey + FOREIGN KEY(field_id) + REFERENCES metaschema_public.field (id) + ON DELETE CASCADE, + CONSTRAINT ref_field_fkey + FOREIGN KEY(ref_field_id) + REFERENCES metaschema_public.field (id) + ON DELETE CASCADE +); + +CREATE INDEX denormalized_table_field_database_id_idx ON metaschema_modules_public.denormalized_table_field (database_id); + +CREATE TABLE metaschema_modules_public.emails_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + owner_table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL, + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT owner_table_fkey + FOREIGN KEY(owner_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE +); + +CREATE INDEX emails_module_database_id_idx ON metaschema_modules_public.emails_module (database_id); + +CREATE TABLE metaschema_modules_public.config_secrets_user_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT 'user_secrets', + config_definitions_table_id uuid NOT NULL DEFAULT uuid_nil(), + api_name text DEFAULT 'config', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT config_defs_table_fkey + FOREIGN KEY(config_definitions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX config_secrets_user_module_database_id_idx ON metaschema_modules_public.config_secrets_user_module (database_id); + +CREATE TABLE metaschema_modules_public.invites_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + emails_table_id uuid NOT NULL DEFAULT uuid_nil(), + users_table_id uuid NOT NULL DEFAULT uuid_nil(), + invites_table_id uuid NOT NULL DEFAULT uuid_nil(), + claimed_invites_table_id uuid NOT NULL DEFAULT uuid_nil(), + invites_table_name text NOT NULL DEFAULT '', + claimed_invites_table_name text NOT NULL DEFAULT '', + submit_invite_code_function text NOT NULL DEFAULT '', + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', + entity_table_id uuid NULL, + api_name text DEFAULT 'admin', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT invites_table_fkey + FOREIGN KEY(invites_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT emails_table_fkey + FOREIGN KEY(emails_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT users_table_fkey + FOREIGN KEY(users_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT entity_table_fkey + FOREIGN KEY(entity_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT claimed_invites_table_fkey + FOREIGN KEY(claimed_invites_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT pschema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE +); + +CREATE INDEX invites_module_database_id_idx ON metaschema_modules_public.invites_module (database_id); + +CREATE UNIQUE INDEX invites_module_unique_scope ON metaschema_modules_public.invites_module (database_id, scope, prefix); + +CREATE TABLE metaschema_modules_public.events_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + events_table_id uuid NOT NULL DEFAULT uuid_nil(), + events_table_name text NOT NULL DEFAULT '', + event_aggregates_table_id uuid NOT NULL DEFAULT uuid_nil(), + event_aggregates_table_name text NOT NULL DEFAULT '', + event_types_table_id uuid NOT NULL DEFAULT uuid_nil(), + event_types_table_name text NOT NULL DEFAULT '', + levels_table_id uuid NOT NULL DEFAULT uuid_nil(), + levels_table_name text NOT NULL DEFAULT '', + level_requirements_table_id uuid NOT NULL DEFAULT uuid_nil(), + level_requirements_table_name text NOT NULL DEFAULT '', + level_grants_table_id uuid NOT NULL DEFAULT uuid_nil(), + level_grants_table_name text NOT NULL DEFAULT '', + achievement_rewards_table_id uuid NOT NULL DEFAULT uuid_nil(), + achievement_rewards_table_name text NOT NULL DEFAULT '', + record_event text NOT NULL DEFAULT '', + remove_event text NOT NULL DEFAULT '', + tg_event text NOT NULL DEFAULT '', + tg_event_toggle text NOT NULL DEFAULT '', + tg_event_toggle_bool text NOT NULL DEFAULT '', + tg_event_bool text NOT NULL DEFAULT '', + upsert_aggregate text NOT NULL DEFAULT '', + tg_update_aggregates text NOT NULL DEFAULT '', + prune_events text NOT NULL DEFAULT '', + steps_required text NOT NULL DEFAULT '', + level_achieved text NOT NULL DEFAULT '', + tg_check_achievements text NOT NULL DEFAULT '', + grant_achievement text NOT NULL DEFAULT '', + tg_achievement_reward text NOT NULL DEFAULT '', + interval text NOT NULL DEFAULT '1 month', + retention text DEFAULT '12 months', + premake int NOT NULL DEFAULT 2, + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', + entity_table_id uuid NULL, + actor_table_id uuid NOT NULL DEFAULT uuid_nil(), + default_permissions text[] DEFAULT NULL, + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT events_table_fkey + FOREIGN KEY(events_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT event_aggregates_table_fkey + FOREIGN KEY(event_aggregates_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT event_types_table_fkey + FOREIGN KEY(event_types_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT levels_table_fkey + FOREIGN KEY(levels_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT level_requirements_table_fkey + FOREIGN KEY(level_requirements_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT level_grants_table_fkey + FOREIGN KEY(level_grants_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT achievement_rewards_table_fkey + FOREIGN KEY(achievement_rewards_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT entity_table_fkey + FOREIGN KEY(entity_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT actor_table_fkey + FOREIGN KEY(actor_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX events_module_database_id_idx ON metaschema_modules_public.events_module (database_id); + +CREATE TABLE metaschema_modules_public.limits_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT '', + default_table_id uuid NOT NULL DEFAULT uuid_nil(), + default_table_name text NOT NULL DEFAULT '', + limit_increment_function text NOT NULL DEFAULT '', + limit_decrement_function text NOT NULL DEFAULT '', + limit_increment_trigger text NOT NULL DEFAULT '', + limit_decrement_trigger text NOT NULL DEFAULT '', + limit_update_trigger text NOT NULL DEFAULT '', + limit_check_function text NOT NULL DEFAULT '', + limit_credits_table_id uuid NULL, + events_table_id uuid NULL, + credit_codes_table_id uuid NULL, + credit_code_items_table_id uuid NULL, + credit_redemptions_table_id uuid NULL, + aggregate_table_id uuid NULL, + limit_caps_table_id uuid NULL, + limit_caps_defaults_table_id uuid NULL, + cap_check_trigger text NOT NULL DEFAULT '', + resolve_cap_function text NOT NULL DEFAULT '', + limit_warnings_table_id uuid NULL, + limit_warning_state_table_id uuid NULL, + limit_check_soft_function text NOT NULL DEFAULT '', + limit_aggregate_check_soft_function text NOT NULL DEFAULT '', + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', + entity_table_id uuid NULL, + actor_table_id uuid NOT NULL DEFAULT uuid_nil(), + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT default_table_fkey + FOREIGN KEY(default_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT entity_table_fkey + FOREIGN KEY(entity_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT actor_table_fkey + FOREIGN KEY(actor_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT aggregate_table_fkey + FOREIGN KEY(aggregate_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT limit_credits_table_fkey + FOREIGN KEY(limit_credits_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT events_table_fkey + FOREIGN KEY(events_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT credit_codes_table_fkey + FOREIGN KEY(credit_codes_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT credit_code_items_table_fkey + FOREIGN KEY(credit_code_items_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT credit_redemptions_table_fkey + FOREIGN KEY(credit_redemptions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT limit_caps_table_fkey + FOREIGN KEY(limit_caps_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT limit_caps_defaults_table_fkey + FOREIGN KEY(limit_caps_defaults_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT limit_warnings_table_fkey + FOREIGN KEY(limit_warnings_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT limit_warning_state_table_fkey + FOREIGN KEY(limit_warning_state_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX limits_module_database_id_idx ON metaschema_modules_public.limits_module (database_id); + +CREATE TABLE metaschema_modules_public.membership_types_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT 'membership_types', + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX membership_types_module_database_id_idx ON metaschema_modules_public.membership_types_module (database_id); + +CREATE TABLE metaschema_modules_public.memberships_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + memberships_table_id uuid NOT NULL DEFAULT uuid_nil(), + memberships_table_name text NOT NULL DEFAULT '', + members_table_id uuid NOT NULL DEFAULT uuid_nil(), + members_table_name text NOT NULL DEFAULT '', + membership_defaults_table_id uuid NOT NULL DEFAULT uuid_nil(), + membership_defaults_table_name text NOT NULL DEFAULT '', + membership_settings_table_id uuid NULL, + membership_settings_table_name text NOT NULL DEFAULT '', + grants_table_id uuid NOT NULL DEFAULT uuid_nil(), + grants_table_name text NOT NULL DEFAULT '', + actor_table_id uuid NOT NULL DEFAULT uuid_nil(), + limits_table_id uuid NOT NULL DEFAULT uuid_nil(), + default_limits_table_id uuid NOT NULL DEFAULT uuid_nil(), + permissions_table_id uuid NOT NULL DEFAULT uuid_nil(), + default_permissions_table_id uuid NOT NULL DEFAULT uuid_nil(), + sprt_table_id uuid NOT NULL DEFAULT uuid_nil(), + admin_grants_table_id uuid NOT NULL DEFAULT uuid_nil(), + admin_grants_table_name text NOT NULL DEFAULT '', + owner_grants_table_id uuid NOT NULL DEFAULT uuid_nil(), + owner_grants_table_name text NOT NULL DEFAULT '', + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', + entity_table_id uuid NULL, + entity_table_owner_id uuid NULL, + get_org_fn text NULL, + actor_mask_check text NOT NULL DEFAULT '', + actor_perm_check text NOT NULL DEFAULT '', + entity_ids_by_mask text NULL, + entity_ids_by_perm text NULL, + entity_ids_function text NULL, + member_profiles_table_id uuid NULL, + api_name text DEFAULT 'admin', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT memberships_table_fkey + FOREIGN KEY(memberships_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT membership_defaults_table_fkey + FOREIGN KEY(membership_defaults_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT membership_settings_table_fkey + FOREIGN KEY(membership_settings_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT members_table_fkey + FOREIGN KEY(members_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT grants_table_fkey + FOREIGN KEY(grants_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT sprt_table_fkey + FOREIGN KEY(sprt_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT entity_table_fkey + FOREIGN KEY(entity_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT entity_table_owner_fkey + FOREIGN KEY(entity_table_owner_id) + REFERENCES metaschema_public.field (id) + ON DELETE CASCADE, + CONSTRAINT actor_table_fkey + FOREIGN KEY(actor_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT limits_table_fkey + FOREIGN KEY(limits_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT default_limits_table_fkey + FOREIGN KEY(default_limits_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT permissions_table_fkey + FOREIGN KEY(permissions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT default_permissions_table_fkey + FOREIGN KEY(default_permissions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT memberships_module_unique + UNIQUE (database_id, scope, prefix) +); + +CREATE INDEX memberships_module_database_id_idx ON metaschema_modules_public.memberships_module (database_id); + +CREATE TABLE metaschema_modules_public.permissions_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT '', + default_table_id uuid NOT NULL DEFAULT uuid_nil(), + default_table_name text NOT NULL DEFAULT '', + bitlen int NOT NULL DEFAULT 64, + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', + entity_table_id uuid NULL, + actor_table_id uuid NOT NULL DEFAULT uuid_nil(), + get_padded_mask text NOT NULL DEFAULT '', + get_mask text NOT NULL DEFAULT '', + get_by_mask text NOT NULL DEFAULT '', + get_mask_by_name text NOT NULL DEFAULT '', + api_name text DEFAULT 'admin', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT default_table_fkey + FOREIGN KEY(default_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT entity_table_fkey + FOREIGN KEY(entity_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT actor_table_fkey + FOREIGN KEY(actor_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX permissions_module_database_id_idx ON metaschema_modules_public.permissions_module (database_id); + +CREATE TABLE metaschema_modules_public.phone_numbers_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + owner_table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL, + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT owner_table_fkey + FOREIGN KEY(owner_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE +); + +CREATE INDEX phone_numbers_module_database_id_idx ON metaschema_modules_public.phone_numbers_module (database_id); + +CREATE TABLE metaschema_modules_public.profiles_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT '', + profile_permissions_table_id uuid NOT NULL DEFAULT uuid_nil(), + profile_permissions_table_name text NOT NULL DEFAULT '', + profile_grants_table_id uuid NOT NULL DEFAULT uuid_nil(), + profile_grants_table_name text NOT NULL DEFAULT '', + profile_definition_grants_table_id uuid NOT NULL DEFAULT uuid_nil(), + profile_definition_grants_table_name text NOT NULL DEFAULT '', + profile_templates_table_id uuid NOT NULL DEFAULT uuid_nil(), + profile_templates_table_name text NOT NULL DEFAULT '', + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', + entity_table_id uuid NULL, + actor_table_id uuid NOT NULL DEFAULT uuid_nil(), + permissions_table_id uuid NOT NULL DEFAULT uuid_nil(), + memberships_table_id uuid NOT NULL DEFAULT uuid_nil(), + api_name text DEFAULT 'admin', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT profile_permissions_table_fkey + FOREIGN KEY(profile_permissions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT profile_grants_table_fkey + FOREIGN KEY(profile_grants_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT profile_definition_grants_table_fkey + FOREIGN KEY(profile_definition_grants_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT profile_templates_table_fkey + FOREIGN KEY(profile_templates_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT entity_table_fkey + FOREIGN KEY(entity_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT actor_table_fkey + FOREIGN KEY(actor_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT permissions_table_fkey + FOREIGN KEY(permissions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT memberships_table_fkey + FOREIGN KEY(memberships_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT profiles_module_unique + UNIQUE (database_id, scope, prefix) +); + +CREATE INDEX profiles_module_database_id_idx ON metaschema_modules_public.profiles_module (database_id); + +CREATE TABLE metaschema_modules_public.rls_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + session_credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), + sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), + users_table_id uuid NOT NULL DEFAULT uuid_nil(), + authenticate text NOT NULL DEFAULT 'authenticate', + authenticate_strict text NOT NULL DEFAULT 'authenticate_strict', + "current_role" text NOT NULL DEFAULT 'current_user', + current_role_id text NOT NULL DEFAULT 'current_user_id', + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT session_credentials_table_fkey + FOREIGN KEY(session_credentials_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT sessions_table_fkey + FOREIGN KEY(sessions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT users_table_fkey + FOREIGN KEY(users_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT pschema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT database_id_uniq + UNIQUE (database_id) +); + +COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.rls_module IS '@omit'; + +COMMENT ON CONSTRAINT session_credentials_table_fkey ON metaschema_modules_public.rls_module IS '@omit'; + +COMMENT ON CONSTRAINT sessions_table_fkey ON metaschema_modules_public.rls_module IS '@omit'; + +COMMENT ON CONSTRAINT users_table_fkey ON metaschema_modules_public.rls_module IS '@omit'; + +CREATE INDEX rls_module_database_id_idx ON metaschema_modules_public.rls_module (database_id); + +CREATE TABLE metaschema_modules_public.user_state_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT 'user_state', + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX user_state_module_database_id_idx ON metaschema_modules_public.user_state_module (database_id); + +CREATE TABLE metaschema_modules_public.sessions_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), + session_credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), + auth_settings_table_id uuid NOT NULL DEFAULT uuid_nil(), + users_table_id uuid NOT NULL DEFAULT uuid_nil(), + sessions_default_expiration interval NOT NULL DEFAULT '30 days'::interval, + sessions_table text NOT NULL DEFAULT 'sessions', + session_credentials_table text NOT NULL DEFAULT 'session_credentials', + auth_settings_table text NOT NULL DEFAULT 'app_settings_auth', + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT sessions_table_fkey + FOREIGN KEY(sessions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT session_credentials_table_fkey + FOREIGN KEY(session_credentials_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT auth_settings_table_fkey + FOREIGN KEY(auth_settings_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT users_table_fkey + FOREIGN KEY(users_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX sessions_module_database_id_idx ON metaschema_modules_public.sessions_module (database_id); + +COMMENT ON CONSTRAINT sessions_table_fkey ON metaschema_modules_public.sessions_module IS '@fieldName sessionsTableBySessionsTableId'; + +COMMENT ON CONSTRAINT session_credentials_table_fkey ON metaschema_modules_public.sessions_module IS '@fieldName sessionCredentialsTableBySessionCredentialsTableId'; + +COMMENT ON CONSTRAINT auth_settings_table_fkey ON metaschema_modules_public.sessions_module IS '@fieldName authSettingsTableByAuthSettingsTableId'; + +CREATE TABLE metaschema_modules_public.user_auth_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + emails_table_id uuid NOT NULL DEFAULT uuid_nil(), + users_table_id uuid NOT NULL DEFAULT uuid_nil(), + secrets_table_id uuid NOT NULL DEFAULT uuid_nil(), + encrypted_table_id uuid NOT NULL DEFAULT uuid_nil(), + sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), + session_credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), + audits_table_id uuid NOT NULL DEFAULT uuid_nil(), + audits_table_name text NOT NULL DEFAULT 'audit_log_auth', + sign_in_function text NOT NULL DEFAULT 'sign_in', + sign_up_function text NOT NULL DEFAULT 'sign_up', + sign_out_function text NOT NULL DEFAULT 'sign_out', + set_password_function text NOT NULL DEFAULT 'set_password', + reset_password_function text NOT NULL DEFAULT 'reset_password', + forgot_password_function text NOT NULL DEFAULT 'forgot_password', + send_verification_email_function text NOT NULL DEFAULT 'send_verification_email', + verify_email_function text NOT NULL DEFAULT 'verify_email', + verify_password_function text NOT NULL DEFAULT 'verify_password', + check_password_function text NOT NULL DEFAULT 'check_password', + send_account_deletion_email_function text NOT NULL DEFAULT 'send_account_deletion_email', + delete_account_function text NOT NULL DEFAULT 'confirm_delete_account', + sign_in_cross_origin_function text NOT NULL DEFAULT 'sign_in_cross_origin', + request_cross_origin_token_function text NOT NULL DEFAULT 'request_cross_origin_token', + extend_token_expires text NOT NULL DEFAULT 'extend_token_expires', + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT email_table_fkey + FOREIGN KEY(emails_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT users_table_fkey + FOREIGN KEY(users_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT secrets_table_fkey + FOREIGN KEY(secrets_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT encrypted_table_fkey + FOREIGN KEY(encrypted_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT sessions_table_fkey + FOREIGN KEY(sessions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT session_credentials_table_fkey + FOREIGN KEY(session_credentials_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX user_auth_module_database_id_idx ON metaschema_modules_public.user_auth_module (database_id); + +COMMENT ON CONSTRAINT email_table_fkey ON metaschema_modules_public.user_auth_module IS '@omit'; + +COMMENT ON CONSTRAINT users_table_fkey ON metaschema_modules_public.user_auth_module IS '@omit'; + +COMMENT ON CONSTRAINT secrets_table_fkey ON metaschema_modules_public.user_auth_module IS '@omit'; + +COMMENT ON CONSTRAINT encrypted_table_fkey ON metaschema_modules_public.user_auth_module IS '@omit'; + +COMMENT ON CONSTRAINT sessions_table_fkey ON metaschema_modules_public.user_auth_module IS '@omit'; + +COMMENT ON CONSTRAINT session_credentials_table_fkey ON metaschema_modules_public.user_auth_module IS '@omit'; + +CREATE TABLE metaschema_modules_public.users_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT 'users', + type_table_id uuid NOT NULL DEFAULT uuid_nil(), + type_table_name text NOT NULL DEFAULT 'role_types', + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT type_table_fkey + FOREIGN KEY(type_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX users_module_database_id_idx ON metaschema_modules_public.users_module (database_id); + +CREATE TABLE metaschema_modules_public.hierarchy_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + chart_edges_table_id uuid NOT NULL DEFAULT uuid_nil(), + chart_edges_table_name text NOT NULL DEFAULT '', + hierarchy_sprt_table_id uuid NOT NULL DEFAULT uuid_nil(), + hierarchy_sprt_table_name text NOT NULL DEFAULT '', + chart_edge_grants_table_id uuid NOT NULL DEFAULT uuid_nil(), + chart_edge_grants_table_name text NOT NULL DEFAULT '', + entity_table_id uuid NOT NULL, + users_table_id uuid NOT NULL, + scope text NOT NULL DEFAULT 'org', + prefix text NOT NULL DEFAULT '', + private_schema_name text NOT NULL DEFAULT '', + sprt_table_name text NOT NULL DEFAULT '', + rebuild_hierarchy_function text NOT NULL DEFAULT '', + get_subordinates_function text NOT NULL DEFAULT '', + get_managers_function text NOT NULL DEFAULT '', + is_manager_of_function text NOT NULL DEFAULT '', + default_permissions text[] DEFAULT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT chart_edges_table_fkey + FOREIGN KEY(chart_edges_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT hierarchy_sprt_table_fkey + FOREIGN KEY(hierarchy_sprt_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT chart_edge_grants_table_fkey + FOREIGN KEY(chart_edge_grants_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT entity_table_fkey + FOREIGN KEY(entity_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT users_table_fkey + FOREIGN KEY(users_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT hierarchy_module_database_unique + UNIQUE (database_id) +); + +CREATE INDEX hierarchy_module_database_id_idx ON metaschema_modules_public.hierarchy_module (database_id); + +CREATE TABLE metaschema_modules_public.secure_table_provision ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text DEFAULT NULL, + nodes jsonb NOT NULL DEFAULT '[]', + use_rls boolean NOT NULL DEFAULT true, + fields jsonb[] NOT NULL DEFAULT '{}', + grants jsonb NOT NULL DEFAULT '[]', + policies jsonb NOT NULL DEFAULT '[]', + out_fields uuid[] DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE +); + +COMMENT ON TABLE metaschema_modules_public.secure_table_provision IS 'Provisions security, fields, grants, and policies onto a table. Each row can independently: (1) create fields via nodes[] array (supporting multiple Data* modules per row), (2) grant privileges via grants[] array (supporting per-role privilege targeting), (3) create RLS policies via policies[] array (supporting multiple Authz* policies per row). Multiple rows can target the same table to compose different concerns. All three concerns are optional and independent.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.id IS 'Unique identifier for this provision row.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.database_id IS 'The database this provision belongs to. Required.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.schema_id IS 'Target schema for the table. Defaults to uuid_nil(); the trigger resolves this to the app_public schema if not explicitly provided.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.table_id IS 'Target table to provision. Defaults to uuid_nil(); the trigger creates or resolves the table via table_name if not explicitly provided.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.table_name IS 'Name of the target table. Used to create or look up the table when table_id is not provided. If omitted, it is backfilled from the resolved table.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.nodes IS 'Array of node objects to apply to the table. Each element is a jsonb object with a required "$type" key (one of: DataId, DataDirectOwner, DataEntityMembership, DataOwnershipInEntity, DataTimestamps, DataPeoplestamps, DataPublishable, DataSoftDelete, DataEmbedding, DataFullTextSearch, DataSlug, etc.) and an optional "data" key containing generator-specific configuration. Supports multiple nodes per row, matching the blueprint definition format. Example: [{"$type": "DataId"}, {"$type": "DataTimestamps"}, {"$type": "DataDirectOwner", "data": {"owner_field_name": "author_id"}}]. Defaults to ''[]'' (no node processing).'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.use_rls IS 'If true and Row Level Security is not yet enabled on the target table, enable it. Automatically set to true by the trigger when policies[] is non-empty. Defaults to true.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.fields IS 'PostgreSQL array of jsonb field definition objects to create on the target table. Each object has keys: "name" (text, required), "type" (text, required), "default" (text, optional), "is_required" (boolean, optional, defaults to false), "min" (float, optional), "max" (float, optional), "regexp" (text, optional), "index" (boolean, optional, defaults to false — creates a btree index on the field). min/max generate CHECK constraints: for text/citext they constrain character_length, for integer/float types they constrain the value. regexp generates a CHECK (col ~ pattern) constraint for text/citext. Fields are created via metaschema.create_field() after any node_type generator runs, and their IDs are appended to out_fields. Example: ARRAY[''{"name":"username","type":"citext","max":256,"regexp":"^[a-z0-9_]+$"}''::jsonb, ''{"name":"score","type":"integer","min":0,"max":100}''::jsonb]. Defaults to ''{}'' (no additional fields).'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.grants IS 'Array of grant objects defining table privileges. Each element is a jsonb object with keys: "roles" (text[], required — database roles to grant to, e.g. ["authenticated","admin"]), "privileges" (jsonb[], required — array of [privilege, columns] tuples, e.g. [["select","*"],["insert","*"]]). "*" means all columns; an array means column-level grant. Supports per-role privilege targeting: different grant entries can target different roles with different privileges. Example: [{"roles":["authenticated"],"privileges":[["select","*"]]},{"roles":["admin"],"privileges":[["insert","*"],["update","*"],["delete","*"]]}]. Defaults to ''[]'' (no grants). When policies[] omit explicit privileges/policy_role, they fall back to the verbs and first role from grants[].'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.policies IS 'Array of policy objects to create on the target table. Each element is a jsonb object with keys: "$type" (text, required — the Authz* policy generator type, e.g. AuthzEntityMembership, AuthzMembership, AuthzDirectOwner, AuthzPublishable, AuthzAllowAll), "data" (jsonb, optional — opaque configuration passed to metaschema.create_policy(), structure varies by type), "privileges" (text[], optional — privileges the policy applies to, e.g. ["select","insert"]; if omitted, derived from grants[] privilege verbs), "policy_role" (text, optional — role the policy targets; if omitted, falls back to first role in first grants[] entry, or ''authenticated'' if no grants), "permissive" (boolean, optional — PERMISSIVE or RESTRICTIVE; defaults to true), "policy_name" (text, optional — custom suffix for the generated policy name; if omitted, auto-derived from $type by stripping Authz prefix). Supports multiple policies per row. Example: [{"$type": "AuthzEntityMembership", "data": {"entity_field": "owner_id", "membership_type": 3}, "privileges": ["select", "insert"]}, {"$type": "AuthzDirectOwner", "data": {"entity_field": "actor_id"}, "privileges": ["update", "delete"]}]. Defaults to ''[]'' (no policies created). When non-empty, the trigger automatically enables RLS.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.out_fields IS 'Output column populated by the trigger after field creation. Contains the UUIDs of the metaschema fields created on the target table by this provision row''s nodes. NULL when nodes is empty or before the trigger runs. Callers should not set this directly.'; + +CREATE INDEX secure_table_provision_database_id_idx ON metaschema_modules_public.secure_table_provision (database_id); + +CREATE INDEX secure_table_provision_table_id_idx ON metaschema_modules_public.secure_table_provision (table_id); + +CREATE TABLE metaschema_modules_public.relation_provision ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + relation_type text NOT NULL CHECK (relation_type IN ('RelationBelongsTo', 'RelationHasOne', 'RelationHasMany', 'RelationManyToMany')), + source_table_id uuid NOT NULL, + target_table_id uuid NOT NULL, + field_name text DEFAULT NULL, + delete_action text DEFAULT NULL, + is_required boolean NOT NULL DEFAULT true, + api_required boolean NOT NULL DEFAULT false, + junction_table_id uuid NOT NULL DEFAULT uuid_nil(), + junction_table_name text DEFAULT NULL, + junction_schema_id uuid DEFAULT NULL, + source_field_name text DEFAULT NULL, + target_field_name text DEFAULT NULL, + use_composite_key boolean NOT NULL DEFAULT false, + create_index boolean NOT NULL DEFAULT true, + expose_in_api boolean NOT NULL DEFAULT true, + nodes jsonb NOT NULL DEFAULT '[]', + grants jsonb NOT NULL DEFAULT '[]', + policies jsonb NOT NULL DEFAULT '[]', + out_field_id uuid DEFAULT NULL, + out_junction_table_id uuid DEFAULT NULL, + out_source_field_id uuid DEFAULT NULL, + out_target_field_id uuid DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT source_table_fkey + FOREIGN KEY(source_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT target_table_fkey + FOREIGN KEY(target_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +COMMENT ON TABLE metaschema_modules_public.relation_provision IS 'Provisions relational structure between tables. Supports four relation types: + - RelationBelongsTo: adds a FK field on the source table referencing the target table (child perspective: "tasks belongs to projects" -> tasks.project_id). + - RelationHasMany: adds a FK field on the target table referencing the source table (parent perspective: "projects has many tasks" -> tasks.project_id). Inverse of BelongsTo. + - RelationHasOne: adds a FK field with a unique constraint on the source table referencing the target table. Also supports shared-primary-key patterns where the FK field IS the primary key (set field_name to the existing PK field name). + - RelationManyToMany: creates a junction table with FK fields to both source and target tables, delegating table creation and security to secure_table_provision. + This is a one-and-done structural provisioner. To layer additional security onto junction tables after creation, use secure_table_provision directly. + All operations are graceful: existing fields, FK constraints, and unique constraints are reused if found. + The trigger never injects values the caller did not provide. All security config is forwarded to secure_table_provision as-is.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.id IS 'Unique identifier for this relation provision row.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.database_id IS 'The database this relation belongs to. Required. Must match the database of both source_table_id and target_table_id.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.relation_type IS 'The type of relation to create. Uses SuperCase naming: + - RelationBelongsTo: creates a FK field on source_table referencing target_table (e.g., tasks belongs to projects -> tasks.project_id). Field name auto-derived from target table. + - RelationHasMany: creates a FK field on target_table referencing source_table (e.g., projects has many tasks -> tasks.project_id). Field name auto-derived from source table. Inverse of BelongsTo — same FK, different perspective. + - RelationHasOne: creates a FK field + unique constraint on source_table referencing target_table (e.g., user_settings has one user -> user_settings.user_id with UNIQUE). Also supports shared-primary-key patterns (e.g., user_profiles.id = users.id) by setting field_name to the existing PK field. + - RelationManyToMany: creates a junction table with FK fields to both tables (e.g., projects and tags -> project_tags table). + Each relation type uses a different subset of columns on this table. Required.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.source_table_id IS 'The source table in the relation. Required. + - RelationBelongsTo: the table that receives the FK field (e.g., tasks in "tasks belongs to projects"). + - RelationHasMany: the parent table being referenced (e.g., projects in "projects has many tasks"). The FK field is created on the target table. + - RelationHasOne: the table that receives the FK field + unique constraint (e.g., user_settings in "user_settings has one user"). + - RelationManyToMany: one of the two tables being joined (e.g., projects in "projects and tags"). The junction table will have a FK field referencing this table.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.target_table_id IS 'The target table in the relation. Required. + - RelationBelongsTo: the table being referenced by the FK (e.g., projects in "tasks belongs to projects"). + - RelationHasMany: the table that receives the FK field (e.g., tasks in "projects has many tasks"). + - RelationHasOne: the table being referenced by the FK (e.g., users in "user_settings has one user"). + - RelationManyToMany: the other table being joined (e.g., tags in "projects and tags"). The junction table will have a FK field referencing this table.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.field_name IS 'FK field name for RelationBelongsTo, RelationHasOne, and RelationHasMany. + - RelationBelongsTo/RelationHasOne: if NULL, auto-derived from the target table name (e.g., target "projects" derives "project_id"). + - RelationHasMany: if NULL, auto-derived from the source table name (e.g., source "projects" derives "project_id"). + For RelationHasOne shared-primary-key patterns, set field_name to the existing PK field (e.g., "id") so the FK reuses it. + Ignored for RelationManyToMany — use source_field_name/target_field_name instead.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.delete_action IS 'FK delete action for RelationBelongsTo, RelationHasOne, and RelationHasMany. One of: c (CASCADE), r (RESTRICT), n (SET NULL), d (SET DEFAULT), a (NO ACTION). Required — the trigger raises an error if not provided. The caller must explicitly choose the cascade behavior; there is no default. Ignored for RelationManyToMany (junction FK fields always use CASCADE).'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.is_required IS 'Whether the FK field is NOT NULL. Defaults to true. + - RelationBelongsTo: set to false for optional associations (e.g., tasks.assignee_id that can be NULL). + - RelationHasMany: set to false if the child can exist without a parent. + - RelationHasOne: typically true. + Ignored for RelationManyToMany (junction FK fields are always required).'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.api_required IS 'Whether the FK field should be required at the API level even though it is nullable at the database level. Defaults to false. + When true and is_required is false, the field is created as nullable (allowing SET NULL cascade) but a @requiredInput smart tag is added so PostGraphile treats it as non-null in create/update input types. + When is_required is true, api_required is ignored (the field is already required at both levels). + Ignored for RelationManyToMany (junction FK fields are always required).'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.junction_table_id IS 'For RelationManyToMany: an existing junction table to use. Defaults to uuid_nil(). + - When uuid_nil(): the trigger creates a new junction table via secure_table_provision using junction_table_name. + - When set to a valid table UUID: the trigger skips table creation and only adds FK fields, composite key (if use_composite_key is true), and security to the existing table. + Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.junction_table_name IS 'For RelationManyToMany: name of the junction table to create or look up. If NULL, auto-derived from source and target table names using inflection_db (e.g., "projects" + "tags" derives "project_tags"). Only used when junction_table_id is uuid_nil(). Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.junction_schema_id IS 'For RelationManyToMany: schema for the junction table. If NULL, defaults to the source table''s schema. Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.source_field_name IS 'For RelationManyToMany: FK field name on the junction table referencing the source table. If NULL, auto-derived from the source table name using inflection_db.get_foreign_key_field_name() (e.g., source table "projects" derives "project_id"). Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.target_field_name IS 'For RelationManyToMany: FK field name on the junction table referencing the target table. If NULL, auto-derived from the target table name using inflection_db.get_foreign_key_field_name() (e.g., target table "tags" derives "tag_id"). Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.use_composite_key IS 'For RelationManyToMany: whether to create a composite primary key from the two FK fields (source + target) on the junction table. Defaults to false. + - When true: the trigger calls metaschema.pk() with ARRAY[source_field_id, target_field_id] to create a composite PK. No separate id column is created. This enforces uniqueness of the pair and is suitable for simple junction tables. + - When false: no primary key is created by the trigger. The caller should provide node_type=''DataId'' to create a UUID primary key, or handle the PK strategy via a separate secure_table_provision row. + use_composite_key and node_type=''DataId'' are mutually exclusive — using both would create two conflicting PKs. + Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.create_index IS 'Whether to create a btree index on FK fields created by this relation. Defaults to true. + PostgreSQL does not automatically index foreign key columns (only the referenced PK side is indexed). + Without indexes on FK columns, JOINs, CASCADE deletes, and RLS policy lookups perform sequential scans. + - RelationBelongsTo: creates an index on the FK field on the source table. + - RelationHasMany: creates an index on the FK field on the target table. + - RelationHasOne: skipped — the unique constraint already creates an implicit index. + - RelationManyToMany: creates indexes on both FK fields on the junction table. + Set to false only for very small tables or write-heavy tables where index maintenance cost outweighs read performance.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.expose_in_api IS 'For RelationManyToMany: whether to expose the M:N shortcut fields in the GraphQL API. Defaults to true. + When true, sets @behavior +manyToMany on the junction table smart_tags so PostGraphile generates + clean M:N connection fields (e.g., event.contacts instead of event.contactEventsByEventId). + When false (or toggled off via UPDATE), the behavior tag is removed and the M:N fields disappear from GraphQL. + Toggling is supported: UPDATE expose_in_api to true/false and the smart tag is added/removed automatically. + Ignored for RelationBelongsTo/RelationHasOne/RelationHasMany.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.nodes IS 'For RelationManyToMany: array of node objects to apply to the junction table. Each element is a jsonb object with a required "$type" key and an optional "data" key. Forwarded to provision_table as-is. The trigger does not interpret or validate this value. + Examples: [{"$type": "DataId"}, {"$type": "DataTimestamps"}, {"$type": "DataDirectOwner", "data": {"owner_field_name": "author_id"}}]. + Defaults to ''[]'' (no node processing beyond the FK fields and composite key if use_composite_key is true). + Ignored for RelationBelongsTo/RelationHasOne/RelationHasMany.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.grants IS 'For RelationManyToMany: array of grant objects for the junction table. Forwarded to provision_table as-is. Each element is a jsonb object with keys: "roles" (text[], required), "privileges" (jsonb[], required — array of [privilege, columns] tuples). Example: [{"roles":["authenticated"],"privileges":[["select","*"],["insert","*"],["delete","*"]]}]. Defaults to ''[]'' (no grants). Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.policies IS 'For RelationManyToMany: array of policy objects for the junction table. Forwarded to provision_table as-is. Each element is a jsonb object with keys: "$type" (text, required — the Authz* policy generator type), "data" (jsonb, optional — opaque config), "privileges" (text[], optional — e.g. ["select","insert"]; if omitted, derived from grants[] privilege verbs), "policy_role" (text, optional — falls back to first role in first grants[] entry, or ''authenticated''), "permissive" (boolean, optional, defaults to true), "policy_name" (text, optional). Supports multiple policies per row. + Example: [{"$type": "AuthzEntityMembership", "data": {"entity_field": "entity_id", "membership_type": 2}, "privileges": ["select", "insert", "delete"]}]. + Defaults to ''[]'' (no policies — the junction table will have RLS enabled but no policies unless added separately). + Ignored for RelationBelongsTo/RelationHasOne/RelationHasMany.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.out_field_id IS 'Output column for RelationBelongsTo/RelationHasOne/RelationHasMany: the UUID of the FK field created (or found). For BelongsTo/HasOne this is on the source table; for HasMany this is on the target table. Populated by the trigger. NULL for RelationManyToMany. Callers should not set this directly.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.out_junction_table_id IS 'Output column for RelationManyToMany: the UUID of the junction table created (or found). Populated by the trigger. NULL for RelationBelongsTo/RelationHasOne. Callers should not set this directly.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.out_source_field_id IS 'Output column for RelationManyToMany: the UUID of the FK field on the junction table referencing the source table. Populated by the trigger. NULL for RelationBelongsTo/RelationHasOne. Callers should not set this directly.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.out_target_field_id IS 'Output column for RelationManyToMany: the UUID of the FK field on the junction table referencing the target table. Populated by the trigger. NULL for RelationBelongsTo/RelationHasOne. Callers should not set this directly.'; + +CREATE INDEX relation_provision_database_id_idx ON metaschema_modules_public.relation_provision (database_id); + +CREATE INDEX relation_provision_relation_type_idx ON metaschema_modules_public.relation_provision (relation_type); + +CREATE INDEX relation_provision_source_table_id_idx ON metaschema_modules_public.relation_provision (source_table_id); + +CREATE INDEX relation_provision_target_table_id_idx ON metaschema_modules_public.relation_provision (target_table_id); + +CREATE TABLE metaschema_modules_public.blueprint_template ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + name text NOT NULL, + version text NOT NULL DEFAULT '1.0.0', + display_name text NOT NULL, + description text, + owner_id uuid NOT NULL, + visibility text NOT NULL DEFAULT 'private' CHECK (visibility IN ('private', 'public')), + categories text[] NOT NULL DEFAULT '{}', + tags text[] NOT NULL DEFAULT '{}', + definition jsonb NOT NULL, + definition_schema_version text NOT NULL DEFAULT '1', + source text NOT NULL DEFAULT 'user' CHECK (source IN ('user', 'system', 'agent')), + complexity text DEFAULT NULL CHECK ( + complexity IS NULL + OR complexity IN ('simple', 'moderate', 'complex') + ), + copy_count int NOT NULL DEFAULT 0, + fork_count int NOT NULL DEFAULT 0, + forked_from_id uuid DEFAULT NULL, + definition_hash uuid, + table_hashes jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT blueprint_template_unique_owner_name_version + UNIQUE (owner_id, name, version), + CONSTRAINT blueprint_template_forked_from_fkey + FOREIGN KEY(forked_from_id) + REFERENCES metaschema_modules_public.blueprint_template (id) +); + +COMMENT ON TABLE metaschema_modules_public.blueprint_template IS 'A shareable, versioned schema recipe for the blueprint marketplace. Templates define arrays of secure_table_provision + relation_provision inputs that together describe a complete domain schema (e.g. e-commerce, telemedicine, habit tracker). Templates are never executed directly — they are copied into a blueprint first via copy_template_to_blueprint(). Can be private (owner-only) or public (marketplace-visible).'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.id IS 'Unique identifier for this template.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.name IS 'Machine-readable name for the template (e.g. e_commerce_basic). Must be unique per owner + version.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.version IS 'Semantic version string. Defaults to 1.0.0.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.display_name IS 'Human-readable display name for the template (e.g. E-Commerce Basic).'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.description IS 'Optional description of what the template provisions.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.owner_id IS 'The user who created or published this template.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.visibility IS 'Access control for the template. private: only the owner can see and copy. public: anyone can browse and copy from the marketplace. Defaults to private.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.categories IS 'Domain categories for marketplace browsing (e.g. e-commerce, healthcare, social). Defaults to empty array.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.tags IS 'Freeform tags for search and discovery (e.g. products, orders, payments). Defaults to empty array.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.definition IS 'The blueprint definition as a JSONB document. Contains tables[] (each with nodes[] for data behaviors via string shorthand or {"$type": "...", "data": {...}} objects, fields[], grants[], and policies[] using {"$type": "...", "data": {...}}), and relations[] (using $type for relation_type with junction config in data). This is the core payload that gets copied into a blueprint for execution.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.definition_schema_version IS 'Version of the definition format schema. Used for forward-compatible parsing. Defaults to 1.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.source IS 'Provenance of the template. user: manually created by a human. system: official curated template from the Constructive team. agent: AI-generated. Defaults to user.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.complexity IS 'Complexity indicator for marketplace filtering. simple: 3-5 tables. moderate: 6-12 tables. complex: 13+ tables. NULL if not categorized.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.copy_count IS 'Denormalized count of how many blueprints have been created from this template via copy_template_to_blueprint(). Incremented automatically. Defaults to 0.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.fork_count IS 'Denormalized count of how many derivative templates have been forked from this template. Defaults to 0.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.forked_from_id IS 'If this template was forked from another template, the ID of the parent. NULL for original templates.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.created_at IS 'Timestamp when this template was created.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.definition_hash IS 'UUIDv5 Merkle root hash of the definition. Computed automatically via trigger from the ordered table_hashes. Used for content-addressable deduplication, provenance tracking, and cross-blueprint structural comparison. NULL columns are backend-computed — clients should never set this directly.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.table_hashes IS 'JSONB map of table ref names to their individual UUIDv5 content hashes (e.g. {"products": "uuid", "categories": "uuid"}). Each table hash is computed from the canonical jsonb::text of the table entry. Enables structural comparison at the table level across different blueprints. Backend-computed via trigger.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_template.updated_at IS 'Timestamp when this template was last modified.'; + +CREATE INDEX blueprint_template_owner_id_idx ON metaschema_modules_public.blueprint_template (owner_id); + +CREATE INDEX blueprint_template_visibility_idx ON metaschema_modules_public.blueprint_template (visibility); + +CREATE INDEX blueprint_template_forked_from_id_idx ON metaschema_modules_public.blueprint_template (forked_from_id); + +CREATE INDEX blueprint_template_categories_idx ON metaschema_modules_public.blueprint_template USING gin (categories); + +CREATE INDEX blueprint_template_tags_idx ON metaschema_modules_public.blueprint_template USING gin (tags); + +CREATE INDEX blueprint_template_definition_hash_idx ON metaschema_modules_public.blueprint_template (definition_hash); + +CREATE TABLE metaschema_modules_public.blueprint ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + owner_id uuid NOT NULL, + database_id uuid NOT NULL, + name text NOT NULL, + display_name text NOT NULL, + description text, + definition jsonb NOT NULL, + template_id uuid DEFAULT NULL, + definition_hash uuid, + table_hashes jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT blueprint_unique_database_name + UNIQUE (database_id, name), + CONSTRAINT blueprint_db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT blueprint_template_fkey + FOREIGN KEY(template_id) + REFERENCES metaschema_modules_public.blueprint_template (id) +); + +COMMENT ON TABLE metaschema_modules_public.blueprint IS 'An owned, editable blueprint scoped to a specific database. Created by copying from a blueprint_template via copy_template_to_blueprint() or built from scratch. The owner can customize the definition at any time. Execute it with construct_blueprint() which creates a separate blueprint_construction record to track the build.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.id IS 'Unique identifier for this blueprint.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.owner_id IS 'The user who owns this blueprint.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.database_id IS 'The database this blueprint is scoped to. Tables created by construct_blueprint() are provisioned in this database.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.name IS 'Machine-readable name for the blueprint. Must be unique per database.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.display_name IS 'Human-readable display name for the blueprint.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.description IS 'Optional description of the blueprint.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.definition IS 'The blueprint definition as a JSONB document. Contains tables[] (each with table_name, optional schema_name, nodes[] for data behaviors, fields[], grants[], and policies[] using $type), relations[] (using $type with source_table/target_table and optional source_schema/target_schema), indexes[] (using table_name + column), and full_text_searches[] (using table_name + field + sources[]). Everything is name-based — no UUIDs in the definition.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.template_id IS 'If this blueprint was created by copying a template, the ID of the source template. NULL if built from scratch.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.created_at IS 'Timestamp when this blueprint was created.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.definition_hash IS 'UUIDv5 Merkle root hash of the definition. Computed automatically via trigger from the ordered table_hashes. Used for content-addressable deduplication and provenance tracking. Backend-computed — clients should never set this directly.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.table_hashes IS 'JSONB map of table names to their individual UUIDv5 content hashes. Each table hash is computed from the canonical jsonb::text of the table entry. Enables structural comparison at the table level across blueprints and templates. Backend-computed via trigger.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint.updated_at IS 'Timestamp when this blueprint was last modified.'; + +CREATE INDEX blueprint_owner_id_idx ON metaschema_modules_public.blueprint (owner_id); + +CREATE INDEX blueprint_database_id_idx ON metaschema_modules_public.blueprint (database_id); + +CREATE INDEX blueprint_template_id_idx ON metaschema_modules_public.blueprint (template_id); + +CREATE INDEX blueprint_definition_hash_idx ON metaschema_modules_public.blueprint (definition_hash); + +CREATE TABLE metaschema_modules_public.blueprint_construction ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + blueprint_id uuid NOT NULL, + database_id uuid NOT NULL, + schema_id uuid, + status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'constructing', 'constructed', 'failed')), + error_details text, + table_map jsonb NOT NULL DEFAULT '{}', + constructed_definition jsonb, + constructed_at timestamptz, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT blueprint_construction_blueprint_fkey + FOREIGN KEY(blueprint_id) + REFERENCES metaschema_modules_public.blueprint (id) + ON DELETE CASCADE, + CONSTRAINT blueprint_construction_db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE +); + +COMMENT ON TABLE metaschema_modules_public.blueprint_construction IS 'Tracks individual construction attempts of a blueprint. Each time construct_blueprint() is called, a new record is created here. This separates the editable blueprint definition from its build history, allowing blueprints to be re-executed, constructed into multiple databases, and maintain an audit trail of all construction attempts.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.id IS 'Unique identifier for this construction attempt.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.blueprint_id IS 'The blueprint that was constructed.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.database_id IS 'The database the blueprint was constructed into.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.schema_id IS 'The default schema used for tables that did not specify an explicit schema_name. NULL if not yet resolved.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.status IS 'Execution state of this construction attempt. pending: created but not yet started. constructing: currently executing. constructed: successfully completed. failed: execution failed (see error_details).'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.error_details IS 'Error message from a failed construction attempt. NULL unless status is failed.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.table_map IS 'Mapping of table names to created table UUIDs, populated after successful construction. Format: {"products": "uuid", "categories": "uuid", ...}. Defaults to empty object.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.constructed_definition IS 'Immutable snapshot of the definition at construct-time. Preserved so the exact definition that was executed is recorded even if the user later modifies the blueprint definition.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.constructed_at IS 'Timestamp when construction successfully completed. NULL until constructed.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.created_at IS 'Timestamp when this construction attempt was created.'; + +COMMENT ON COLUMN metaschema_modules_public.blueprint_construction.updated_at IS 'Timestamp when this construction attempt was last modified.'; + +CREATE INDEX blueprint_construction_blueprint_id_idx ON metaschema_modules_public.blueprint_construction (blueprint_id); + +CREATE INDEX blueprint_construction_database_id_idx ON metaschema_modules_public.blueprint_construction (database_id); + +CREATE INDEX blueprint_construction_status_idx ON metaschema_modules_public.blueprint_construction (status); + +CREATE TABLE metaschema_modules_public.storage_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + buckets_table_id uuid NOT NULL DEFAULT uuid_nil(), + files_table_id uuid NOT NULL DEFAULT uuid_nil(), + buckets_table_name text NOT NULL DEFAULT 'buckets', + files_table_name text NOT NULL DEFAULT 'files', + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', + policies jsonb NULL, + provisions jsonb NULL, + entity_table_id uuid NULL, + endpoint text NULL, + public_url_prefix text NULL, + provider text NULL, + allowed_origins text[] NULL, + restrict_reads boolean NOT NULL DEFAULT false, + has_path_shares boolean NOT NULL DEFAULT false, + path_shares_table_id uuid NULL DEFAULT NULL, + upload_url_expiry_seconds int NULL, + download_url_expiry_seconds int NULL, + default_max_file_size bigint NULL, + max_filename_length int NULL, + cache_ttl_seconds int NULL, + max_bulk_files int NULL, + max_bulk_total_size bigint NULL, + has_versioning boolean NOT NULL DEFAULT false, + has_content_hash boolean NOT NULL DEFAULT false, + has_custom_keys boolean NOT NULL DEFAULT false, + has_audit_log boolean NOT NULL DEFAULT false, + has_confirm_upload boolean NOT NULL DEFAULT false, + confirm_upload_delay interval NOT NULL DEFAULT '30 seconds', + file_events_table_id uuid NULL DEFAULT NULL, + default_permissions text[] DEFAULT NULL, + api_name text DEFAULT 'admin', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT buckets_table_fkey + FOREIGN KEY(buckets_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT files_table_fkey + FOREIGN KEY(files_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT entity_table_fkey + FOREIGN KEY(entity_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT path_shares_table_fkey + FOREIGN KEY(path_shares_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT file_events_table_fkey + FOREIGN KEY(file_events_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX storage_module_database_id_idx ON metaschema_modules_public.storage_module (database_id); + +CREATE UNIQUE INDEX storage_module_unique_scope ON metaschema_modules_public.storage_module (database_id, scope, prefix); + +CREATE TABLE metaschema_modules_public.entity_type_provision ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + name text NOT NULL, + prefix text NOT NULL, + description text NOT NULL DEFAULT '', + parent_entity text NOT NULL DEFAULT 'org', + table_name text DEFAULT NULL, + is_visible boolean NOT NULL DEFAULT true, + has_limits boolean NOT NULL DEFAULT false, + has_profiles boolean NOT NULL DEFAULT false, + has_levels boolean NOT NULL DEFAULT false, + has_invites boolean NOT NULL DEFAULT false, + has_invite_achievements boolean NOT NULL DEFAULT false, + storage jsonb DEFAULT NULL, + namespaces jsonb DEFAULT NULL, + functions jsonb DEFAULT NULL, + graphs jsonb DEFAULT NULL, + agents jsonb DEFAULT NULL, + skip_entity_policies boolean NOT NULL DEFAULT false, + table_provision jsonb DEFAULT NULL, + out_membership_type int DEFAULT NULL, + out_entity_table_id uuid DEFAULT NULL, + out_entity_table_name text DEFAULT NULL, + out_installed_modules text[] DEFAULT NULL, + out_storage_module_id uuid DEFAULT NULL, + out_buckets_table_id uuid DEFAULT NULL, + out_files_table_id uuid DEFAULT NULL, + out_path_shares_table_id uuid DEFAULT NULL, + out_invites_module_id uuid DEFAULT NULL, + out_namespace_module_id uuid DEFAULT NULL, + out_namespaces_table_id uuid DEFAULT NULL, + out_namespace_events_table_id uuid DEFAULT NULL, + out_function_module_id uuid DEFAULT NULL, + out_definitions_table_id uuid DEFAULT NULL, + out_invocations_table_id uuid DEFAULT NULL, + out_execution_logs_table_id uuid DEFAULT NULL, + out_secret_definitions_table_id uuid DEFAULT NULL, + out_graph_module_id uuid DEFAULT NULL, + out_graphs_table_id uuid DEFAULT NULL, + out_agent_module_id uuid DEFAULT NULL, + CONSTRAINT entity_type_provision_unique_prefix + UNIQUE (database_id, prefix), + CONSTRAINT entity_type_provision_db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE +); + +COMMENT ON TABLE metaschema_modules_public.entity_type_provision IS 'Provisions a new membership entity type. Each INSERT creates an entity table, registers a membership type, + and installs the required modules (permissions, memberships, limits) plus optional modules (profiles, levels, invites). + Uses provision_membership_table() internally. Graceful: duplicate (database_id, prefix) pairs are silently skipped + via the unique constraint (use INSERT ... ON CONFLICT DO NOTHING). + Policy behavior: by default the five entity-table RLS policies are applied (gated by is_visible). + Set table_provision to a single jsonb object (using the same shape as provision_table() / + blueprint tables[] entries) to replace the defaults with your own; set skip_entity_policies=true + as an escape hatch to apply zero policies.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.id IS 'Unique identifier for this provision row.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.database_id IS 'The database to provision this entity type in. Required.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.name IS 'Human-readable name for this entity type, e.g. ''Data Room'', ''Team Channel''. Required. + Stored in the entity_types registry table.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.prefix IS 'SQL prefix used for table and module naming, e.g. ''data_room'', ''team_channel''. Required. + Drives entity table name (prefix || ''s'' by default), module labels (permissions_module:prefix), + and membership table names (prefix_memberships, prefix_members, etc.). + Must be unique per database — the (database_id, prefix) constraint ensures graceful ON CONFLICT DO NOTHING.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.description IS 'Description of this entity type. Stored in the entity_types registry table. Defaults to empty string.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.parent_entity IS 'Prefix of the parent entity type. The trigger resolves this to a membership_type integer + by looking up memberships_module WHERE prefix = parent_entity. + Defaults to ''org'' (the organization-level type). For nested types, set to the parent''s prefix + (e.g. ''data_room'' for a team_channel nested under data_room). + The parent type must already be provisioned before this INSERT.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.table_name IS 'Override the entity table name. When NULL (default), the table name is derived as prefix || ''s'' + (e.g. prefix ''data_room'' produces table ''data_rooms''). + Set this when the pluralization rule doesn''t apply (e.g. prefix ''staff'' should produce ''staff'' not ''staffs'').'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.is_visible IS 'Whether members of the parent entity can see child entities. Defaults to true. + When true: a SELECT policy allows parent members to list child entities (e.g. org members can see all data rooms). + When false: only direct members of the entity itself can see it (private entity mode). + Controls whether the parent_member SELECT policy is created on the entity table. + Only meaningful on the defaults path — ignored (no-op) when table_provision is non-NULL or + skip_entity_policies=true, since no default policies are being applied in those cases.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.has_limits IS 'Whether to apply limits_module security for this type. Defaults to false. + The limits_module table structure is always created (memberships_module requires it), + but when false, no RLS policies are applied to the limits tables. + Set to true if this entity type needs configurable resource limits per membership.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.has_profiles IS 'Whether to provision profiles_module for this type. Defaults to false. + Profiles provide named permission roles (e.g. ''Editor'', ''Viewer'') with pre-configured permission bitmasks. + When true, creates profile tables and applies profiles security.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.has_levels IS 'Whether to provision events_module for this type. Defaults to false. + Levels provide gamification/achievement tracking for members. + When true, creates level steps, achievements, and level tables with security.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.has_invites IS 'Whether to provision invites_module for this type. Defaults to false. + When true, the trigger inserts a row into invites_module which in turn + (via insert_invites_module BEFORE INSERT) creates {prefix}_invites and + {prefix}_claimed_invites tables plus the submit_{prefix}_invite_code() function. + Re-provisioning is idempotent: the UNIQUE (database_id, membership_type) constraint + on invites_module combined with ON CONFLICT DO NOTHING in the fan-out makes + repeated INSERTs safe.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.has_invite_achievements IS 'Whether to auto-attach an EventTracker to the claimed_invites table for invite-based + achievements. Defaults to false. Requires has_invites=true AND has_levels=true. + When true, the trigger calls event_tracker() on the claimed_invites table with + event_name=''invite_claimed'', actor_field=''sender_id'', events=[''INSERT''], + crediting the SENDER (inviter) when someone claims their invite code. + Developers can then define achievements in the blueprint achievements[] section + that reference the ''invite_claimed'' event (e.g., "Invite 5 friends" = count: 5).'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.skip_entity_policies IS 'Escape hatch: when true, apply zero RLS policies to the entity table. Defaults to false. + Use this only when you want the entity table provisioned with zero policies (e.g. because you + plan to insert secure_table_provision rows yourself later). In most cases, prefer leaving this + false and either accepting the five defaults (table_provision=NULL) or overriding them via + table_provision. + Defaults (applied when table_provision IS NULL and skip_entity_policies=false): + - SELECT (parent_member): parent entity members can see child entities (only when is_visible=true) + - SELECT (self_member): direct members of the entity can see it + - INSERT: create_entity permission on the parent entity + - UPDATE: admin_entity permission on the entity itself + - DELETE: owner of the entity can delete it'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.table_provision IS 'Single jsonb object describing the full security setup to apply to the entity table. + Uses the same vocabulary as metaschema_modules_public.provision_table() and blueprint tables[] + entries, so an entity table is configured the same way an ordinary blueprint table is. + Defaults to NULL; when non-NULL, the five default policies are implicitly replaced by + table_provision.policies[] (is_visible becomes a no-op on this path). + Recognized keys (all optional): + - use_rls (boolean, default true) + - nodes (jsonb array of {"$type","data"} Data* module entries) + - fields (jsonb array of field objects: name,type,is_required,default,min,max,regexp,index) + - grants (jsonb array of grant objects; each with roles[] and privileges[]) + - policies (jsonb array of policy objects; each with $type, privileges, data, name, role, permissive) + The trigger forwards all setup (nodes/fields/grants/policies) as a single secure_table_provision row + against the newly created entity table. + Example — override with two SELECT policies: + table_provision := jsonb_build_object( + ''policies'', jsonb_build_array( + jsonb_build_object( + ''$type'', ''AuthzEntityMembership'', + ''privileges'', jsonb_build_array(''select''), + ''data'', jsonb_build_object(''entity_field'', ''id'', ''membership_type'', 3), + ''name'', ''self_member'' + ), + jsonb_build_object( + ''$type'', ''AuthzDirectOwner'', + ''privileges'', jsonb_build_array(''select'', ''update''), + ''data'', jsonb_build_object(''owner_field'', ''owner_id'') + ) + ) + )'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_membership_type IS 'Output: the auto-assigned integer membership type ID. Populated by the trigger after successful provisioning. + This is the ID used in entity_types, memberships_module, and all module tables.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_entity_table_id IS 'Output: the UUID of the created entity table. Populated by the trigger. + Use this to reference the entity table in subsequent relation_provision or secure_table_provision rows.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_entity_table_name IS 'Output: the name of the created entity table (e.g. ''data_rooms''). Populated by the trigger.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_installed_modules IS 'Output: array of installed module labels (e.g. ARRAY[''permissions_module:data_room'', ''memberships_module:data_room'', ''invites_module:data_room'']). + Populated by the trigger. Useful for verifying which modules were provisioned.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.storage IS 'Optional JSON array of storage module definitions. Presence triggers provisioning + (same inference model as namespaces, functions, agents). + Each element provisions a separate storage module with its own tables + ({prefix}_{key}_buckets/files), RLS policies, and feature flags. + NULL = do not provision storage. ''[{}]'' = provision one default storage module. + Each array element recognizes (all optional): + - key (text) module discriminator, max 16 chars, lowercase snake_case. + Defaults to ''default'' (omitted from table names). + Non-default keys become infixes: {prefix}_{key}_buckets. + (storage_key accepted for backward compat) + - upload_url_expiry_seconds (integer) presigned PUT URL expiry override + - download_url_expiry_seconds (integer) presigned GET URL expiry override + - default_max_file_size (bigint) global max file size in bytes for this module + - allowed_origins (text[]) default CORS origins for all buckets in this module + - restrict_reads (boolean) require read_files permission for SELECT on files + - has_path_shares (boolean) enable virtual filesystem + path share policies + - has_versioning (boolean) enable file version chains + - has_content_hash (boolean) enable content hash for dedup + - has_custom_keys (boolean) allow client-provided S3 keys + - has_audit_log (boolean) enable file events audit table + - has_confirm_upload (boolean) enable HeadObject confirmation flow + - confirm_upload_delay (interval) delay before first confirmation attempt + - buckets (jsonb[]) array of initial bucket definitions to seed. + Each bucket: { name (required), description, is_public, allowed_mime_types, max_file_size, allowed_origins } + - provisions (jsonb object) per-table customization keyed by "files" or "buckets". + Each value: { nodes, fields, grants, use_rls, policies }. + Example (single module, backward compat): + storage := ''[{"buckets": [{"name": "documents"}]}]''::jsonb + Example (multi-module): + storage := ''[{"has_path_shares": true, "buckets": [{"name": "documents"}]}, {"key": "fn", "has_custom_keys": true, "buckets": [{"name": "functions"}]}]''::jsonb'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_storage_module_id IS 'Output: the UUID of the storage_module row created for this entity type. Populated by the trigger when storage is non-NULL and non-empty.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_buckets_table_id IS 'Output: the UUID of the generated buckets table (e.g. data_room_buckets). Populated by the trigger when storage is non-NULL and non-empty.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_files_table_id IS 'Output: the UUID of the generated files table (e.g. data_room_files). Populated by the trigger when storage is non-NULL and non-empty.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_invites_module_id IS 'Output: the UUID of the invites_module row created for this entity type. Populated by the trigger when has_invites=true. + NULL when has_invites=false, or when re-provisioning hits ON CONFLICT DO NOTHING + (i.e. the invites_module row was created in a previous run).'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.namespaces IS 'Optional JSON array of namespace module definitions. Presence triggers provisioning. + NULL = do not provision namespaces. ''[{}]'' = provision one default namespace module. + Each element recognizes (all optional): + - key (text) module discriminator. Defaults to ''default''. + - policies (jsonb array) RLS policy overrides. NULL = apply defaults from apply_namespace_security(). + Creates {prefix}_namespaces (or {prefix}_{key}_namespaces for non-default keys) + with entity-scoped RLS (AuthzEntityMembership) and a rename proxy trigger. + Registers manage_namespaces permission bit on first provision. + Example: namespaces := ''[{}]''::jsonb'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.functions IS 'Optional JSON array of function module definitions. Presence triggers provisioning. + NULL = do not provision functions. ''[{}]'' = provision one default function module. + Each element recognizes (all optional): + - key (text) module discriminator. Defaults to ''default''. + - policies (jsonb array) RLS policy overrides. NULL = apply defaults from apply_function_security(). + Creates {prefix}_function_definitions (or {prefix}_{key}_function_definitions for non-default keys) + with entity-scoped RLS and a job trigger dispatching function:provision tasks. + Registers manage_functions + invoke_functions permission bits on first provision. + Example: functions := ''[{}]''::jsonb'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.graphs IS 'Optional JSON array of graph module definitions. Presence triggers provisioning. + NULL = do not provision graphs. ''[{}]'' = provision one default graph module. + Each element recognizes (all optional): + - key (text) module discriminator. Defaults to ''default''. + - policies (jsonb array) RLS policy overrides. NULL = apply defaults from apply_graph_security(). + Registers manage_graphs + execute_graphs permission bits on first provision. + Graph module requires a merkle_store_module_id dependency, so entity_type_provision + only registers permissions here. The graph module itself must be provisioned + separately with the merkle store dependency resolved. + Example: graphs := ''[{}]''::jsonb'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_namespace_module_id IS 'Output: the UUID of the namespace_module row created (or found) for this entity type. + Populated by the trigger when namespaces is non-NULL. NULL otherwise.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_namespaces_table_id IS 'Output: the UUID of the generated namespaces table (e.g. data_room_namespaces). + Populated by the trigger when namespaces is non-NULL. NULL otherwise.'; + +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_namespace_events_table_id IS 'Output: the UUID of the generated namespace_events partitioned table (e.g. data_room_namespace_events). + Monthly partitioned, 12-month retention. Populated by the trigger when namespaces is non-NULL. NULL otherwise.'; + +CREATE TABLE metaschema_modules_public.rate_limits_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + rate_limit_settings_table_id uuid NOT NULL DEFAULT uuid_nil(), + ip_rate_limits_table_id uuid NOT NULL DEFAULT uuid_nil(), + rate_limits_table_id uuid NOT NULL DEFAULT uuid_nil(), + rate_limit_settings_table text NOT NULL DEFAULT 'app_settings_rate_limit', + ip_rate_limits_table text NOT NULL DEFAULT 'auth_ip_rate_limits', + rate_limits_table text NOT NULL DEFAULT 'auth_rate_limits', + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT rate_limit_settings_table_fkey + FOREIGN KEY(rate_limit_settings_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT ip_rate_limits_table_fkey + FOREIGN KEY(ip_rate_limits_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT rate_limits_table_fkey + FOREIGN KEY(rate_limits_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT rate_limits_module_database_id_uniq + UNIQUE (database_id) +); + +CREATE INDEX rate_limits_module_database_id_idx ON metaschema_modules_public.rate_limits_module (database_id); + +COMMENT ON CONSTRAINT rate_limit_settings_table_fkey ON metaschema_modules_public.rate_limits_module IS '@fieldName rateLimitSettingsTableByRateLimitSettingsTableId'; + +COMMENT ON CONSTRAINT ip_rate_limits_table_fkey ON metaschema_modules_public.rate_limits_module IS '@fieldName ipRateLimitsTableByIpRateLimitsTableId'; + +COMMENT ON CONSTRAINT rate_limits_table_fkey ON metaschema_modules_public.rate_limits_module IS '@fieldName rateLimitsTableByRateLimitsTableId'; + +CREATE TABLE metaschema_modules_public.devices_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + user_devices_table_id uuid NOT NULL DEFAULT uuid_nil(), + device_settings_table_id uuid NOT NULL DEFAULT uuid_nil(), + user_devices_table text NOT NULL DEFAULT 'auth_user_devices', + device_settings_table text NOT NULL DEFAULT 'app_settings_device', + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT user_devices_table_fkey + FOREIGN KEY(user_devices_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT device_settings_table_fkey + FOREIGN KEY(device_settings_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT devices_module_database_id_uniq + UNIQUE (database_id) +); + +CREATE INDEX devices_module_database_id_idx ON metaschema_modules_public.devices_module (database_id); + +COMMENT ON CONSTRAINT user_devices_table_fkey ON metaschema_modules_public.devices_module IS '@fieldName userDevicesTableByUserDevicesTableId'; + +COMMENT ON CONSTRAINT device_settings_table_fkey ON metaschema_modules_public.devices_module IS '@fieldName deviceSettingsTableByDeviceSettingsTableId'; + +CREATE TABLE metaschema_modules_public.session_secrets_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT 'session_secrets', + sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT sessions_table_fkey + FOREIGN KEY(sessions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX session_secrets_module_database_id_idx ON metaschema_modules_public.session_secrets_module (database_id); + +CREATE INDEX session_secrets_module_schema_id_idx ON metaschema_modules_public.session_secrets_module (schema_id); + +CREATE INDEX session_secrets_module_table_id_idx ON metaschema_modules_public.session_secrets_module (table_id); + +CREATE INDEX session_secrets_module_sessions_table_id_idx ON metaschema_modules_public.session_secrets_module (sessions_table_id); + +COMMENT ON TABLE metaschema_modules_public.session_secrets_module IS 'Config row for the session_secrets_module, which provisions a DB-private, session-scoped ephemeral key-value store for challenges, nonces, and one-time tokens that must never be readable by end users.'; + +COMMENT ON COLUMN metaschema_modules_public.session_secrets_module.sessions_table_id IS 'Resolved reference to sessions_module.sessions_table, used to FK session_secrets.session_id with ON DELETE CASCADE.'; + +CREATE TABLE metaschema_modules_public.webauthn_credentials_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + owner_table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT 'webauthn_credentials', + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT owner_table_fkey + FOREIGN KEY(owner_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE +); + +CREATE INDEX webauthn_credentials_module_database_id_idx ON metaschema_modules_public.webauthn_credentials_module (database_id); + +COMMENT ON TABLE metaschema_modules_public.webauthn_credentials_module IS 'Config row for the webauthn_credentials_module, which provisions the per-user WebAuthn/passkey credentials table (public key, counter, transports, device type, backup state) mirroring crypto_addresses_module. The sibling webauthn_auth_module holds RP config and the registration/sign-in challenge state.'; + +COMMENT ON COLUMN metaschema_modules_public.webauthn_credentials_module.private_schema_id IS 'Private schema that hosts SECURITY DEFINER helpers which write to webauthn_credentials (registration / counter-bump / delete).'; + +CREATE TABLE metaschema_modules_public.webauthn_auth_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + users_table_id uuid NOT NULL DEFAULT uuid_nil(), + credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), + sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), + session_credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), + session_secrets_table_id uuid NOT NULL DEFAULT uuid_nil(), + auth_settings_table_id uuid NOT NULL DEFAULT uuid_nil(), + rp_id text NOT NULL DEFAULT '', + rp_name text NOT NULL DEFAULT '', + origin_allowlist text[] NOT NULL DEFAULT '{}', + attestation_type text NOT NULL DEFAULT 'none' CHECK (attestation_type IN ('none', 'indirect', 'direct', 'enterprise')), + require_user_verification boolean NOT NULL DEFAULT false, + resident_key text NOT NULL DEFAULT 'required' CHECK (resident_key IN ('discouraged', 'preferred', 'required')), + challenge_expiry interval NOT NULL DEFAULT '5 minutes', + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT users_table_fkey + FOREIGN KEY(users_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT credentials_table_fkey + FOREIGN KEY(credentials_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT sessions_table_fkey + FOREIGN KEY(sessions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT session_credentials_table_fkey + FOREIGN KEY(session_credentials_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT session_secrets_table_fkey + FOREIGN KEY(session_secrets_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT auth_settings_table_fkey + FOREIGN KEY(auth_settings_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX webauthn_auth_module_database_id_idx ON metaschema_modules_public.webauthn_auth_module (database_id); + +CREATE TABLE metaschema_modules_public.identity_providers_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT 'identity_providers', + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', + entity_table_id uuid NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT entity_table_fkey + FOREIGN KEY(entity_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX identity_providers_module_database_id_idx ON metaschema_modules_public.identity_providers_module (database_id); + +CREATE INDEX identity_providers_module_schema_id_idx ON metaschema_modules_public.identity_providers_module (schema_id); + +CREATE INDEX identity_providers_module_private_schema_id_idx ON metaschema_modules_public.identity_providers_module (private_schema_id); + +CREATE INDEX identity_providers_module_table_id_idx ON metaschema_modules_public.identity_providers_module (table_id); + +CREATE UNIQUE INDEX identity_providers_module_unique_scope ON metaschema_modules_public.identity_providers_module (database_id, scope); + +COMMENT ON TABLE metaschema_modules_public.identity_providers_module IS 'Entity-aware config row for the identity_providers_module, which provisions a per-database + identity_providers table holding OAuth2 / OIDC (and future SAML) provider definitions. + The scope column determines which config_secrets_module table the rotate proc targets + (app_secrets for app scope, org_secrets for org scope). When scope = platform, + the secrets table gets a database_id column and platform-level RLS via + AuthzRelatedEntityMembership through database.owner_id. + Scoping matrix: + scope=app → per-database flat, in-app admin manages + scope=platform → per-database, platform admin manages (generate:constructive) + scope=org → per-org tenant, org admin manages'; + +COMMENT ON COLUMN metaschema_modules_public.identity_providers_module.private_schema_id IS 'Private schema that hosts SECURITY DEFINER admin helpers which write to identity_providers (create / update / enable / disable / rotate-secret / delete) and the per-app quota check.'; + +CREATE TABLE metaschema_modules_public.notifications_module ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + notifications_table_id uuid NOT NULL DEFAULT uuid_nil(), + read_state_table_id uuid NOT NULL DEFAULT uuid_nil(), + preferences_table_id uuid, + channels_table_id uuid, + delivery_log_table_id uuid, + owner_table_id uuid NOT NULL DEFAULT uuid_nil(), + user_settings_table_id uuid, + organization_settings_table_id uuid, + has_channels boolean NOT NULL DEFAULT true, + has_preferences boolean NOT NULL DEFAULT true, + has_settings_extension boolean NOT NULL DEFAULT false, + has_digest_metadata boolean NOT NULL DEFAULT false, + has_subscriptions boolean NOT NULL DEFAULT false, + default_permissions text[] DEFAULT NULL, + api_name text DEFAULT 'notifications', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT notifications_table_fkey + FOREIGN KEY(notifications_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT read_state_table_fkey + FOREIGN KEY(read_state_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT preferences_table_fkey + FOREIGN KEY(preferences_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE SET NULL, + CONSTRAINT channels_table_fkey + FOREIGN KEY(channels_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE SET NULL, + CONSTRAINT delivery_log_table_fkey + FOREIGN KEY(delivery_log_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE SET NULL, + CONSTRAINT owner_table_fkey + FOREIGN KEY(owner_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT user_settings_table_fkey + FOREIGN KEY(user_settings_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE SET NULL, + CONSTRAINT organization_settings_table_fkey + FOREIGN KEY(organization_settings_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE SET NULL, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE +); + +COMMENT ON CONSTRAINT schema_fkey ON metaschema_modules_public.notifications_module IS '@omit manyToMany'; + +COMMENT ON CONSTRAINT private_schema_fkey ON metaschema_modules_public.notifications_module IS '@omit manyToMany'; + +COMMENT ON CONSTRAINT notifications_table_fkey ON metaschema_modules_public.notifications_module IS '@fieldName notificationsTableByNotificationsTableId +@omit manyToMany'; + +COMMENT ON CONSTRAINT read_state_table_fkey ON metaschema_modules_public.notifications_module IS '@fieldName readStateTableByReadStateTableId +@omit manyToMany'; + +COMMENT ON CONSTRAINT preferences_table_fkey ON metaschema_modules_public.notifications_module IS '@fieldName preferencesTableByPreferencesTableId +@omit manyToMany'; + +COMMENT ON CONSTRAINT channels_table_fkey ON metaschema_modules_public.notifications_module IS '@fieldName channelsTableByChannelsTableId +@omit manyToMany'; + +COMMENT ON CONSTRAINT delivery_log_table_fkey ON metaschema_modules_public.notifications_module IS '@fieldName deliveryLogTableByDeliveryLogTableId +@omit manyToMany'; + +COMMENT ON CONSTRAINT owner_table_fkey ON metaschema_modules_public.notifications_module IS '@omit manyToMany'; + +COMMENT ON CONSTRAINT user_settings_table_fkey ON metaschema_modules_public.notifications_module IS '@fieldName userSettingsTableByUserSettingsTableId +@omit manyToMany'; + +COMMENT ON CONSTRAINT organization_settings_table_fkey ON metaschema_modules_public.notifications_module IS '@fieldName organizationSettingsTableByOrganizationSettingsTableId +@omit manyToMany'; + +COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.notifications_module IS '@omit manyToMany'; + +CREATE INDEX notifications_module_database_id_idx ON metaschema_modules_public.notifications_module (database_id); + +CREATE TABLE metaschema_modules_public.plans_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + plans_table_id uuid NOT NULL DEFAULT uuid_nil(), + plans_table_name text NOT NULL DEFAULT '', + plan_limits_table_id uuid NOT NULL DEFAULT uuid_nil(), + plan_limits_table_name text NOT NULL DEFAULT '', + plan_pricing_table_id uuid NULL, + plan_overrides_table_id uuid NULL, + plan_meter_limits_table_id uuid NULL, + plan_caps_table_id uuid NULL, + apply_plan_function text NOT NULL DEFAULT '', + apply_plan_aggregate_function text NOT NULL DEFAULT '', + apply_billing_plan_function text NULL, + apply_plan_caps_function text NULL, + prefix text NULL, + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT plans_table_fkey + FOREIGN KEY(plans_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT plan_limits_table_fkey + FOREIGN KEY(plan_limits_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT plan_pricing_table_fkey + FOREIGN KEY(plan_pricing_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT plan_overrides_table_fkey + FOREIGN KEY(plan_overrides_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT plan_meter_limits_table_fkey + FOREIGN KEY(plan_meter_limits_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT plan_caps_table_fkey + FOREIGN KEY(plan_caps_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT plans_module_database_id_unique + UNIQUE (database_id) +); + +CREATE INDEX plans_module_database_id_idx ON metaschema_modules_public.plans_module (database_id); + +CREATE TABLE metaschema_modules_public.billing_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + meters_table_id uuid NOT NULL DEFAULT uuid_nil(), + meters_table_name text NOT NULL DEFAULT '', + plan_subscriptions_table_id uuid NOT NULL DEFAULT uuid_nil(), + plan_subscriptions_table_name text NOT NULL DEFAULT '', + ledger_table_id uuid NOT NULL DEFAULT uuid_nil(), + ledger_table_name text NOT NULL DEFAULT '', + balances_table_id uuid NOT NULL DEFAULT uuid_nil(), + balances_table_name text NOT NULL DEFAULT '', + meter_credits_table_id uuid NOT NULL DEFAULT uuid_nil(), + meter_credits_table_name text NOT NULL DEFAULT '', + meter_sources_table_id uuid NOT NULL DEFAULT uuid_nil(), + meter_sources_table_name text NOT NULL DEFAULT '', + meter_defaults_table_id uuid NOT NULL DEFAULT uuid_nil(), + meter_defaults_table_name text NOT NULL DEFAULT '', + record_usage_function text NOT NULL DEFAULT '', + prefix text NULL, + default_permissions text[] DEFAULT NULL, + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT meters_table_fkey + FOREIGN KEY(meters_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT plan_subscriptions_table_fkey + FOREIGN KEY(plan_subscriptions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT ledger_table_fkey + FOREIGN KEY(ledger_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT balances_table_fkey + FOREIGN KEY(balances_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT meter_credits_table_fkey + FOREIGN KEY(meter_credits_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT meter_sources_table_fkey + FOREIGN KEY(meter_sources_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT meter_defaults_table_fkey + FOREIGN KEY(meter_defaults_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT billing_module_database_id_unique + UNIQUE (database_id) +); + +CREATE INDEX billing_module_database_id_idx ON metaschema_modules_public.billing_module (database_id); + +CREATE TABLE metaschema_modules_public.billing_provider_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + provider text NOT NULL DEFAULT 'stripe', + products_table_id uuid NULL, + prices_table_id uuid NULL, + subscriptions_table_id uuid NULL, + billing_customers_table_id uuid NOT NULL DEFAULT uuid_nil(), + billing_customers_table_name text NOT NULL DEFAULT '', + billing_products_table_id uuid NOT NULL DEFAULT uuid_nil(), + billing_products_table_name text NOT NULL DEFAULT '', + billing_prices_table_id uuid NOT NULL DEFAULT uuid_nil(), + billing_prices_table_name text NOT NULL DEFAULT '', + billing_subscriptions_table_id uuid NOT NULL DEFAULT uuid_nil(), + billing_subscriptions_table_name text NOT NULL DEFAULT '', + billing_webhook_events_table_id uuid NOT NULL DEFAULT uuid_nil(), + billing_webhook_events_table_name text NOT NULL DEFAULT '', + process_billing_event_function text NOT NULL DEFAULT '', + prefix text NULL, + api_name text DEFAULT NULL, + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT billing_customers_table_fkey + FOREIGN KEY(billing_customers_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT billing_products_table_fkey + FOREIGN KEY(billing_products_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT billing_prices_table_fkey + FOREIGN KEY(billing_prices_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT billing_subscriptions_table_fkey + FOREIGN KEY(billing_subscriptions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT billing_webhook_events_table_fkey + FOREIGN KEY(billing_webhook_events_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT products_table_fkey + FOREIGN KEY(products_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE SET NULL, + CONSTRAINT prices_table_fkey + FOREIGN KEY(prices_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE SET NULL, + CONSTRAINT subscriptions_table_fkey + FOREIGN KEY(subscriptions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE SET NULL, + CONSTRAINT billing_provider_module_database_id_unique + UNIQUE (database_id) +); + +CREATE INDEX billing_provider_module_database_id_idx ON metaschema_modules_public.billing_provider_module (database_id); + +CREATE TABLE metaschema_modules_public.realtime_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + subscriptions_schema_id uuid NOT NULL DEFAULT uuid_nil(), + change_log_table_id uuid NOT NULL DEFAULT uuid_nil(), + listener_node_table_id uuid NOT NULL DEFAULT uuid_nil(), + source_registry_table_id uuid NOT NULL DEFAULT uuid_nil(), + retention_hours int NOT NULL DEFAULT 168, + premake int NOT NULL DEFAULT 7, + interval text NOT NULL DEFAULT '1 day', + notify_channel text NULL, + api_name text DEFAULT 'realtime', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT subscriptions_schema_fkey + FOREIGN KEY(subscriptions_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT change_log_table_fkey + FOREIGN KEY(change_log_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT listener_node_table_fkey + FOREIGN KEY(listener_node_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT source_registry_table_fkey + FOREIGN KEY(source_registry_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX realtime_module_database_id_idx ON metaschema_modules_public.realtime_module (database_id); + +CREATE UNIQUE INDEX realtime_module_unique_per_db ON metaschema_modules_public.realtime_module (database_id); + +CREATE INDEX realtime_module_schema_id_idx ON metaschema_modules_public.realtime_module (schema_id); + +CREATE INDEX realtime_module_private_schema_id_idx ON metaschema_modules_public.realtime_module (private_schema_id); + +CREATE INDEX realtime_module_subscriptions_schema_id_idx ON metaschema_modules_public.realtime_module (subscriptions_schema_id); + +CREATE INDEX realtime_module_change_log_table_id_idx ON metaschema_modules_public.realtime_module (change_log_table_id); + +CREATE INDEX realtime_module_listener_node_table_id_idx ON metaschema_modules_public.realtime_module (listener_node_table_id); + +CREATE INDEX realtime_module_source_registry_table_id_idx ON metaschema_modules_public.realtime_module (source_registry_table_id); + +CREATE TABLE metaschema_modules_public.rate_limit_meters_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + rate_limit_state_table_id uuid NOT NULL DEFAULT uuid_nil(), + rate_limit_state_table_name text NOT NULL DEFAULT '', + rate_limit_overrides_table_id uuid NULL, + rate_limit_overrides_table_name text NOT NULL DEFAULT '', + rate_window_limits_table_id uuid NULL, + rate_window_limits_table_name text NOT NULL DEFAULT '', + check_rate_limit_function text NOT NULL DEFAULT '', + prefix text NULL, + default_permissions text[] DEFAULT NULL, + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT rate_limit_state_table_fkey + FOREIGN KEY(rate_limit_state_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT rate_limit_overrides_table_fkey + FOREIGN KEY(rate_limit_overrides_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT rate_window_limits_table_fkey + FOREIGN KEY(rate_window_limits_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT rate_limit_meters_module_database_id_unique + UNIQUE (database_id) +); + +CREATE INDEX rate_limit_meters_module_database_id_idx ON metaschema_modules_public.rate_limit_meters_module (database_id); + +COMMENT ON CONSTRAINT rate_limit_state_table_fkey ON metaschema_modules_public.rate_limit_meters_module IS '@fieldName rateLimitStateTableByRateLimitStateTableId'; + +COMMENT ON CONSTRAINT rate_limit_overrides_table_fkey ON metaschema_modules_public.rate_limit_meters_module IS '@fieldName rateLimitOverridesTableByRateLimitOverridesTableId'; + +COMMENT ON CONSTRAINT rate_window_limits_table_fkey ON metaschema_modules_public.rate_limit_meters_module IS '@fieldName rateWindowLimitsTableByRateWindowLimitsTableId'; + +CREATE TABLE metaschema_modules_public.config_secrets_org_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT 'org_secrets', + api_name text DEFAULT 'config', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX config_secrets_org_module_database_id_idx ON metaschema_modules_public.config_secrets_org_module (database_id); + +CREATE INDEX config_secrets_org_module_schema_id_idx ON metaschema_modules_public.config_secrets_org_module (schema_id); + +CREATE INDEX config_secrets_org_module_table_id_idx ON metaschema_modules_public.config_secrets_org_module (table_id); + +COMMENT ON TABLE metaschema_modules_public.config_secrets_org_module IS 'Config row for the config_secrets_org_module, which provisions an organization-scoped encrypted key-value secrets store with manage_secrets permission and entity-membership RLS.'; + +CREATE TABLE metaschema_modules_public.inference_log_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + inference_log_table_id uuid NOT NULL DEFAULT uuid_nil(), + inference_log_table_name text NOT NULL DEFAULT '', + usage_daily_table_id uuid NOT NULL DEFAULT uuid_nil(), + usage_daily_table_name text NOT NULL DEFAULT '', + interval text NOT NULL DEFAULT '1 month', + retention text NOT NULL DEFAULT '12 months', + premake int NOT NULL DEFAULT 2, + scope text NOT NULL DEFAULT 'app', + actor_fk_table_id uuid NULL, + entity_fk_table_id uuid NULL, + prefix text NOT NULL DEFAULT '', + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT inference_log_table_fkey + FOREIGN KEY(inference_log_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT usage_daily_table_fkey + FOREIGN KEY(usage_daily_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT inference_log_module_database_id_prefix_unique + UNIQUE NULLS NOT DISTINCT (database_id, prefix) +); + +CREATE INDEX inference_log_module_database_id_idx ON metaschema_modules_public.inference_log_module (database_id); + +CREATE TABLE metaschema_modules_public.compute_log_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + compute_log_table_id uuid NOT NULL DEFAULT uuid_nil(), + compute_log_table_name text NOT NULL DEFAULT '', + usage_daily_table_id uuid NOT NULL DEFAULT uuid_nil(), + usage_daily_table_name text NOT NULL DEFAULT '', + interval text NOT NULL DEFAULT '1 month', + retention text NOT NULL DEFAULT '12 months', + premake int NOT NULL DEFAULT 2, + scope text NOT NULL DEFAULT 'app', + actor_fk_table_id uuid NULL, + entity_fk_table_id uuid NULL, + prefix text NOT NULL DEFAULT '', + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT compute_log_table_fkey + FOREIGN KEY(compute_log_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT usage_daily_table_fkey + FOREIGN KEY(usage_daily_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT compute_log_module_database_id_prefix_unique + UNIQUE NULLS NOT DISTINCT (database_id, prefix) +); + +CREATE INDEX compute_log_module_database_id_idx ON metaschema_modules_public.compute_log_module (database_id); + +CREATE TABLE metaschema_modules_public.transfer_log_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + transfer_log_table_id uuid NOT NULL DEFAULT uuid_nil(), + transfer_log_table_name text NOT NULL DEFAULT '', + usage_daily_table_id uuid NOT NULL DEFAULT uuid_nil(), + usage_daily_table_name text NOT NULL DEFAULT '', + interval text NOT NULL DEFAULT '1 month', + retention text NOT NULL DEFAULT '12 months', + premake int NOT NULL DEFAULT 2, + scope text NOT NULL DEFAULT 'app', + actor_fk_table_id uuid NULL, + entity_fk_table_id uuid NULL, + prefix text NOT NULL DEFAULT '', + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT transfer_log_table_fkey + FOREIGN KEY(transfer_log_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT usage_daily_table_fkey + FOREIGN KEY(usage_daily_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT transfer_log_module_database_id_prefix_unique + UNIQUE NULLS NOT DISTINCT (database_id, prefix) +); + +CREATE INDEX transfer_log_module_database_id_idx ON metaschema_modules_public.transfer_log_module (database_id); + +CREATE TABLE metaschema_modules_public.storage_log_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + storage_log_table_id uuid NOT NULL DEFAULT uuid_nil(), + storage_log_table_name text NOT NULL DEFAULT '', + usage_daily_table_id uuid NOT NULL DEFAULT uuid_nil(), + usage_daily_table_name text NOT NULL DEFAULT '', + interval text NOT NULL DEFAULT '1 month', + retention text NOT NULL DEFAULT '12 months', + premake int NOT NULL DEFAULT 2, + scope text NOT NULL DEFAULT 'app', + actor_fk_table_id uuid NULL, + entity_fk_table_id uuid NULL, + prefix text NOT NULL DEFAULT '', + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT storage_log_table_fkey + FOREIGN KEY(storage_log_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT usage_daily_table_fkey + FOREIGN KEY(usage_daily_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT storage_log_module_database_id_prefix_unique + UNIQUE NULLS NOT DISTINCT (database_id, prefix) +); + +CREATE INDEX storage_log_module_database_id_idx ON metaschema_modules_public.storage_log_module (database_id); + +CREATE TABLE metaschema_modules_public.db_usage_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_stats_log_table_id uuid NOT NULL DEFAULT uuid_nil(), + table_stats_log_table_name text NOT NULL DEFAULT '', + table_stats_daily_table_id uuid NOT NULL DEFAULT uuid_nil(), + table_stats_daily_table_name text NOT NULL DEFAULT '', + query_stats_log_table_id uuid NOT NULL DEFAULT uuid_nil(), + query_stats_log_table_name text NOT NULL DEFAULT '', + query_stats_daily_table_id uuid NOT NULL DEFAULT uuid_nil(), + query_stats_daily_table_name text NOT NULL DEFAULT '', + interval text NOT NULL DEFAULT '1 month', + retention text NOT NULL DEFAULT '12 months', + premake int NOT NULL DEFAULT 2, + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', + default_permissions text[] DEFAULT NULL, + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT table_stats_log_table_fkey + FOREIGN KEY(table_stats_log_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT table_stats_daily_table_fkey + FOREIGN KEY(table_stats_daily_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT query_stats_log_table_fkey + FOREIGN KEY(query_stats_log_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT query_stats_daily_table_fkey + FOREIGN KEY(query_stats_daily_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT db_usage_module_database_id_prefix_unique + UNIQUE NULLS NOT DISTINCT (database_id, prefix) +); + +CREATE INDEX db_usage_module_database_id_idx ON metaschema_modules_public.db_usage_module (database_id); + +CREATE TABLE metaschema_modules_public.agent_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + thread_table_id uuid NOT NULL DEFAULT uuid_nil(), + message_table_id uuid NOT NULL DEFAULT uuid_nil(), + task_table_id uuid NOT NULL DEFAULT uuid_nil(), + prompts_table_id uuid NOT NULL DEFAULT uuid_nil(), + plan_table_id uuid DEFAULT NULL, + agent_table_id uuid DEFAULT NULL, + persona_table_id uuid DEFAULT NULL, + resource_table_id uuid DEFAULT NULL, + thread_table_name text NOT NULL DEFAULT 'agent_thread', + message_table_name text NOT NULL DEFAULT 'agent_message', + task_table_name text NOT NULL DEFAULT 'agent_task', + prompts_table_name text NOT NULL DEFAULT 'agent_prompt', + plan_table_name text NOT NULL DEFAULT 'agent_plan', + agent_table_name text NOT NULL DEFAULT 'agent', + persona_table_name text NOT NULL DEFAULT 'agent_persona', + resource_table_name text NOT NULL DEFAULT 'agent_resource', + has_plans boolean NOT NULL DEFAULT false, + has_resources boolean NOT NULL DEFAULT false, + has_agents boolean NOT NULL DEFAULT false, + shared boolean NOT NULL DEFAULT false, + api_name text DEFAULT 'agent', + private_api_name text DEFAULT NULL, + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', + entity_table_id uuid NULL, + policies jsonb NULL, + resources jsonb NULL, + provisions jsonb NULL, + default_permissions text[] DEFAULT NULL, + CONSTRAINT agent_module_db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT agent_module_schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT agent_module_private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT agent_module_thread_table_fkey + FOREIGN KEY(thread_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT agent_module_message_table_fkey + FOREIGN KEY(message_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT agent_module_task_table_fkey + FOREIGN KEY(task_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT agent_module_prompts_table_fkey + FOREIGN KEY(prompts_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT agent_module_plan_table_fkey + FOREIGN KEY(plan_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT agent_module_agent_table_fkey + FOREIGN KEY(agent_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT agent_module_persona_table_fkey + FOREIGN KEY(persona_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT agent_module_resource_table_fkey + FOREIGN KEY(resource_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT agent_module_entity_table_fkey + FOREIGN KEY(entity_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX agent_module_database_id_idx ON metaschema_modules_public.agent_module (database_id); + +CREATE UNIQUE INDEX agent_module_unique_scope ON metaschema_modules_public.agent_module (database_id, scope, prefix); + +CREATE TABLE metaschema_modules_public.merkle_store_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + public_schema_name text, + private_schema_name text, + object_table_id uuid NOT NULL DEFAULT uuid_nil(), + store_table_id uuid NOT NULL DEFAULT uuid_nil(), + commit_table_id uuid NOT NULL DEFAULT uuid_nil(), + ref_table_id uuid NOT NULL DEFAULT uuid_nil(), + prefix text NOT NULL DEFAULT '', + api_name text, + private_api_name text, + scope text NOT NULL DEFAULT 'app', + created_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT object_table_fkey + FOREIGN KEY(object_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT store_table_fkey + FOREIGN KEY(store_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT commit_table_fkey + FOREIGN KEY(commit_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT ref_table_fkey + FOREIGN KEY(ref_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT merkle_store_module_database_prefix_unique + UNIQUE (database_id, prefix) +); + +CREATE INDEX merkle_store_module_database_id_idx ON metaschema_modules_public.merkle_store_module (database_id); + +CREATE INDEX merkle_store_module_private_schema_id_idx ON metaschema_modules_public.merkle_store_module (private_schema_id); + +CREATE TABLE metaschema_modules_public.graph_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + public_schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + public_schema_name text, + private_schema_name text, + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', + merkle_store_module_id uuid NOT NULL, + graphs_table_id uuid NOT NULL DEFAULT uuid_nil(), + executions_table_id uuid NOT NULL DEFAULT uuid_nil(), + outputs_table_id uuid NOT NULL DEFAULT uuid_nil(), + api_name text, + private_api_name text, + entity_table_id uuid NULL, + policies jsonb NULL, + provisions jsonb NULL, + default_permissions text[] DEFAULT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT public_schema_fkey + FOREIGN KEY(public_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT merkle_store_fkey + FOREIGN KEY(merkle_store_module_id) + REFERENCES metaschema_modules_public.merkle_store_module (id) + ON DELETE CASCADE, + CONSTRAINT graphs_table_fkey + FOREIGN KEY(graphs_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT executions_table_fkey + FOREIGN KEY(executions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT outputs_table_fkey + FOREIGN KEY(outputs_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT graph_module_entity_table_fkey + FOREIGN KEY(entity_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT graph_module_database_merkle_unique + UNIQUE (database_id, merkle_store_module_id) +); + +CREATE INDEX graph_module_database_id_idx ON metaschema_modules_public.graph_module (database_id); + +CREATE TABLE metaschema_modules_public.namespace_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + public_schema_name text, + private_schema_name text, + namespaces_table_id uuid NOT NULL DEFAULT uuid_nil(), + namespace_events_table_id uuid NOT NULL DEFAULT uuid_nil(), + namespaces_table_name text NOT NULL DEFAULT 'namespaces', + namespace_events_table_name text NOT NULL DEFAULT 'namespace_events', + api_name text, + private_api_name text, + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', + entity_table_id uuid NULL, + policies jsonb NULL, + provisions jsonb NULL, + default_permissions text[] DEFAULT NULL, + CONSTRAINT namespace_module_db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT namespace_module_schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT namespace_module_private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT namespace_module_namespaces_table_fkey + FOREIGN KEY(namespaces_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT namespace_module_events_table_fkey + FOREIGN KEY(namespace_events_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT namespace_module_entity_table_fkey + FOREIGN KEY(entity_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX namespace_module_database_id_idx ON metaschema_modules_public.namespace_module (database_id); + +CREATE UNIQUE INDEX namespace_module_unique_scope ON metaschema_modules_public.namespace_module (database_id, scope, prefix); + +CREATE TABLE metaschema_modules_public.function_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + public_schema_name text, + private_schema_name text, + definitions_table_id uuid NOT NULL DEFAULT uuid_nil(), + invocations_table_id uuid NOT NULL DEFAULT uuid_nil(), + execution_logs_table_id uuid NOT NULL DEFAULT uuid_nil(), + secret_definitions_table_id uuid NOT NULL DEFAULT uuid_nil(), + definitions_table_name text NOT NULL DEFAULT 'function_definitions', + invocations_table_name text NOT NULL DEFAULT 'function_invocations', + execution_logs_table_name text NOT NULL DEFAULT 'function_execution_logs', + secret_definitions_table_name text NOT NULL DEFAULT 'secret_definitions', + api_name text, + private_api_name text, + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', + entity_table_id uuid NULL, + policies jsonb NULL, + provisions jsonb NULL, + default_permissions text[] DEFAULT NULL, + CONSTRAINT function_module_db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT function_module_schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT function_module_private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT function_module_definitions_table_fkey + FOREIGN KEY(definitions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT function_module_invocations_table_fkey + FOREIGN KEY(invocations_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT function_module_execution_logs_table_fkey + FOREIGN KEY(execution_logs_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT function_module_secret_defs_table_fkey + FOREIGN KEY(secret_definitions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT function_module_entity_table_fkey + FOREIGN KEY(entity_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX function_module_database_id_idx ON metaschema_modules_public.function_module (database_id); + +CREATE UNIQUE INDEX function_module_unique_scope ON metaschema_modules_public.function_module (database_id, scope, prefix); + +CREATE TABLE metaschema_modules_public.config_secrets_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + config_definitions_table_id uuid NULL DEFAULT NULL, + table_name text NOT NULL DEFAULT 'secrets', + api_name text DEFAULT 'config', + private_api_name text DEFAULT NULL, + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', + entity_table_id uuid NULL, + policies jsonb NULL, + provisions jsonb NULL, + has_config boolean NOT NULL DEFAULT false, + CONSTRAINT config_secrets_module_db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT config_secrets_module_schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT config_secrets_module_private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT config_secrets_module_table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT config_secrets_module_config_defs_table_fkey + FOREIGN KEY(config_definitions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT config_secrets_module_entity_table_fkey + FOREIGN KEY(entity_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX config_secrets_module_database_id_idx ON metaschema_modules_public.config_secrets_module (database_id); + +CREATE INDEX config_secrets_module_schema_id_idx ON metaschema_modules_public.config_secrets_module (schema_id); + +CREATE INDEX config_secrets_module_table_id_idx ON metaschema_modules_public.config_secrets_module (table_id); + +CREATE UNIQUE INDEX config_secrets_module_unique_scope ON metaschema_modules_public.config_secrets_module (database_id, scope, prefix); + +COMMENT ON TABLE metaschema_modules_public.config_secrets_module IS 'Entity-aware PGP-encrypted key-value config/secrets module. Supports app-level (admin-only) + and org-scoped (per-org secrets with manage_secrets permission) via the scope column. + User-scoped bcrypt credentials are handled by user_credentials_module.'; + +CREATE TABLE metaschema_modules_public.user_credentials_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT 'user_secrets', + api_name text DEFAULT 'config', + private_api_name text DEFAULT NULL, + CONSTRAINT user_credentials_module_db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT user_credentials_module_schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT user_credentials_module_table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX user_credentials_module_database_id_idx ON metaschema_modules_public.user_credentials_module (database_id); + +CREATE INDEX user_credentials_module_schema_id_idx ON metaschema_modules_public.user_credentials_module (schema_id); + +CREATE INDEX user_credentials_module_table_id_idx ON metaschema_modules_public.user_credentials_module (table_id); + +CREATE UNIQUE INDEX user_credentials_module_unique ON metaschema_modules_public.user_credentials_module (database_id); + +COMMENT ON TABLE metaschema_modules_public.user_credentials_module IS 'Per-user bcrypt credential store (password hashes, API key hashes). + Always user-scoped with AuthzDirectOwner RLS. Consumed by user_auth_module, + identity_providers_module, and bootstrap procedures.'; + +CREATE TABLE metaschema_modules_public.user_settings_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + owner_table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT 'user_settings', + api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT owner_table_fkey + FOREIGN KEY(owner_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX user_settings_module_database_id_idx ON metaschema_modules_public.user_settings_module (database_id); + +CREATE UNIQUE INDEX user_settings_module_unique_per_db ON metaschema_modules_public.user_settings_module (database_id); + +CREATE TABLE metaschema_modules_public.i18n_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + settings_table_id uuid NOT NULL DEFAULT uuid_nil(), + api_name text DEFAULT NULL, + private_api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT settings_table_fkey + FOREIGN KEY(settings_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX i18n_module_database_id_idx ON metaschema_modules_public.i18n_module (database_id); + +CREATE UNIQUE INDEX i18n_module_unique_per_db ON metaschema_modules_public.i18n_module (database_id); \ No newline at end of file diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/schema.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/schema.sql new file mode 100644 index 00000000..4e919dcb --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/schema.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/schema on pg + +BEGIN; + +SELECT pg_catalog.has_schema_privilege('metaschema_modules_public', 'usage'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/agent_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/agent_module/table.sql new file mode 100644 index 00000000..aa3519ff --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/agent_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/agent_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.agent_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/billing_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/billing_module/table.sql new file mode 100644 index 00000000..aa1c1c91 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/billing_module/table.sql @@ -0,0 +1,25 @@ +-- Verify schemas/metaschema_modules_public/tables/billing_module/table on pg + +BEGIN; + +SELECT + id, + database_id, + schema_id, + private_schema_id, + meters_table_id, + meters_table_name, + plan_subscriptions_table_id, + plan_subscriptions_table_name, + ledger_table_id, + ledger_table_name, + balances_table_id, + balances_table_name, + meter_sources_table_id, + meter_sources_table_name, + record_usage_function, + prefix +FROM metaschema_modules_public.billing_module +WHERE FALSE; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/billing_provider_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/billing_provider_module/table.sql new file mode 100644 index 00000000..1e87f0bb --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/billing_provider_module/table.sql @@ -0,0 +1,29 @@ +-- Verify schemas/metaschema_modules_public/tables/billing_provider_module/table on pg + +BEGIN; + +SELECT + id, + database_id, + schema_id, + private_schema_id, + provider, + products_table_id, + prices_table_id, + subscriptions_table_id, + billing_customers_table_id, + billing_customers_table_name, + billing_products_table_id, + billing_products_table_name, + billing_prices_table_id, + billing_prices_table_name, + billing_subscriptions_table_id, + billing_subscriptions_table_name, + billing_webhook_events_table_id, + billing_webhook_events_table_name, + process_billing_event_function, + prefix +FROM metaschema_modules_public.billing_provider_module +WHERE FALSE; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/blueprint/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/blueprint/table.sql new file mode 100644 index 00000000..d5683b39 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/blueprint/table.sql @@ -0,0 +1,21 @@ +-- Verify schemas/metaschema_modules_public/tables/blueprint/table on pg + +BEGIN; + +SELECT + id, + owner_id, + database_id, + name, + display_name, + description, + definition, + template_id, + definition_hash, + table_hashes, + created_at, + updated_at +FROM metaschema_modules_public.blueprint +WHERE FALSE; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/blueprint_construction/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/blueprint_construction/table.sql new file mode 100644 index 00000000..aceac402 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/blueprint_construction/table.sql @@ -0,0 +1,10 @@ +-- Verify schemas/metaschema_modules_public/tables/blueprint_construction/table + +BEGIN; + +SELECT id, blueprint_id, database_id, schema_id, status, error_details, + table_map, constructed_definition, constructed_at, created_at, updated_at +FROM metaschema_modules_public.blueprint_construction +WHERE FALSE; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/blueprint_template/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/blueprint_template/table.sql new file mode 100644 index 00000000..e26f09cb --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/blueprint_template/table.sql @@ -0,0 +1,29 @@ +-- Verify schemas/metaschema_modules_public/tables/blueprint_template/table on pg + +BEGIN; + +SELECT + id, + name, + version, + display_name, + description, + owner_id, + visibility, + categories, + tags, + definition, + definition_schema_version, + source, + complexity, + copy_count, + fork_count, + forked_from_id, + definition_hash, + table_hashes, + created_at, + updated_at +FROM metaschema_modules_public.blueprint_template +WHERE FALSE; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/compute_log_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/compute_log_module/table.sql new file mode 100644 index 00000000..de44722a --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/compute_log_module/table.sql @@ -0,0 +1,9 @@ +-- Verify schemas/metaschema_modules_public/tables/compute_log_module/table on pg + +SELECT id, database_id, schema_id, private_schema_id, + compute_log_table_id, compute_log_table_name, + usage_daily_table_id, usage_daily_table_name, + retention, scope, actor_fk_table_id, entity_fk_table_id, + prefix +FROM metaschema_modules_public.compute_log_module +WHERE FALSE; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_module/table.sql new file mode 100644 index 00000000..8ecf487d --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/config_secrets_module/table on pg + +BEGIN; + +SELECT verify_table('metaschema_modules_public.config_secrets_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql new file mode 100644 index 00000000..b8551286 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/config_secrets_org_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.config_secrets_org_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql new file mode 100644 index 00000000..6ca9dcfd --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/config_secrets_user_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.config_secrets_user_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/connected_accounts_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/connected_accounts_module/table.sql new file mode 100644 index 00000000..c986b454 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/connected_accounts_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/connected_accounts_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.connected_accounts_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/crypto_addresses_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/crypto_addresses_module/table.sql new file mode 100644 index 00000000..7d0462db --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/crypto_addresses_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/crypto_addresses_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.crypto_addresses_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/crypto_auth_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/crypto_auth_module/table.sql new file mode 100644 index 00000000..46e584b7 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/crypto_auth_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/crypto_auth_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.crypto_auth_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/db_usage_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/db_usage_module/table.sql new file mode 100644 index 00000000..57f884fe --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/db_usage_module/table.sql @@ -0,0 +1,10 @@ +-- Verify schemas/metaschema_modules_public/tables/db_usage_module/table on pg + +SELECT id, database_id, schema_id, private_schema_id, + table_stats_log_table_id, table_stats_log_table_name, + table_stats_daily_table_id, table_stats_daily_table_name, + query_stats_log_table_id, query_stats_log_table_name, + query_stats_daily_table_id, query_stats_daily_table_name, + retention, scope, prefix +FROM metaschema_modules_public.db_usage_module +WHERE FALSE; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/default_ids_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/default_ids_module/table.sql new file mode 100644 index 00000000..1333b7d3 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/default_ids_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/default_ids_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.default_ids_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/denormalized_table_field/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/denormalized_table_field/table.sql new file mode 100644 index 00000000..2868c4e1 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/denormalized_table_field/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/denormalized_table_field/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.denormalized_table_field'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/devices_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/devices_module/table.sql new file mode 100644 index 00000000..c0c198e9 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/devices_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/devices_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.devices_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/emails_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/emails_module/table.sql new file mode 100644 index 00000000..5ea33e94 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/emails_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/emails_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.emails_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/entity_type_provision/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/entity_type_provision/table.sql new file mode 100644 index 00000000..f5d77396 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/entity_type_provision/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/entity_type_provision/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.entity_type_provision'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/events_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/events_module/table.sql new file mode 100644 index 00000000..fcc1bd22 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/events_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/events_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.events_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/function_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/function_module/table.sql new file mode 100644 index 00000000..4af5faec --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/function_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/function_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.function_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/graph_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/graph_module/table.sql new file mode 100644 index 00000000..0c4e161a --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/graph_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/graph_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.graph_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/hierarchy_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/hierarchy_module/table.sql new file mode 100644 index 00000000..15df17f8 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/hierarchy_module/table.sql @@ -0,0 +1,29 @@ +-- Verify schemas/metaschema_modules_public/tables/hierarchy_module/table on pg + +BEGIN; + +SELECT + id, + database_id, + schema_id, + private_schema_id, + chart_edges_table_id, + chart_edges_table_name, + hierarchy_sprt_table_id, + hierarchy_sprt_table_name, + chart_edge_grants_table_id, + chart_edge_grants_table_name, + entity_table_id, + users_table_id, + prefix, + private_schema_name, + sprt_table_name, + rebuild_hierarchy_function, + get_subordinates_function, + get_managers_function, + is_manager_of_function, + created_at +FROM metaschema_modules_public.hierarchy_module +WHERE FALSE; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/i18n_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/i18n_module/table.sql new file mode 100644 index 00000000..2f17fba3 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/i18n_module/table.sql @@ -0,0 +1,9 @@ +-- Verify schemas/metaschema_modules_public/tables/i18n_module/table on pg + +BEGIN; + +SELECT id, database_id, schema_id, private_schema_id, settings_table_id, api_name, private_api_name +FROM metaschema_modules_public.i18n_module +WHERE FALSE; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/identity_providers_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/identity_providers_module/table.sql new file mode 100644 index 00000000..35ba6326 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/identity_providers_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/identity_providers_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.identity_providers_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/inference_log_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/inference_log_module/table.sql new file mode 100644 index 00000000..eaf4c39c --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/inference_log_module/table.sql @@ -0,0 +1,22 @@ +-- Verify schemas/metaschema_modules_public/tables/inference_log_module/table on pg + +BEGIN; + +SELECT + id, + database_id, + schema_id, + private_schema_id, + inference_log_table_id, + inference_log_table_name, + usage_daily_table_id, + usage_daily_table_name, + retention, + scope, + actor_fk_table_id, + entity_fk_table_id, + prefix +FROM metaschema_modules_public.inference_log_module +WHERE FALSE; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/invites_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/invites_module/table.sql new file mode 100644 index 00000000..6012c443 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/invites_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/invites_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.invites_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/limits_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/limits_module/table.sql new file mode 100644 index 00000000..fd92486f --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/limits_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/limits_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.limits_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/membership_types_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/membership_types_module/table.sql new file mode 100644 index 00000000..168025dd --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/membership_types_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/membership_types_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.membership_types_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/memberships_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/memberships_module/table.sql new file mode 100644 index 00000000..5518679b --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/memberships_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/memberships_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.memberships_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/merkle_store_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/merkle_store_module/table.sql new file mode 100644 index 00000000..8b8a33f7 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/merkle_store_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/merkle_store_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.merkle_store_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/namespace_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/namespace_module/table.sql new file mode 100644 index 00000000..8cfe5404 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/namespace_module/table.sql @@ -0,0 +1,14 @@ +-- Verify schemas/metaschema_modules_public/tables/namespace_module/table on pg + +BEGIN; + +SELECT id, database_id, schema_id, private_schema_id, + public_schema_name, private_schema_name, + namespaces_table_id, namespace_events_table_id, + namespaces_table_name, namespace_events_table_name, + api_name, private_api_name, scope, prefix, + entity_table_id, policies, provisions, default_permissions +FROM metaschema_modules_public.namespace_module +WHERE false; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/notifications_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/notifications_module/table.sql new file mode 100644 index 00000000..fe683e55 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/notifications_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/notifications_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.notifications_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/permissions_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/permissions_module/table.sql new file mode 100644 index 00000000..45e2fa6d --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/permissions_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/permissions_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.permissions_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/phone_numbers_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/phone_numbers_module/table.sql new file mode 100644 index 00000000..f1e40951 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/phone_numbers_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/phone_numbers_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.phone_numbers_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/plans_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/plans_module/table.sql new file mode 100644 index 00000000..bf90a186 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/plans_module/table.sql @@ -0,0 +1,13 @@ +-- Verify schemas/metaschema_modules_public/tables/plans_module/table on pg + +BEGIN; + +SELECT id, database_id, schema_id, private_schema_id, + plans_table_id, plans_table_name, + plan_limits_table_id, plan_limits_table_name, + apply_plan_function, apply_plan_aggregate_function, + prefix +FROM metaschema_modules_public.plans_module +WHERE FALSE; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/profiles_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/profiles_module/table.sql new file mode 100644 index 00000000..9c52869f --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/profiles_module/table.sql @@ -0,0 +1,15 @@ +-- Verify schemas/metaschema_modules_public/tables/profiles_module/table on pg + +BEGIN; + +SELECT id, database_id, schema_id, private_schema_id, table_id, table_name, + profile_permissions_table_id, profile_permissions_table_name, + profile_grants_table_id, profile_grants_table_name, + profile_definition_grants_table_id, profile_definition_grants_table_name, + profile_templates_table_id, profile_templates_table_name, + entity_table_id, actor_table_id, + permissions_table_id, memberships_table_id, prefix +FROM metaschema_modules_public.profiles_module +WHERE FALSE; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql new file mode 100644 index 00000000..1053f4f6 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/rate_limit_meters_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.rate_limit_meters_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rate_limits_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rate_limits_module/table.sql new file mode 100644 index 00000000..86e7fe89 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rate_limits_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/rate_limits_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.rate_limits_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/realtime_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/realtime_module/table.sql new file mode 100644 index 00000000..aca571c9 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/realtime_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/realtime_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.realtime_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/relation_provision/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/relation_provision/table.sql new file mode 100644 index 00000000..0caae856 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/relation_provision/table.sql @@ -0,0 +1,33 @@ +-- Verify schemas/metaschema_modules_public/tables/relation_provision/table on pg + +BEGIN; + +SELECT + id, + database_id, + relation_type, + source_table_id, + target_table_id, + field_name, + delete_action, + is_required, + api_required, + junction_table_id, + junction_table_name, + junction_schema_id, + source_field_name, + target_field_name, + use_composite_key, + create_index, + expose_in_api, + nodes, + grants, + policies, + out_field_id, + out_junction_table_id, + out_source_field_id, + out_target_field_id +FROM metaschema_modules_public.relation_provision +WHERE FALSE; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rls_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rls_module/table.sql new file mode 100644 index 00000000..12226184 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rls_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/rls_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.rls_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/secure_table_provision/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/secure_table_provision/table.sql new file mode 100644 index 00000000..f953e507 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/secure_table_provision/table.sql @@ -0,0 +1,20 @@ +-- Verify schemas/metaschema_modules_public/tables/secure_table_provision/table on pg + +BEGIN; + +SELECT + id, + database_id, + schema_id, + table_id, + table_name, + nodes, + use_rls, + fields, + grants, + policies, + out_fields +FROM metaschema_modules_public.secure_table_provision +WHERE FALSE; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/session_secrets_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/session_secrets_module/table.sql new file mode 100644 index 00000000..5cf6fc68 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/session_secrets_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/session_secrets_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.session_secrets_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/sessions_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/sessions_module/table.sql new file mode 100644 index 00000000..199463ca --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/sessions_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/sessions_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.sessions_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/storage_log_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/storage_log_module/table.sql new file mode 100644 index 00000000..adbf9ac7 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/storage_log_module/table.sql @@ -0,0 +1,10 @@ +-- Verify schemas/metaschema_modules_public/tables/storage_log_module/table on pg + +SELECT id, database_id, schema_id, private_schema_id, + storage_log_table_id, storage_log_table_name, + usage_daily_table_id, usage_daily_table_name, + retention, scope, + actor_fk_table_id, entity_fk_table_id, + prefix +FROM metaschema_modules_public.storage_log_module +WHERE FALSE; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/storage_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/storage_module/table.sql new file mode 100644 index 00000000..47099c40 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/storage_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/storage_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.storage_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/transfer_log_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/transfer_log_module/table.sql new file mode 100644 index 00000000..6a17ba16 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/transfer_log_module/table.sql @@ -0,0 +1,10 @@ +-- Verify schemas/metaschema_modules_public/tables/transfer_log_module/table on pg + +SELECT id, database_id, schema_id, private_schema_id, + transfer_log_table_id, transfer_log_table_name, + usage_daily_table_id, usage_daily_table_name, + retention, scope, + actor_fk_table_id, entity_fk_table_id, + prefix +FROM metaschema_modules_public.transfer_log_module +WHERE FALSE; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_auth_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_auth_module/table.sql new file mode 100644 index 00000000..7ac2d45b --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_auth_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/user_auth_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.user_auth_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql new file mode 100644 index 00000000..940681c3 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/user_credentials_module/table on pg + +BEGIN; + +SELECT verify_table('metaschema_modules_public.user_credentials_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_settings_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_settings_module/table.sql new file mode 100644 index 00000000..7a56d8ee --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_settings_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/user_settings_module/table on pg + +BEGIN; + +SELECT verify_table('metaschema_modules_public.user_settings_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_state_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_state_module/table.sql new file mode 100644 index 00000000..36655782 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_state_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/user_state_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.user_state_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/users_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/users_module/table.sql new file mode 100644 index 00000000..2b0557cc --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/users_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/users_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.users_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/webauthn_auth_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/webauthn_auth_module/table.sql new file mode 100644 index 00000000..8fc84c14 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/webauthn_auth_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/webauthn_auth_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.webauthn_auth_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/webauthn_credentials_module/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/webauthn_credentials_module/table.sql new file mode 100644 index 00000000..a8859bf5 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/metaschema_modules_public/tables/webauthn_credentials_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/webauthn_credentials_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.webauthn_credentials_module'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/services_private/schema.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/services_private/schema.sql new file mode 100644 index 00000000..e59c3d07 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/services_private/schema.sql @@ -0,0 +1 @@ +-- Verify schemas/services_private/schema on pg diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/services_public/schema.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/services_public/schema.sql new file mode 100644 index 00000000..d51679b1 --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/services_public/schema.sql @@ -0,0 +1 @@ +-- Verify schemas/services_public/schema on pg diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/services_public/tables/apis/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/services_public/tables/apis/table.sql new file mode 100644 index 00000000..c0bee4ce --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/services_public/tables/apis/table.sql @@ -0,0 +1 @@ +-- Verify schemas/services_public/tables/apis/table on pg diff --git a/extensions/@pgpm/metaschema-modules/verify/schemas/services_public/tables/sites/table.sql b/extensions/@pgpm/metaschema-modules/verify/schemas/services_public/tables/sites/table.sql new file mode 100644 index 00000000..e334fb0f --- /dev/null +++ b/extensions/@pgpm/metaschema-modules/verify/schemas/services_public/tables/sites/table.sql @@ -0,0 +1 @@ +-- Verify schemas/services_public/tables/sites/table on pg diff --git a/extensions/@pgpm/metaschema-schema/LICENSE b/extensions/@pgpm/metaschema-schema/LICENSE new file mode 100644 index 00000000..7b18c918 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2025 Dan Lynch +Copyright (c) 2025 Constructive + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/@pgpm/metaschema-schema/Makefile b/extensions/@pgpm/metaschema-schema/Makefile new file mode 100644 index 00000000..9c6e3a8a --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/Makefile @@ -0,0 +1,6 @@ +EXTENSION = metaschema-schema +DATA = sql/metaschema-schema--0.26.3.sql + +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) diff --git a/extensions/@pgpm/metaschema-schema/README.md b/extensions/@pgpm/metaschema-schema/README.md new file mode 100644 index 00000000..7053a219 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/README.md @@ -0,0 +1,323 @@ +# @pgpm/db-meta-schema + +

+ +

+ +

+ + + + + +

+ +Database metadata utilities and introspection functions. + +## Overview + +`@pgpm/db-meta-schema` provides a comprehensive metadata management system for PostgreSQL databases. This package creates tables and schemas for storing and querying database structure information including databases, schemas, tables, fields, constraints, indexes, and more. It enables runtime schema introspection, metadata-driven code generation, and database structure management. + +## Features + +- **Database Metadata Storage**: Store information about databases, schemas, tables, and fields +- **Constraint Tracking**: Track primary keys, foreign keys, unique constraints, and check constraints +- **Index Management**: Store and query index definitions +- **Trigger and Procedure Metadata**: Track database functions and triggers +- **RLS and Policy Information**: Store row-level security policies +- **Extension Tracking**: Manage database extensions and their relationships +- **API and Site Metadata**: Store API configurations and site information +- **GraphQL Integration**: Smart tags and annotations for GraphQL schema generation + +## Installation + +If you have `pgpm` installed: + +```bash +pgpm install @pgpm/db-meta-schema +pgpm deploy +``` + +This is a quick way to get started. The sections below provide more detailed installation options. + +### Prerequisites + +```bash +# Install pgpm CLI +npm install -g pgpm + +# Start local Postgres (via Docker) and export env vars +pgpm docker start +eval "$(pgpm env)" +``` + +> **Tip:** Already running Postgres? Skip the Docker step and just export your `PG*` environment variables. + +### **Add to an Existing Package** + +```bash +# 1. Install the package +pgpm install @pgpm/db-meta-schema + +# 2. Deploy locally +pgpm deploy +``` + +### **Add to a New Project** + +```bash +# 1. Create a workspace +pgpm init workspace + +# 2. Create your first module +cd my-workspace +pgpm init + +# 3. Install a package +cd packages/my-module +pgpm install @pgpm/db-meta-schema + +# 4. Deploy everything +pgpm deploy --createdb --database mydb1 +``` + +## Core Schemas + +### metaschema_public Schema + +Stores database structure metadata: + +- **database**: Database definitions with schema names and hashes +- **schema**: Schema definitions within databases +- **table**: Table definitions with RLS, timestamps, and naming conventions +- **field**: Column definitions with types, constraints, and validation rules +- **primary_key_constraint**: Primary key definitions +- **foreign_key_constraint**: Foreign key relationships +- **unique_constraint**: Unique constraints +- **check_constraint**: Check constraint definitions +- **index**: Index definitions +- **trigger**: Trigger definitions +- **procedure**: Stored procedure definitions +- **policy**: Row-level security policies +- **extension**: PostgreSQL extensions + +### metaschema_private Schema + +Private schema for internal metadata operations. + +### services_public Schema + +Application-level metadata: + +- **apis**: API configurations +- **api_extensions**: API extension relationships +- **api_modules**: API module definitions +- **api_schemas**: API schema configurations +- **sites**: Site definitions +- **apps**: Application definitions +- **domains**: Domain configurations +- **site_metadata**: Site metadata +- **site_modules**: Site module configurations +- **site_themes**: Site theme definitions + +## Usage + +### Storing Database Metadata + +```sql +-- Create a database entry +INSERT INTO metaschema_public.database (name, label, schema_name, private_schema_name) +VALUES ('my_app', 'My Application', 'my_app_public', 'my_app_private') +RETURNING id; + +-- Create a schema entry +INSERT INTO metaschema_public.schema (database_id, name) +VALUES ('database-uuid', 'public') +RETURNING id; + +-- Create a table entry +INSERT INTO metaschema_public.table ( + database_id, + schema_id, + name, + label, + use_rls, + timestamps, + peoplestamps +) VALUES ( + 'database-uuid', + 'schema-uuid', + 'users', + 'Users', + true, + true, + true +); + +-- Create field entries +INSERT INTO metaschema_public.field ( + database_id, + table_id, + name, + label, + type, + is_required, + field_order +) VALUES + ('database-uuid', 'table-uuid', 'id', 'ID', 'uuid', true, 1), + ('database-uuid', 'table-uuid', 'email', 'Email', 'email', true, 2), + ('database-uuid', 'table-uuid', 'name', 'Name', 'text', false, 3); +``` + +### Querying Metadata + +```sql +-- Get all tables in a database +SELECT t.name, t.label, s.name as schema_name +FROM metaschema_public.table t +JOIN metaschema_public.schema s ON t.schema_id = s.id +WHERE t.database_id = 'database-uuid'; + +-- Get all fields for a table +SELECT f.name, f.label, f.type, f.is_required, f.default_value +FROM metaschema_public.field f +WHERE f.table_id = 'table-uuid' +ORDER BY f.field_order; + +-- Get foreign key relationships +SELECT + fk.name as constraint_name, + t1.name as from_table, + t2.name as to_table +FROM metaschema_public.foreign_key_constraint fk +JOIN metaschema_public.table t1 ON fk.table_id = t1.id +JOIN metaschema_public.table t2 ON fk.foreign_table_id = t2.id +WHERE fk.database_id = 'database-uuid'; +``` + +### Smart Tags for GraphQL + +The package supports smart tags for GraphQL schema generation: + +```sql +-- Add smart tags to a table +UPDATE metaschema_public.table +SET smart_tags = '{ + "@omit": "create,update,delete", + "@name": "CustomTableName" +}'::jsonb +WHERE id = 'table-uuid'; + +-- Add smart tags to a field +UPDATE metaschema_public.field +SET smart_tags = '{ + "@omit": true, + "@deprecated": "Use new_field instead" +}'::jsonb +WHERE id = 'field-uuid'; +``` + +## Table Structures + +### database Table + +Stores database definitions: +- `id`: UUID primary key +- `owner_id`: Owner UUID +- `schema_hash`: Unique schema hash +- `schema_name`: Public schema name +- `private_schema_name`: Private schema name +- `name`: Database name +- `label`: Display label +- `hash`: Database hash + +### table Table + +Stores table definitions: +- `id`: UUID primary key +- `database_id`: Foreign key to database +- `schema_id`: Foreign key to schema +- `name`: Table name +- `label`: Display label +- `description`: Table description +- `smart_tags`: JSONB smart tags for GraphQL +- `use_rls`: Enable row-level security +- `timestamps`: Enable created_at/updated_at +- `peoplestamps`: Enable created_by/updated_by +- `plural_name`: Plural form for API +- `singular_name`: Singular form for API +- `inherits_id`: Table inheritance + +### field Table + +Stores column definitions: +- `id`: UUID primary key +- `database_id`: Foreign key to database +- `table_id`: Foreign key to table +- `name`: Column name +- `label`: Display label +- `description`: Column description +- `smart_tags`: JSONB smart tags +- `is_required`: NOT NULL constraint +- `default_value`: Default value +- `smart_tags`: Use smart tags with `@omit` to hide from API +- `type`: PostgreSQL type +- `field_order`: Display order +- `regexp`: Validation regex +- `chk`: Check constraint JSON +- `min`/`max`: Numeric constraints + +## Use Cases + +### Schema-Driven Code Generation + +Use metadata to generate: +- GraphQL schemas +- TypeScript types +- API documentation +- Database migration scripts +- Admin interfaces + +### Runtime Schema Introspection + +Query metadata at runtime to: +- Build dynamic forms +- Generate validation rules +- Create custom queries +- Implement multi-tenancy + +### Database Documentation + +Generate documentation from metadata: +- Entity-relationship diagrams +- Data dictionaries +- API specifications + +## Dependencies + +- `@pgpm/database-jobs`: Background job processing +- `@pgpm/inflection`: String inflection utilities +- `@pgpm/types`: Core PostgreSQL types +- `@pgpm/verify`: Verification utilities + +## Testing + +```bash +pnpm test +``` + +## Related Tooling + +* [pgpm](https://github.com/constructive-io/constructive/tree/main/packages/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages. +* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/packages/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation. +* [supabase-test](https://github.com/constructive-io/constructive/tree/main/packages/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready. +* [graphile-test](https://github.com/constructive-io/constructive/tree/main/packages/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts. +* [pgsql-parser](https://github.com/constructive-io/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax. +* [libpg-query-node](https://github.com/constructive-io/libpg-query-node): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees. +* [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. + +## Disclaimer + +AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND. + +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. diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_private/schema.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_private/schema.sql new file mode 100644 index 00000000..73dbc806 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_private/schema.sql @@ -0,0 +1,12 @@ +-- Deploy schemas/metaschema_private/schema to pg + +BEGIN; + +CREATE SCHEMA metaschema_private; + +GRANT USAGE ON SCHEMA metaschema_private TO authenticated; +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_private GRANT ALL ON TABLES TO authenticated; +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_private GRANT ALL ON SEQUENCES TO authenticated; +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_private GRANT ALL ON FUNCTIONS TO authenticated; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/schema.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/schema.sql new file mode 100644 index 00000000..46b7b864 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/schema.sql @@ -0,0 +1,12 @@ +-- Deploy schemas/metaschema_public/schema to pg + +BEGIN; + +CREATE SCHEMA metaschema_public; + +GRANT USAGE ON SCHEMA metaschema_public TO authenticated; +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_public GRANT ALL ON TABLES TO authenticated; +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_public GRANT ALL ON SEQUENCES TO authenticated; +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_public GRANT ALL ON FUNCTIONS TO authenticated; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/check_constraint/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/check_constraint/table.sql new file mode 100644 index 00000000..071ec662 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/check_constraint/table.sql @@ -0,0 +1,42 @@ +-- Deploy schemas/metaschema_public/tables/check_constraint/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/database/table +-- requires: schemas/metaschema_public/tables/table/table +-- requires: schemas/metaschema_public/types/object_category + +BEGIN; + +CREATE TABLE metaschema_public.check_constraint ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + + table_id uuid NOT NULL, + name text, + type text, + field_ids uuid[] NOT NULL, + expr jsonb, + + smart_tags jsonb, + + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + + tags citext[] NOT NULL DEFAULT '{}', + + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + UNIQUE (table_id, name), + CHECK (field_ids <> '{}') +); + + +CREATE INDEX check_constraint_table_id_idx ON metaschema_public.check_constraint ( table_id ); +CREATE INDEX check_constraint_database_id_idx ON metaschema_public.check_constraint ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/composite_type/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/composite_type/table.sql new file mode 100644 index 00000000..806328fc --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/composite_type/table.sql @@ -0,0 +1,38 @@ +-- Deploy schemas/metaschema_public/tables/composite_type/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/database/table +-- requires: schemas/metaschema_public/tables/schema/table +-- requires: schemas/metaschema_public/types/object_category + +BEGIN; + +CREATE TABLE metaschema_public.composite_type ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL, + name text NOT NULL, + + label text, + description text, + + attributes jsonb NOT NULL DEFAULT '[]', + + smart_tags jsonb, + + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + + tags citext[] NOT NULL DEFAULT '{}', + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + + UNIQUE (schema_id, name) +); + +CREATE INDEX composite_type_schema_id_idx ON metaschema_public.composite_type ( schema_id ); +CREATE INDEX composite_type_database_id_idx ON metaschema_public.composite_type ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx.sql new file mode 100644 index 00000000..e0a83f4a --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx.sql @@ -0,0 +1,20 @@ +-- Deploy schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx to pg +-- requires: schemas/metaschema_private/schema +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/database/table + +BEGIN; + +CREATE FUNCTION metaschema_private.database_name_hash (name text) + RETURNS bytea + AS $BODY$ + SELECT + DECODE(MD5(LOWER(inflection.plural (name))), 'hex'); +$BODY$ +LANGUAGE sql +IMMUTABLE; + +CREATE UNIQUE INDEX databases_database_unique_name_idx ON metaschema_public.database (owner_id, metaschema_private.database_name_hash (name)); + +COMMIT; + diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/database/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/database/table.sql new file mode 100644 index 00000000..fbfd17e7 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/database/table.sql @@ -0,0 +1,28 @@ +-- Deploy schemas/metaschema_public/tables/database/table to pg + +-- requires: schemas/metaschema_public/schema + +BEGIN; + +CREATE TABLE metaschema_public.database ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + owner_id uuid, + schema_hash text, + + name text, + label text, + + hash uuid, + + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + + unique(schema_hash) +); + +ALTER TABLE metaschema_public.database + ADD CONSTRAINT db_namechk CHECK (char_length(name) > 2); + +COMMENT ON COLUMN metaschema_public.database.schema_hash IS '@omit'; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/default_privilege/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/default_privilege/table.sql new file mode 100644 index 00000000..c6f94f1b --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/default_privilege/table.sql @@ -0,0 +1,37 @@ +-- Deploy schemas/metaschema_public/tables/default_privilege/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/schema/table +-- requires: schemas/metaschema_public/tables/database/table + +BEGIN; + +CREATE TABLE metaschema_public.default_privilege ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + + schema_id uuid NOT NULL, + + -- 'tables', 'functions', 'sequences' + object_type text NOT NULL, + + -- 'ALL', 'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'USAGE', 'EXECUTE', etc. + privilege text NOT NULL, + + -- role receiving the privilege (e.g. 'authenticated', 'administrator', 'anonymous') + grantee_name text NOT NULL, + + -- true = GRANT, false = REVOKE + is_grant boolean NOT NULL DEFAULT true, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + + UNIQUE (schema_id, object_type, privilege, grantee_name, is_grant) +); + + +CREATE INDEX default_privilege_schema_id_idx ON metaschema_public.default_privilege ( schema_id ); +CREATE INDEX default_privilege_database_id_idx ON metaschema_public.default_privilege ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/embedding_chunks/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/embedding_chunks/table.sql new file mode 100644 index 00000000..76337cb0 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/embedding_chunks/table.sql @@ -0,0 +1,70 @@ +-- Deploy schemas/metaschema_public/tables/embedding_chunks/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/database/table +-- requires: schemas/metaschema_public/tables/table/table +-- requires: schemas/metaschema_public/tables/field/table + +BEGIN; + +CREATE TABLE metaschema_public.embedding_chunks ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + + table_id uuid NOT NULL, + embedding_field_id uuid, + chunks_table_id uuid, + chunks_table_name text, + + -- content field configuration + content_field_name text NOT NULL DEFAULT 'content', + + -- embedding configuration + dimensions int NOT NULL DEFAULT 768, + metric text NOT NULL DEFAULT 'cosine', + + -- chunking configuration + chunk_size int NOT NULL DEFAULT 1000, + chunk_overlap int NOT NULL DEFAULT 200, + chunk_strategy text NOT NULL DEFAULT 'fixed', + + -- metadata fields from parent to copy into chunks + metadata_fields jsonb, + + -- search indexes to create on the chunks content column + -- NULL means "mirror parent table's text search indexes" + search_indexes jsonb, + + -- job configuration + enqueue_chunking_job boolean NOT NULL DEFAULT true, + chunking_task_name text NOT NULL DEFAULT 'generate_chunks', + + -- model config (optional — worker falls back to runtime config when null) + embedding_model text, + embedding_provider text, + + -- FK field on chunks table pointing to parent + parent_fk_field_id uuid, + + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT chunks_table_fkey FOREIGN KEY (chunks_table_id) REFERENCES metaschema_public.table (id) ON DELETE SET NULL, + CONSTRAINT embedding_field_fkey FOREIGN KEY (embedding_field_id) REFERENCES metaschema_public.field (id) ON DELETE SET NULL, + CONSTRAINT parent_fk_field_fkey FOREIGN KEY (parent_fk_field_id) REFERENCES metaschema_public.field (id) ON DELETE SET NULL, + + CONSTRAINT valid_metric CHECK (metric IN ('cosine', 'l2', 'ip')), + CONSTRAINT valid_chunk_strategy CHECK (chunk_strategy IN ('fixed', 'sentence', 'paragraph', 'semantic')), + CONSTRAINT valid_dimensions CHECK (dimensions > 0), + CONSTRAINT valid_chunk_size CHECK (chunk_size > 0), + CONSTRAINT valid_chunk_overlap CHECK (chunk_overlap >= 0 AND chunk_overlap < chunk_size) +); + + +CREATE INDEX embedding_chunks_table_id_idx ON metaschema_public.embedding_chunks ( table_id ); +CREATE INDEX embedding_chunks_database_id_idx ON metaschema_public.embedding_chunks ( database_id ); +CREATE INDEX embedding_chunks_chunks_table_id_idx ON metaschema_public.embedding_chunks ( chunks_table_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/enum/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/enum/table.sql new file mode 100644 index 00000000..ae9ba3a6 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/enum/table.sql @@ -0,0 +1,39 @@ +-- Deploy schemas/metaschema_public/tables/enum/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/database/table +-- requires: schemas/metaschema_public/tables/schema/table +-- requires: schemas/metaschema_public/types/object_category + +BEGIN; + +CREATE TABLE metaschema_public.enum ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL, + name text NOT NULL, + + label text, + description text, + + values text[] NOT NULL DEFAULT '{}', + + smart_tags jsonb, + + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + + tags citext[] NOT NULL DEFAULT '{}', + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + + UNIQUE (schema_id, name) +); + + +CREATE INDEX enum_schema_id_idx ON metaschema_public.enum ( schema_id ); +CREATE INDEX enum_database_id_idx ON metaschema_public.enum ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx.sql new file mode 100644 index 00000000..6182d432 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx.sql @@ -0,0 +1,19 @@ +-- Deploy schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/field/table + +BEGIN; + +CREATE UNIQUE INDEX databases_field_uniq_names_idx ON metaschema_public.field ( + -- strip out any _id, etc., so that if you do create one and make foreign key relation, there is no conflict + -- only apply normalization to uuid fields (FK candidates) to avoid false collisions on text fields like current_role/current_role_id + table_id, DECODE(MD5(LOWER( + CASE + WHEN type->>'name' = 'uuid' THEN regexp_replace(name, '^(.+?)(_row_id|_id|_uuid|_fk|_pk)$', '\1', 'i') + ELSE name + END + )), 'hex') +); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/field/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/field/table.sql new file mode 100644 index 00000000..27959dd0 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/field/table.sql @@ -0,0 +1,74 @@ +-- Deploy schemas/metaschema_public/tables/field/table to pg + + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/table/table + +BEGIN; + +-- TODO should we just query this table and make a view? +-- https://www.postgresql.org/docs/9.2/catalog-pg-attribute.html + +-- IF YOU WANT TO REMOVE THIS TABLE, answer the qustion, how would you add RLS to this: +-- SELECT +-- attrelid::text AS tbl +-- , attname::text AS col +-- , p.attnum::int as id, +-- t.typname as typename + +-- FROM pg_catalog.pg_attribute p +-- INNER JOIN pg_catalog.pg_type t ON (t.oid = p.atttypid) +-- WHERE attrelid = 'dude_schema.products'::regclass +-- AND p.attnum > 0 +-- AND NOT attisdropped; + +CREATE TABLE metaschema_public.field ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + + table_id uuid NOT NULL, + + name text NOT NULL, + label text, + + description text, + smart_tags jsonb, + + is_required boolean NOT NULL DEFAULT FALSE, + api_required boolean NOT NULL DEFAULT FALSE, + default_value jsonb NULL DEFAULT NULL, + + type jsonb NOT NULL, + + field_order int not null default 0, + + regexp text default null, + chk jsonb default null, + chk_expr jsonb default null, + min float default null, + max float default null, + + tags citext[] NOT NULL DEFAULT '{}', + + -- Field categorization for system/module/app fields (mirrors table categorization) + -- category: 'core' for system fields (id, entity_id, actor_id), 'module' for module-generated fields, 'app' for user-defined fields + -- module: the module name that created this field (e.g., 'users', 'permissions', 'memberships') + -- scope: membership_type int (1=app, 2=org, 3=group, NULL=not scoped) + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + UNIQUE (table_id, name) +); + + +CREATE INDEX field_table_id_idx ON metaschema_public.field ( table_id ); +CREATE INDEX field_database_id_idx ON metaschema_public.field ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/foreign_key_constraint/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/foreign_key_constraint/table.sql new file mode 100644 index 00000000..6a5f47c7 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/foreign_key_constraint/table.sql @@ -0,0 +1,46 @@ +-- Deploy schemas/metaschema_public/tables/foreign_key_constraint/table to pg + +-- requires: schemas/metaschema_public/tables/field/table +-- requires: schemas/metaschema_public/tables/table/table +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/types/object_category + +BEGIN; + +CREATE TABLE metaschema_public.foreign_key_constraint ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + + table_id uuid NOT NULL, + name text, + description text, + smart_tags jsonb, + type text, + field_ids uuid[] NOT NULL, + ref_table_id uuid NOT NULL REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + ref_field_ids uuid[] NOT NULL, + delete_action char(1) DEFAULT 'c', -- postgres default is 'a' + update_action char(1) DEFAULT 'a', + + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + + tags citext[] NOT NULL DEFAULT '{}', + + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + UNIQUE(table_id, name), + CHECK (field_ids <> '{}'), + CHECK (ref_field_ids <> '{}') +); + + +CREATE INDEX foreign_key_constraint_table_id_idx ON metaschema_public.foreign_key_constraint ( table_id ); +CREATE INDEX foreign_key_constraint_database_id_idx ON metaschema_public.foreign_key_constraint ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/full_text_search/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/full_text_search/table.sql new file mode 100644 index 00000000..fb695602 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/full_text_search/table.sql @@ -0,0 +1,34 @@ +-- Deploy schemas/metaschema_public/tables/full_text_search/table to pg + +-- requires: schemas/metaschema_public/schema + +BEGIN; + +CREATE TABLE metaschema_public.full_text_search ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + + table_id uuid NOT NULL, + field_id uuid NOT NULL, + field_ids uuid[] NOT NULL, + weights text[] NOT NULL, + langs text[] NOT NULL, + lang_column text, + + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + + -- + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + CHECK (cardinality(field_ids) = cardinality(weights) AND cardinality(weights) = cardinality(langs)) +); + + +CREATE INDEX full_text_search_table_id_idx ON metaschema_public.full_text_search ( table_id ); +CREATE INDEX full_text_search_database_id_idx ON metaschema_public.full_text_search ( database_id ); + + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/function/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/function/table.sql new file mode 100644 index 00000000..0f0b33e3 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/function/table.sql @@ -0,0 +1,26 @@ +-- Deploy schemas/metaschema_public/tables/function/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/database/table +-- requires: schemas/metaschema_public/tables/schema/table + +BEGIN; + +CREATE TABLE metaschema_public.function ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL, + + name text NOT NULL, + + -- + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + + UNIQUE (schema_id, name) +); + +CREATE INDEX function_database_id_idx ON metaschema_public.function ( database_id ); +CREATE INDEX function_schema_id_idx ON metaschema_public.function ( schema_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/index/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/index/table.sql new file mode 100644 index 00000000..3034fd3d --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/index/table.sql @@ -0,0 +1,50 @@ +-- Deploy schemas/metaschema_public/tables/index/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/table/table +-- requires: schemas/metaschema_public/tables/database/table +-- requires: schemas/metaschema_public/types/object_category + +BEGIN; + +CREATE TABLE metaschema_public.index ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + table_id uuid NOT NULL, + name text NOT NULL DEFAULT '', + + field_ids uuid[], + include_field_ids uuid[], + + access_method text NOT NULL DEFAULT 'BTREE', + + index_params jsonb, + where_clause jsonb, + is_unique boolean NOT NULL default false, + + options jsonb, + op_classes text[], + + smart_tags jsonb, + + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + + tags citext[] NOT NULL DEFAULT '{}', + + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + -- index names are UNIQUE across schemas, so for portability we will check against database_id + UNIQUE (database_id, name) +); + + +CREATE INDEX index_table_id_idx ON metaschema_public.index ( table_id ); +CREATE INDEX index_database_id_idx ON metaschema_public.index ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql new file mode 100644 index 00000000..05361345 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql @@ -0,0 +1,1883 @@ +-- Deploy schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed to pg +-- +-- GENERATED FILE — DO NOT EDIT +-- Regenerate with: cd packages/node-type-registry && pnpm generate +-- +-- Node types: 78 + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/node_type_registry/table + +BEGIN; +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'AuthzAllowAll', + 'authz_allow_all', + 'authz', + 'Public Access', + 'Allows all access. Generates TRUE expression.', + '{"type":"object","properties":{}}'::jsonb, + '{"authz"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'AuthzAppMembership', + 'authz_app_membership_check', + 'authz', + 'App Membership Check', + 'App-level membership check (hardcoded membership_type=1). Verifies the user has app membership (optionally with specific permission) without binding to any entity from the row. Uses EXISTS subquery against SPRT table. For entity-scoped checks (org, channel, etc.), use AuthzEntityMembership instead.', + '{"type":"object","properties":{"permission":{"type":"string","description":"Single permission name to check (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check (ORed together into mask)"},"is_admin":{"type":"boolean","description":"If true, require is_admin flag"},"is_owner":{"type":"boolean","description":"If true, require is_owner flag"}},"required":[]}'::jsonb, + '{"membership","authz"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'AuthzComposite', + 'authz_composite', + 'authz', + 'Composite Policy', + 'Composite authorization policy that combines multiple authorization nodes using boolean logic (AND/OR). The data field contains a JSONB AST with nested authorization nodes.', + '{"type":"object","description":"A composite policy containing nested authorization nodes combined with boolean logic","properties":{"BoolExpr":{"type":"object","description":"Boolean expression combining multiple authorization nodes","properties":{"boolop":{"type":"string","enum":["AND_EXPR","OR_EXPR","NOT_EXPR"],"description":"Boolean operator: AND_EXPR, OR_EXPR, or NOT_EXPR"},"args":{"type":"array","description":"Array of authorization nodes to combine","items":{"type":"object"}}}}}}'::jsonb, + '{"composite","authz"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'AuthzDenyAll', + 'authz_deny_all', + 'authz', + 'No Access', + 'Denies all access. Generates FALSE expression.', + '{"type":"object","properties":{}}'::jsonb, + '{"authz"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'AuthzDirectOwner', + 'authz_direct_owner', + 'authz', + 'Direct Ownership', + 'Direct equality comparison between a table column and the current user ID. Simplest authorization pattern with no subqueries.', + '{"type":"object","properties":{"entity_field":{"type":"string","format":"column-ref","description":"Column name containing the owner user ID (e.g., owner_id)"}},"required":["entity_field"]}'::jsonb, + '{"ownership","authz"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'AuthzDirectOwnerAny', + 'authz_direct_owner_any', + 'authz', + 'Multi-Owner Access', + 'OR logic for multiple ownership fields. Checks if current user matches any of the specified fields.', + '{"type":"object","properties":{"entity_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Array of column names to check for ownership"}},"required":["entity_fields"]}'::jsonb, + '{"ownership","authz"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'AuthzEntityMembership', + 'authz_entity_membership', + 'authz', + 'Entity Membership', + 'Membership check scoped by a field on the row through the SPRT table. Verifies user has membership in the entity referenced by the row.', + '{"type":"object","properties":{"entity_field":{"type":"string","format":"column-ref","description":"Column name referencing the entity (e.g., entity_id, org_id)"},"sel_field":{"type":"string","description":"SPRT column to select for the entity match","default":"entity_id"},"membership_type":{"type":["integer","string"],"description":"Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"},"entity_type":{"type":"string","description":"Entity type prefix (e.g. ''channel'', ''department''). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."},"permission":{"type":"string","description":"Single permission name to check (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check (ORed together into mask)"},"is_admin":{"type":"boolean","description":"If true, require is_admin flag"},"is_owner":{"type":"boolean","description":"If true, require is_owner flag"}},"required":["entity_field"]}'::jsonb, + '{"membership","authz"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'AuthzFilePath', + 'authz_file_path', + 'authz', + 'File Path Share', + 'Path-scoped file sharing via ltree containment. Grants access when a path_shares row matches the current user, bucket, and an ancestor path with the required permission.', + '{"type":"object","properties":{"shares_schema":{"type":"string","description":"Schema of the path_shares table"},"shares_table":{"type":"string","description":"Name of the path_shares table"},"files_schema":{"type":"string","description":"Schema of the files table (used to qualify column references inside the EXISTS subquery)"},"files_table":{"type":"string","description":"Name of the files table (used to qualify column references inside the EXISTS subquery)"},"permission_field":{"type":"string","format":"column-ref","description":"Boolean column on the path_shares table that grants the required permission (e.g. can_read, can_write)"},"bucket_field":{"type":"string","format":"column-ref","description":"Column on the files table referencing the bucket","default":"bucket_id"},"path_field":{"type":"string","format":"column-ref","description":"Ltree column on the files table representing the file path","default":"path"}},"required":["shares_schema","shares_table","files_table","permission_field"]}'::jsonb, + '{"storage","authz"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'AuthzMemberList', + 'authz_member_list', + 'authz', + 'Member List', + 'Check if current user is in an array column on the same row.', + '{"type":"object","properties":{"array_field":{"type":"string","format":"column-ref","description":"Column name containing the array of user IDs"}},"required":["array_field"]}'::jsonb, + '{"ownership","authz"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'AuthzMemberOwner', + 'authz_member_owner', + 'authz', + 'Member Owner', + 'Compound policy: the row must be owned by the current user (owner_field = current_user_id) AND the current user must be a member of the entity referenced by entity_field. Combines direct ownership with entity membership — the actor can only access rows they own within entities they belong to.', + '{"type":"object","properties":{"owner_field":{"type":"string","format":"column-ref","description":"Column name containing the owner user ID (e.g., owner_id)","default":"owner_id"},"entity_field":{"type":"string","format":"column-ref","description":"Column name referencing the entity (e.g., entity_id)","default":"entity_id"},"sel_field":{"type":"string","description":"SPRT column to select for the entity match","default":"entity_id"},"membership_type":{"type":["integer","string"],"description":"Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"},"entity_type":{"type":"string","description":"Entity type prefix (e.g. ''channel'', ''department''). Resolved to membership_type integer via memberships_module lookup."},"permission":{"type":"string","description":"Single permission name to check (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check (ORed together into mask)"}},"required":["owner_field","entity_field"]}'::jsonb, + '{"ownership","membership","authz"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'AuthzNotReadOnly', + 'authz_not_read_only', + 'authz', + 'Not Read-Only', + 'Restrictive policy that blocks read-only members from mutations. Checks actor_id + is_read_only IS NOT TRUE on the SPRT. Designed to run as a restrictive counterpart after a permissive AuthzEntityMembership policy has already verified membership.', + '{"type":"object","properties":{"entity_field":{"type":"string","format":"column-ref","description":"Column name referencing the entity (e.g., entity_id, org_id)"},"membership_type":{"type":["integer","string"],"description":"Scope: 2=org, 3+=dynamic entity types. Must be >= 2 (entity-scoped)."}},"required":["entity_field"]}'::jsonb, + '{"membership","authz","restrictive"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'AuthzOrgHierarchy', + 'authz_org_hierarchy', + 'authz', + 'Org Hierarchy', + 'Organizational hierarchy visibility using closure table. Managers can see subordinate data or subordinates can see manager data.', + '{"type":"object","properties":{"direction":{"type":"string","enum":["up","down"],"description":"down=manager sees subordinates, up=subordinate sees managers"},"entity_field":{"type":"string","format":"column-ref","description":"Field referencing the org entity","default":"entity_id"},"anchor_field":{"type":"string","format":"column-ref","description":"Field referencing the user (e.g., owner_id)"},"max_depth":{"type":"integer","description":"Optional max depth to limit visibility"}},"required":["direction","anchor_field"]}'::jsonb, + '{"membership","hierarchy","authz"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'AuthzPeerOwnership', + 'authz_peer_ownership', + 'authz', + 'Peer Ownership', + 'Peer visibility through shared entity membership. Authorizes access to user-owned rows when the owner and current user are both members of the same entity. Self-joins the SPRT table to find peers.', + '{"type":"object","properties":{"owner_field":{"type":"string","format":"column-ref","description":"Column name on protected table referencing the owning user (e.g., owner_id)"},"membership_type":{"type":["integer","string"],"description":"Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"},"entity_type":{"type":"string","description":"Entity type prefix (e.g. ''channel'', ''department''). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."},"permission":{"type":"string","description":"Single permission name to check on the current user membership (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check on the current user membership (ORed together into mask)"},"is_admin":{"type":"boolean","description":"If true, require is_admin flag on current user membership"},"is_owner":{"type":"boolean","description":"If true, require is_owner flag on current user membership"}},"required":["owner_field"]}'::jsonb, + '{"membership","peer","authz"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'AuthzPublishable', + 'authz_publishable', + 'authz', + 'Published Content', + 'Published state access control. Restricts access to records that are published.', + '{"type":"object","properties":{"is_published_field":{"type":"string","format":"column-ref","description":"Boolean field indicating published state","default":"is_published"},"published_at_field":{"type":"string","format":"column-ref","description":"Timestamp field for publish time","default":"published_at"},"require_published_at":{"type":"boolean","description":"Require published_at to be non-null and <= now()","default":true}}}'::jsonb, + '{"temporal","publishing","authz"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'AuthzRelatedEntityMembership', + 'authz_related_entity_membership', + 'authz', + 'Related Entity Membership', + 'JOIN-based membership verification through related tables. Joins SPRT table with another table to verify membership.', + '{"type":"object","properties":{"entity_field":{"type":"string","format":"column-ref","description":"Column name on protected table referencing the join table"},"sel_field":{"type":"string","description":"SPRT column to select for the entity match","default":"entity_id"},"sprt_join_field":{"type":"string","description":"SPRT column to join on with the related table","default":"entity_id"},"membership_type":{"type":["integer","string"],"description":"Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"},"entity_type":{"type":"string","description":"Entity type prefix (e.g. ''channel'', ''department''). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."},"obj_table_id":{"type":"string","format":"uuid","description":"UUID of the join table (alternative to obj_schema/obj_table)"},"obj_schema":{"type":"string","description":"Schema of the join table (or use obj_table_id)"},"obj_table":{"type":"string","description":"Name of the join table (or use obj_table_id)"},"obj_field_id":{"type":"string","format":"uuid","description":"UUID of field on join table (alternative to obj_field)"},"obj_field":{"type":"string","format":"column-ref","description":"Field name on join table to match against SPRT entity_id"},"permission":{"type":"string","description":"Single permission name to check (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check (ORed together into mask)"},"is_admin":{"type":"boolean","description":"If true, require is_admin flag"},"is_owner":{"type":"boolean","description":"If true, require is_owner flag"}},"required":["entity_field"]}'::jsonb, + '{"membership","authz"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'AuthzRelatedMemberList', + 'authz_related_member_list', + 'authz', + 'Related Member List', + 'Array membership check in a related table.', + '{"type":"object","properties":{"owned_schema":{"type":"string","description":"Schema of the related table"},"owned_table":{"type":"string","description":"Name of the related table"},"owned_table_key":{"type":"string","format":"column-ref","description":"Array column in related table"},"owned_table_ref_key":{"type":"string","format":"column-ref","description":"FK column in related table"},"this_object_key":{"type":"string","format":"column-ref","description":"PK column in protected table"}},"required":["owned_schema","owned_table","owned_table_key","owned_table_ref_key","this_object_key"]}'::jsonb, + '{"ownership","authz"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'AuthzRelatedPeerOwnership', + 'authz_related_peer_ownership', + 'authz', + 'Related Peer Ownership', + 'Peer visibility through shared entity membership via a related table. Like AuthzPeerOwnership but the owning user is resolved through a FK JOIN to a related table. Combines SPRT self-join with object table JOIN.', + '{"type":"object","properties":{"entity_field":{"type":"string","format":"column-ref","description":"Column name on protected table referencing the related table (e.g., message_id)"},"membership_type":{"type":["integer","string"],"description":"Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"},"entity_type":{"type":"string","description":"Entity type prefix (e.g. ''channel'', ''department''). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."},"obj_table_id":{"type":"string","format":"uuid","description":"UUID of the related table (alternative to obj_schema/obj_table)"},"obj_schema":{"type":"string","description":"Schema of the related table (or use obj_table_id)"},"obj_table":{"type":"string","description":"Name of the related table (or use obj_table_id)"},"obj_field_id":{"type":"string","format":"uuid","description":"UUID of field on related table containing the owner user ID (alternative to obj_field)"},"obj_field":{"type":"string","format":"column-ref","description":"Field name on related table containing the owner user ID (e.g., sender_id)"},"obj_ref_field":{"type":"string","format":"column-ref","description":"Field on related table to select for matching entity_field","default":"id"},"permission":{"type":"string","description":"Single permission name to check on the current user membership (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check on the current user membership (ORed together into mask)"},"is_admin":{"type":"boolean","description":"If true, require is_admin flag on current user membership"},"is_owner":{"type":"boolean","description":"If true, require is_owner flag on current user membership"}},"required":["entity_field"]}'::jsonb, + '{"membership","peer","authz"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'AuthzTemporal', + 'authz_temporal', + 'authz', + 'Temporal Access', + 'Time-window based access control. Restricts access based on valid_from and/or valid_until timestamps. At least one of valid_from_field or valid_until_field must be provided.', + '{"type":"object","properties":{"valid_from_field":{"type":"string","format":"column-ref","description":"Column for start time (at least one of valid_from_field or valid_until_field required)"},"valid_until_field":{"type":"string","format":"column-ref","description":"Column for end time (at least one of valid_from_field or valid_until_field required)"},"valid_from_inclusive":{"type":"boolean","description":"Include start boundary","default":true},"valid_until_inclusive":{"type":"boolean","description":"Include end boundary","default":false}},"anyOf":[{"required":["valid_from_field"]},{"required":["valid_until_field"]}]}'::jsonb, + '{"temporal","authz"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'CheckGreaterThan', + 'check_greater_than', + 'check', + 'Check Greater Than', + 'Adds a CHECK constraint that validates a column value is greater than a threshold (single-column: column > value) or that one column is greater than another (cross-column: columns[0] > columns[1]). Compiled via AST helpers.', + '{"type":"object","properties":{"column":{"type":"string","format":"column-ref","description":"Single column to compare against value (mutually exclusive with columns)"},"value":{"type":"number","description":"Threshold value for single-column comparison (column > value)","default":0},"columns":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Two columns for cross-column comparison (columns[0] > columns[1])","minItems":2,"maxItems":2}}}'::jsonb, + '{"check","constraint","validation","comparison"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'CheckLessThan', + 'check_less_than', + 'check', + 'Check Less Than', + 'Adds a CHECK constraint that validates a column value is less than a threshold (single-column: column < value) or that one column is less than another (cross-column: columns[0] < columns[1]). Compiled via AST helpers.', + '{"type":"object","properties":{"column":{"type":"string","format":"column-ref","description":"Single column to compare against value (mutually exclusive with columns)"},"value":{"type":"number","description":"Threshold value for single-column comparison (column < value)"},"columns":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Two columns for cross-column comparison (columns[0] < columns[1])","minItems":2,"maxItems":2}}}'::jsonb, + '{"check","constraint","validation","comparison"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'CheckNotEqual', + 'check_not_equal', + 'check', + 'Check Not Equal', + 'Adds a CHECK constraint that validates two columns are not equal (columns[0] != columns[1]). Useful for preventing self-referencing rows. Compiled via AST helpers.', + '{"type":"object","properties":{"columns":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Two columns that must not be equal","minItems":2,"maxItems":2}},"required":["columns"]}'::jsonb, + '{"check","constraint","validation","inequality"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'CheckOneOf', + 'check_one_of', + 'check', + 'Check One Of', + 'Adds a CHECK constraint that validates a column value is one of an allowed set (e.g. tier IN (''free'', ''paid'', ''custom'')). Compiled to column = ANY(ARRAY[...]) via AST helpers.', + '{"type":"object","properties":{"column":{"type":"string","format":"column-ref","description":"Column to validate against the allowed values"},"values":{"type":"array","items":{"type":"string"},"description":"Array of allowed values for the column"}},"required":["column","values"]}'::jsonb, + '{"check","constraint","validation","enum"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataArchivable', + 'data_archivable', + 'data', + 'Archivable', + 'Adds user-reversible archive support with is_archived boolean and archived_at timestamp, plus a partial index for efficient active-row queries.', + '{"type":"object","properties":{"is_archived_field":{"type":"string","format":"column-ref","description":"Column name for the archive boolean flag","default":"is_archived"},"archived_at_field":{"type":"string","format":"column-ref","description":"Column name for the archive timestamp","default":"archived_at"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true}}}'::jsonb, + '{"schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataBulk', + 'data_bulk', + 'data', + 'Bulk Operations', + 'Enables bulk mutation smart tags on a table. When provisioned, adds @behavior tags for the selected bulk operations (insert, upsert, update, delete). Requires the graphile-bulk-mutations plugin.', + '{"type":"object","properties":{"insert":{"type":"boolean","description":"Enable bulk insert (+bulkInsert)","default":true},"upsert":{"type":"boolean","description":"Enable bulk upsert (+bulkUpsert)","default":false},"update":{"type":"boolean","description":"Enable bulk update (+bulkUpdate)","default":false},"delete":{"type":"boolean","description":"Enable bulk delete (+bulkDelete)","default":false}}}'::jsonb, + '{"bulk","mutations","graphile"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataCompositeField', + 'data_composite_field', + 'data', + 'Composite Field', + 'Creates a derived text field that automatically concatenates multiple source fields via BEFORE INSERT/UPDATE triggers. Used to produce a unified text representation (e.g., embedding_text) from multiple columns on a table. The trigger fires with ''_000'' prefix to run before Search* triggers alphabetically.', + '{"type":"object","properties":{"target":{"type":"string","format":"column-ref","description":"Name of the derived text field to create","default":"embedding_text"},"source_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Array of source field names to concatenate into the target field"},"format":{"type":"string","enum":["labeled","plain"],"description":"Output format: ''labeled'' (field_name: value) or ''plain'' (values only)","default":"labeled"}},"required":["source_fields"]}'::jsonb, + '{"transform","behavior"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataDirectOwner', + 'data_direct_owner', + 'data', + 'Ownership', + 'Adds ownership column for direct user ownership. Enables AuthzDirectOwner authorization.', + '{"type":"object","properties":{"owner_field_name":{"type":"string","format":"column-ref","description":"Column name for owner ID","default":"owner_id"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds a foreign key constraint from owner_id to the users table","default":true},"create_index":{"type":"boolean","description":"If true, creates a B-tree index on the owner column","default":true}}}'::jsonb, + '{"ownership","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataEntityMembership', + 'data_entity_membership', + 'data', + 'Entity Membership', + 'Adds entity reference for organization/group scoping. Enables AuthzEntityMembership, AuthzMembership, AuthzOrgHierarchy authorization.', + '{"type":"object","properties":{"entity_field_name":{"type":"string","format":"column-ref","description":"Column name for entity ID","default":"entity_id"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds a foreign key constraint from entity_id to the users table","default":true},"create_index":{"type":"boolean","description":"If true, creates a B-tree index on the entity column","default":true}}}'::jsonb, + '{"membership","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataForceCurrentUser', + 'data_force_current_user', + 'data', + 'Force Current User', + 'BEFORE INSERT trigger that forces a field to the value of jwt_public.current_user_id(). Prevents clients from spoofing the actor/uploader identity. The field value is always overwritten regardless of what the client provides.', + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the field to force to current_user_id()","default":"actor_id"}}}'::jsonb, + '{"trigger","security","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataI18n', + 'data_i18n', + 'data', + 'Internationalization', + 'Creates a companion _translations table with lang_code + translatable fields. Copies SELECT policies and column-ref fields from the base table. Adds @i18n smart comment so the Graphile i18n plugin discovers it. Requires i18n_module to be provisioned for the database.', + '{"type":"object","properties":{"fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names on the base table to make translatable. Each field is duplicated on the translation table with the same type."},"table_suffix":{"type":"string","description":"Suffix for the translation table name","default":"_translations"},"lang_code_type":{"type":"string","enum":["citext","text"],"description":"Type for the lang_code column","default":"citext"},"copy_mutation_policies":{"type":"boolean","description":"Whether to also copy INSERT/UPDATE/DELETE policies (not just SELECT). Default true — translations should be editable by the same users who can edit the base row.","default":true},"search":{"type":"object","description":"SearchFullText configuration for the translations table. When provided, creates a tsvector column on the translations table with lang_column=lang_code for dynamic per-row language stemming.","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the tsvector column on the translations table","default":"search"},"source_fields":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","format":"column-ref","description":"Name of the translatable source column"},"weight":{"type":"string","enum":["A","B","C","D"],"description":"tsvector weight class (A=highest, D=lowest)","default":"D"}},"required":["field"]},"description":"Translatable columns that feed the tsvector. Language is determined dynamically from the lang_code column of each row."},"search_score_weight":{"type":"number","description":"Weight for this algorithm in composite searchScore","default":1}},"required":["source_fields"]}},"required":["fields"]}'::jsonb, + '{"i18n","translation","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataId', + 'data_id', + 'data', + 'Primary Key ID', + 'Adds a UUID primary key column with auto-generation default (uuidv7). This is the standard primary key pattern for all tables.', + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the primary key","default":"id"}}}'::jsonb, + '{"primary_key","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataImmutableFields', + 'data_immutable_fields', + 'data', + 'Immutable Fields', + 'BEFORE UPDATE trigger that prevents changes to a list of specified fields after INSERT. Raises an exception if any of the listed fields have changed. Unlike FieldImmutable (single-field), this handles multiple fields in a single trigger for efficiency.', + '{"type":"object","properties":{"fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names that cannot be modified after INSERT (e.g. [\"key\", \"bucket_id\", \"owner_id\"])"}},"required":["fields"]}'::jsonb, + '{"trigger","constraint","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataInflection', + 'data_inflection', + 'data', + 'Inflection', + 'Transforms field values using inflection operations (snake_case, camelCase, slugify, plural, singular, etc). Attaches BEFORE INSERT and BEFORE UPDATE triggers. References fields by name in data jsonb.', + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the field to transform"},"ops":{"type":"array","items":{"type":"string","enum":["plural","singular","camel","pascal","dashed","slugify","underscore","lower","upper"]},"description":"Inflection operations to apply in order"}},"required":["field_name","ops"]}'::jsonb, + '{"transform","behavior"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataInheritFromParent', + 'data_inherit_from_parent', + 'data', + 'Inherit From Parent', + 'BEFORE INSERT trigger that copies specified fields from a parent table via a foreign key. The parent row is looked up through RLS (SECURITY INVOKER), so the insert fails if the caller cannot see the parent. Used by the storage module to inherit owner_id and is_public from buckets to files.', + '{"type":"object","properties":{"parent_fk_field":{"type":"string","format":"column-ref","description":"Name of the FK field on this table that references the parent (e.g. bucket_id)"},"fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names to copy from the parent row (e.g. [\"owner_id\", \"is_public\"])"},"parent_table":{"type":"string","description":"Parent table name (optional fallback if FK not yet registered in metaschema)"},"parent_schema":{"type":"string","description":"Parent table schema (optional, defaults to same schema as child table)"}},"required":["parent_fk_field","fields"]}'::jsonb, + '{"trigger","inheritance","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataJsonb', + 'data_jsonb', + 'data', + 'JSONB Field', + 'Adds a JSONB column with optional GIN index for containment queries (@>, ?, ?|, ?&). Standard pattern for semi-structured metadata.', + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the JSONB field","default":"metadata"},"default_value":{"type":"object","description":"Default value as a FieldDefault object","default":{"value":{},"cast":{"name":"jsonb"}}},"is_required":{"type":"boolean","description":"Whether the column has a NOT NULL constraint","default":false},"create_index":{"type":"boolean","description":"Whether to create a GIN index","default":true}}}'::jsonb, + '{"jsonb","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataMemberOwner', + 'data_member_owner', + 'data', + 'Member Owner', + 'Adds owner_id and entity_id columns with a compound AuthzMemberOwner policy. The actor must own the row (owner_id = current_user_id()) AND be a member of the entity (entity_id in SPRT). Use for private data within an entity scope — e.g., personal chat threads that belong to the company but only the author can see.', + '{"type":"object","properties":{"owner_field_name":{"type":"string","format":"column-ref","description":"Column name for the owner reference","default":"owner_id"},"entity_field_name":{"type":"string","format":"column-ref","description":"Column name for the entity reference","default":"entity_id"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds foreign key constraints from owner_id and entity_id to the users table","default":true},"create_index":{"type":"boolean","description":"If true, creates B-tree indexes on the owner and entity columns","default":true},"membership_type":{"type":"integer","description":"Membership type for SPRT resolution. Required for entity-scoped provisioning.","default":null}}}'::jsonb, + '{"ownership","membership","security","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataOwnedFields', + 'data_owned_fields', + 'data', + 'Owned Fields', + 'Restricts which user can modify specific columns in shared objects. Creates an AFTER UPDATE trigger that throws OWNED_PROPS when a non-owner tries to change protected fields. References fields by name in data jsonb.', + '{"type":"object","properties":{"role_key_field_name":{"type":"string","format":"column-ref","description":"Name of the field identifying the owner (e.g. sender_id)"},"protected_field_names":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Names of fields only this owner can modify"}},"required":["role_key_field_name","protected_field_names"]}'::jsonb, + '{"ownership","constraint","behavior"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataOwnershipInEntity', + 'data_ownership_in_entity', + 'data', + 'Ownership In Entity', + 'Combines direct ownership with entity scoping. Adds both owner_id and entity_id columns. Enables AuthzDirectOwner, AuthzEntityMembership, and AuthzOrgHierarchy authorization. Particularly useful for OrgHierarchy where a user owns a row (owner_id) within an entity (entity_id), and managers above can see subordinate-owned records via the hierarchy closure table.', + '{"type":"object","properties":{"owner_field_name":{"type":"string","format":"column-ref","description":"Column name for the owner reference","default":"owner_id"},"entity_field_name":{"type":"string","format":"column-ref","description":"Column name for the entity reference","default":"entity_id"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds foreign key constraints from owner_id and entity_id to the users table","default":true},"create_index":{"type":"boolean","description":"If true, creates B-tree indexes on the owner and entity columns","default":true}}}'::jsonb, + '{"ownership","membership","hierarchy","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataPeoplestamps', + 'data_peoplestamps', + 'data', + 'Peoplestamps', + 'Adds user tracking for creates/updates with created_by and updated_by columns.', + '{"type":"object","properties":{"created_by_field":{"type":"string","format":"column-ref","description":"Column name for the creating user reference","default":"created_by"},"updated_by_field":{"type":"string","format":"column-ref","description":"Column name for the last-updating user reference","default":"updated_by"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds foreign key constraints from created_by and updated_by to the users table","default":false},"create_index":{"type":"boolean","description":"If true, creates B-tree indexes on the peoplestamp columns","default":true}}}'::jsonb, + '{"timestamps","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataPublishable', + 'data_publishable', + 'data', + 'Publishable', + 'Adds publish state columns (is_published, published_at) for content visibility. Enables AuthzPublishable and AuthzTemporal authorization.', + '{"type":"object","properties":{"is_published_field_name":{"type":"string","format":"column-ref","description":"Column name for the published boolean flag","default":"is_published"},"published_at_field_name":{"type":"string","format":"column-ref","description":"Column name for the publish timestamp","default":"published_at"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true}}}'::jsonb, + '{"publishing","temporal","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataRealtime', + 'data_realtime', + 'data', + 'Realtime Subscriptions', + 'Creates per-table subscriber tables in subscriptions_public with RLS policies derived from source table SELECT policies. Attaches statement-level triggers to emit changes to subscribers.', + '{"type":"object","properties":{"operations":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE","DELETE"]},"description":"Which DML operations to track with emit_change triggers","default":["INSERT","UPDATE","DELETE"]},"subscriber_table_name":{"type":"string","description":"Custom name for the subscriber table (defaults to {source_table}_subscriber)"}}}'::jsonb, + '{"realtime","subscriptions","triggers"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataSlug', + 'data_slug', + 'data', + 'Slug', + 'Auto-generates URL-friendly slugs from field values on insert/update. Attaches BEFORE INSERT and BEFORE UPDATE triggers that call inflection.slugify() on the target field. References fields by name in data jsonb.', + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the field to slugify","default":"slug"},"source_field_name":{"type":"string","format":"column-ref","description":"Optional source field name (defaults to field_name)"}},"required":[]}'::jsonb, + '{"transform","behavior"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataSoftDelete', + 'data_soft_delete', + 'data', + 'Soft Delete', + 'Adds soft delete support with deleted_at and is_deleted columns.', + '{"type":"object","properties":{"deleted_at_field":{"type":"string","format":"column-ref","description":"Column name for the soft-delete timestamp","default":"deleted_at"},"is_deleted_field":{"type":"string","format":"column-ref","description":"Column name for the soft-delete boolean flag","default":"is_deleted"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true}}}'::jsonb, + '{"schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataStatusField', + 'data_status_field', + 'data', + 'Status Field', + 'Adds a status column with B-tree index for efficient equality filtering and sorting. Optionally constrains values via CHECK constraint when allowed_values is provided.', + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the status field","default":"status"},"type":{"type":"object","description":"Column type as a FieldType object","default":{"name":"text"}},"default_value":{"type":"string","description":"Default value expression (e.g., active)"},"is_required":{"type":"boolean","description":"Whether the column has a NOT NULL constraint","default":true},"allowed_values":{"type":"array","items":{"type":"string"},"description":"If provided, creates a CHECK constraint restricting the column to these values"}}}'::jsonb, + '{"status","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataTags', + 'data_tags', + 'data', + 'Tags', + 'Adds a citext[] tags column with GIN index for efficient array containment queries (@>, &&). Standard tagging pattern for categorization and filtering.', + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the tags array","default":"tags"},"default_value":{"type":"object","description":"Default value as a FieldDefault object","default":{"value":[],"cast":{"name":"citext","array_dimensions":1}}},"is_required":{"type":"boolean","description":"Whether the column has a NOT NULL constraint","default":false}}}'::jsonb, + '{"tags","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataTimestamps', + 'data_timestamps', + 'data', + 'Timestamps', + 'Adds automatic timestamp tracking with created_at and updated_at columns.', + '{"type":"object","properties":{"created_at_field":{"type":"string","format":"column-ref","description":"Column name for the creation timestamp","default":"created_at"},"updated_at_field":{"type":"string","format":"column-ref","description":"Column name for the last-updated timestamp","default":"updated_at"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true}}}'::jsonb, + '{"timestamps","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'EventReferral', + 'event_referral', + 'event', + 'Event Referral', + 'Creates triggers that record events for the referrer (inviter) when their invitees perform actions on a watched table. Resolves the referrer automatically via the invites module''s claimed_invites table using the membership_type context. Supports the same compound condition system as EventTracker. Use with achievements to unlock levels and grant credits based on invitee activity.', + '{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"event_name":{"type":"string","description":"Event type name to record for the referrer (e.g., \"invitee_uploaded_avatar\", \"invitee_completed_onboarding\")"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE","DELETE"]},"description":"DML events that trigger recording","default":["INSERT"]},"actor_field":{"type":"string","format":"column-ref","description":"Column containing the invitee (actor) ID on the source table — used to look up the referrer via claimed_invites.receiver_id","default":"owner_id"},"entity_field":{"type":"string","format":"column-ref","description":"Column containing the entity ID (org/group) for entity-scoped referral events. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup. Omit for user-only events."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]},"max_depth":{"type":"integer","description":"Maximum depth to walk up the invite chain. Default 1 (direct inviter only). Set 2–10 to enable multi-level referral rewards. App-level only — must not be combined with entity_field.","default":1,"minimum":1,"maximum":10},"auto_register_type":{"type":"boolean","description":"Automatically register the event_name in event_types during provisioning","default":true},"condition_field":{"type":"string","format":"column-ref","description":"Column name for conditional WHEN clause (fires only when field equals condition_value)"},"condition_value":{"type":"string","description":"Value to compare against condition_field in WHEN clause"},"conditions":{"description":"Compound conditions for the trigger WHEN clause. Accepts a single leaf condition, an array of conditions (implicitly AND), or a nested combinator tree ({AND: [...], OR: [...], NOT: {...}}). Each leaf is {field, op, value?, row?, ref?}. Column types are resolved automatically from the table schema. Cannot be combined with condition_field or watch_fields.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"watch_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"For UPDATE triggers, only fire when these fields change (uses DISTINCT FROM)"}},"required":["event_name"]}'::jsonb, + '{"events","referral","invites","analytics","tracking"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'EventTracker', + 'event_tracker', + 'event', + 'Event Tracker', + 'Creates triggers that record events via the events module when table rows change. Supports the same compound condition system as JobTrigger (condition_field, watch_fields, or full AND/OR/NOT conditions). Events are recorded to app_events and aggregated automatically. Use with achievements (blueprint-level) to unlock levels and grant credits based on event accumulation.', + '{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"event_name":{"type":"string","description":"Event type name to record (e.g., \"avatar_uploaded\", \"order_completed\")"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE","DELETE"]},"description":"DML events that trigger recording","default":["INSERT"]},"count":{"type":"integer","description":"Number of events to record per trigger fire","default":1},"toggle":{"type":"boolean","description":"Toggle mode: records event when condition is met, removes when condition is unmet","default":false},"actor_field":{"type":"string","format":"column-ref","description":"Column containing the actor (user) ID to attribute the event to","default":"owner_id"},"entity_field":{"type":"string","format":"column-ref","description":"Column containing the entity ID (org/group) for entity-scoped events. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup. Omit for user-only events."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]},"auto_register_type":{"type":"boolean","description":"Automatically register the event_name in event_types during provisioning","default":true},"condition_field":{"type":"string","format":"column-ref","description":"Column name for conditional WHEN clause (fires only when field equals condition_value)"},"condition_value":{"type":"string","description":"Value to compare against condition_field in WHEN clause"},"conditions":{"description":"Compound conditions for the trigger WHEN clause. Accepts a single leaf condition, an array of conditions (implicitly AND), or a nested combinator tree ({AND: [...], OR: [...], NOT: {...}}). Each leaf is {field, op, value?, row?, ref?}. Column types are resolved automatically from the table schema. Cannot be combined with condition_field or watch_fields.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"watch_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"For UPDATE triggers, only fire when these fields change (uses DISTINCT FROM)"}},"required":["event_name"]}'::jsonb, + '{"events","triggers","analytics","tracking"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'JobTrigger', + 'data_job_trigger', + 'job', + 'Job Trigger', + 'Dynamically creates PostgreSQL triggers that enqueue jobs via app_jobs.add_job() when table rows are inserted, updated, or deleted. Supports configurable payload strategies (full row, row ID, selected fields, or custom mapping), conditional firing via WHEN clauses, watched field changes, and extended job options (queue, priority, delay, max attempts).', + '{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"task_identifier":{"type":"string","format":"function-ref","description":"Job task identifier passed to add_job (e.g., process_invoice, sync_to_stripe). Must match a registered function definition when function_module is installed."},"payload_strategy":{"type":"string","enum":["row","row_id","fields","custom"],"description":"How to build the job payload: row (full NEW/OLD), row_id (just id), fields (selected columns), custom (mapped columns)","default":"row_id"},"payload_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Column names to include in payload (only for fields strategy)"},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Key-to-column mapping for custom payload (e.g., {\"invoice_id\": \"id\", \"total\": \"amount\"})"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE","DELETE"]},"description":"Trigger events to create","default":["INSERT","UPDATE"]},"include_old":{"type":"boolean","description":"Include OLD row in payload (for UPDATE triggers)","default":false},"include_meta":{"type":"boolean","description":"Include table/schema metadata in payload","default":false},"condition_field":{"type":"string","format":"column-ref","description":"Column name for conditional WHEN clause (fires only when field equals condition_value)"},"condition_value":{"type":"string","description":"Value to compare against condition_field in WHEN clause"},"conditions":{"description":"Compound conditions for the trigger WHEN clause. Accepts a single leaf condition, an array of conditions (implicitly AND), or a nested combinator tree ({AND: [...], OR: [...], NOT: {...}}). Each leaf is {field, op, value?, row?, ref?}. Column types are resolved automatically from the table schema. Cannot be combined with condition_field or watch_fields.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"watch_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"For UPDATE triggers, only fire when these fields change (uses DISTINCT FROM)"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the trigger table that holds (or references) the entity_id for billing scope. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]},"job_key":{"type":"string","description":"Static job key for upsert semantics (prevents duplicate jobs)"},"queue_name":{"type":"string","description":"Job queue name for routing to specific workers"},"priority":{"type":"integer","description":"Job priority (lower = higher priority)","default":0},"run_at_delay":{"type":"string","description":"Delay before job runs as PostgreSQL interval (e.g., 30 seconds, 5 minutes)"},"max_attempts":{"type":"integer","description":"Maximum retry attempts for the job","default":25}},"required":["task_identifier"]}'::jsonb, + '{"jobs","triggers","async"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'LimitEnforceAggregate', + 'limit_enforce_aggregate', + 'limit_enforce', + 'Enforce Aggregate Counter', + 'Declaratively attaches aggregate limit-tracking triggers to a table. On INSERT the named limit is incremented per entity; on DELETE it is decremented. Uses org_limit_aggregates_inc/dec for per-entity (org-level) aggregate limits rather than per-user limits. Requires a provisioned limits_module for the target database.', + '{"type":"object","properties":{"limit_name":{"type":"string","description":"Name of the aggregate limit to track (must match a default_limits entry, e.g. \"databases\", \"members\")"},"scope":{"type":"string","description":"Membership type prefix that determines which limits_module row to use. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \"org\", \"data_room\", \"channel\", \"team\").","default":"org"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for aggregate limit lookup. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]},"events":{"type":"array","items":{"type":"string","enum":["INSERT","DELETE","UPDATE"]},"description":"Which DML events to attach triggers for","default":["INSERT","DELETE"]}},"required":["limit_name"]}'::jsonb, + '{"limits","triggers","aggregates","enforce"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'LimitEnforceCounter', + 'limit_enforce_counter', + 'limit_enforce', + 'Enforce Counter', + 'Declaratively attaches limit-tracking triggers to a table. On INSERT the named limit is incremented; on DELETE it is decremented. Requires a provisioned limits_module for the target scope.', + '{"type":"object","properties":{"limit_name":{"type":"string","description":"Name of the limit to track (must match a default_limits entry, e.g. \"projects\", \"members\")"},"scope":{"type":"string","description":"Membership type prefix that determines which limits_module row to use. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \"app\", \"org\", \"data_room\", \"channel\", \"team\").","default":"app"},"actor_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the actor or entity id used for limit lookup","default":"owner_id"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for entity context resolution. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]},"events":{"type":"array","items":{"type":"string","enum":["INSERT","DELETE","UPDATE"]},"description":"Which DML events to attach triggers for","default":["INSERT","DELETE"]}},"required":["limit_name"]}'::jsonb, + '{"limits","triggers","enforce"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'LimitEnforceFeature', + 'limit_enforce_feature', + 'limit_enforce', + 'Enforce Feature Flag', + 'Gates a table behind a feature flag backed by the cap tables. Attaches a BEFORE INSERT trigger that checks whether the named feature cap value is > 0. Features are modeled as caps with max=0 (disabled) or max=1 (enabled) in limit_caps / limit_caps_defaults tables. Resolution: COALESCE(per-entity cap, scope default, 0).', + '{"type":"object","properties":{"feature_name":{"type":"string","description":"Cap name representing this feature (must match a limit_caps_defaults entry with max=0 or max=1)"},"scope":{"type":"string","description":"Membership type prefix that determines which limits_module row to use. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \"app\", \"org\", \"data_room\", \"channel\", \"team\").","default":"app"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for per-entity cap lookups (only used for org scope). For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]}},"required":["feature_name"]}'::jsonb, + '{"limits","triggers","feature-flags","enforce","caps"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'LimitEnforceRate', + 'limit_enforce_rate', + 'limit_enforce', + 'Enforce Rate Limit', + 'Attaches a BEFORE trigger that calls check_rate_limit() to enforce sliding-window rate limits before allowing mutations. The function checks all three scopes (entity, actor-in-entity, actor) in a single call; which scopes are actually enforced is controlled by what rows exist in rate_window_limits (plan-based config). Requires a provisioned meter_rate_limits_module and billing_module for the target database.', + '{"type":"object","properties":{"meter_slug":{"type":"string","description":"Slug of the billing meter to check rate limits against (must match a meters table entry, e.g. \"messaging\", \"inference\")"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for rate limiting. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]},"actor_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the actor id (user) for rate limiting","default":"owner_id"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Which DML events to enforce rate limits on (DELETE is excluded since it reduces load)","default":["INSERT"]}},"required":["meter_slug"]}'::jsonb, + '{"rate-limits","triggers","enforce","metering","abuse-protection"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'LimitTrackUsage', + 'limit_track_usage', + 'limit_track', + 'Track Usage', + 'Declaratively attaches billing usage-recording triggers to a table. On INSERT the named meter is incremented via record_usage; on DELETE it is decremented (reversal). On UPDATE, if the entity_field changes, the old entity is decremented and the new entity is incremented. Requires a provisioned billing_module for the target database.', + '{"type":"object","properties":{"meter_slug":{"type":"string","description":"Slug of the billing meter to record usage against (must match a meters table entry, e.g. \"databases\", \"seats\")"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for billing. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]},"quantity":{"type":"integer","description":"Units to record per event (default 1)","default":1},"events":{"type":"array","items":{"type":"string","enum":["INSERT","DELETE","UPDATE"]},"description":"Which DML events to attach triggers for","default":["INSERT","DELETE"]}},"required":["meter_slug"]}'::jsonb, + '{"billing","triggers","metering","usage","track"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'LimitWarningAggregate', + 'limit_warning_aggregate', + 'limit_warning', + 'Warning Aggregate', + 'Attaches an AFTER INSERT trigger that checks if the entity''s aggregate usage has crossed any warning threshold configured in the limit_warnings table. If a threshold is reached for the first time, enqueues a background job (e.g. email notification). Uses limit_warning_state for one-time dedup per warning/actor/entity triple. Requires a provisioned limits_module with limit_warnings and aggregate limits enabled.', + '{"type":"object","properties":{"limit_name":{"type":"string","description":"Name of the aggregate limit to watch (must match a limit_warnings.name entry, e.g. \"databases\", \"members\")"},"scope":{"type":"string","description":"Membership type prefix that determines which limits_module row to use. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \"org\", \"data_room\", \"channel\", \"team\").","default":"org"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for aggregate limit lookup. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]}},"required":["limit_name"]}'::jsonb, + '{"limits","triggers","aggregates","warning","notifications"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'LimitWarningCounter', + 'limit_warning_counter', + 'limit_warning', + 'Warning Counter', + 'Attaches an AFTER INSERT trigger that checks if the actor''s current usage has crossed any warning threshold configured in the limit_warnings table. If a threshold is reached for the first time, enqueues a background job (e.g. email notification). Uses limit_warning_state for one-time dedup per warning/actor pair. Requires a provisioned limits_module with limit_warnings enabled.', + '{"type":"object","properties":{"limit_name":{"type":"string","description":"Name of the limit to watch (must match a limit_warnings.name entry, e.g. \"projects\", \"members\")"},"scope":{"type":"string","description":"Membership type prefix that determines which limits_module row to use. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \"app\", \"org\", \"data_room\", \"channel\", \"team\").","default":"app"},"actor_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the actor id for limit lookup","default":"owner_id"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id. When provided, entity_id is included in the job payload and dedup state. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]}},"required":["limit_name"]}'::jsonb, + '{"limits","triggers","warning","notifications"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'LimitWarningRate', + 'limit_warning_rate', + 'limit_warning', + 'Warning Rate Limit', + 'Attaches an AFTER INSERT trigger that checks if the actor''s current request count in the active sliding window has crossed any warning threshold configured in the limit_warnings table. If a threshold is reached for the first time, enqueues a background job (e.g. email notification). Uses limit_warning_state for one-time dedup per warning/actor pair. Requires both a limits_module with limit_warnings enabled and a rate_limit_meters_module.', + '{"type":"object","properties":{"meter_slug":{"type":"string","description":"Slug of the billing meter to check rate limits against (must match a meters table entry)"},"scope":{"type":"string","description":"Membership type prefix that determines which limits_module row to use for warnings and warning_state tables. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \"app\", \"org\", \"data_room\", \"channel\", \"team\").","default":"app"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for rate limit lookup. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]},"actor_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the actor id for rate limit lookup","default":"owner_id"}},"required":["meter_slug"]}'::jsonb, + '{"rate-limits","triggers","warning","notifications","metering"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'ProcessChunks', + 'data_chunks', + 'process', + 'Chunks', + 'Creates a chunked-embedding child table for any parent table. Provisions the chunks table with content, chunk_index, embedding vector, metadata, HNSW index, inherited RLS, and optional job trigger for automatic text splitting. Composed internally by ProcessFileEmbedding (enabled by default in extract mode) but can also be used standalone.', + '{"type":"object","properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"paragraph"},"dimensions":{"type":"integer","description":"Vector dimensions for per-chunk embeddings","default":768},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric for the HNSW index on chunk embeddings","default":"cosine"},"embedding_model":{"type":"string","description":"Embedding model identifier for per-chunk embeddings. When null, the worker falls back to runtime config (llm_module / env vars)."},"embedding_provider":{"type":"string","description":"Embedding provider name (e.g. \"ollama\", \"openai\"). When null, the worker falls back to runtime config."},"chunks_table_name":{"type":"string","description":"Override the chunks table name. Defaults to {parent_table}_chunks."},"metadata_fields":{"type":"array","items":{"type":"string"},"description":"Field names from the parent table to copy into chunk metadata"},"search_indexes":{"type":"array","items":{"type":"string","enum":["fulltext","bm25","trigram"]},"description":"Text search indexes to create on the chunks content column. Omit to mirror the parent table''s text search indexes. Set explicitly to override (e.g. [\"fulltext\", \"bm25\"])."},"entity_field":{"type":"string","format":"column-ref","description":"Column on the parent table that holds (or references) the entity_id for billing scope. Forwarded to the chunking job trigger."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Forwarded to the chunking job trigger.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from"},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, optional)"},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id"}},"required":["obj_table","obj_field"]},"enqueue_chunking_job":{"type":"boolean","description":"Whether to create a job trigger that auto-enqueues chunking on parent INSERT/UPDATE","default":true},"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"}}}'::jsonb, + '{"embedding","chunks","vector","ai","rag"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'ProcessExtraction', + 'process_extraction', + 'process', + 'File Extraction', + 'Creates extraction output fields and a job trigger for file text extraction. Fires when a file is uploaded (status = ''uploaded'') or on INSERT. The external worker extracts text/metadata from the file (PDF, DOCX, HTML, etc.) and writes the result back to the configured output fields. Typically used upstream of ProcessFileEmbedding or ProcessChunks.', + '{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"text_field":{"type":"string","format":"column-ref","description":"Field to store extracted text/markdown","default":"extracted_text"},"metadata_field":{"type":"string","format":"column-ref","description":"JSONB field for extraction metadata (page count, language, etc.)","default":"extracted_metadata"},"extraction_model":{"type":"string","description":"Extraction model identifier (e.g. a vision model for OCR, an LLM for structured extraction). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config."},"extraction_provider":{"type":"string","description":"Extraction provider name (e.g. \"ollama\", \"openai\"). When null, the worker falls back to runtime config."},"mime_patterns":{"type":"array","items":{"type":"string"},"description":"MIME type LIKE patterns to match. Multiple patterns are OR''d together. Examples: [''application/pdf'', ''text/%''], [''application/vnd.openxmlformats%''].","default":["application/pdf","text/%"]},"task_identifier":{"type":"string","description":"Job task identifier for the extraction worker","default":"extract_file_text"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Trigger events that fire the job","default":["INSERT"]},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Custom payload key-to-column mapping for the job trigger","default":{"file_id":"id","key":"key","mime_type":"mime_type","bucket_id":"bucket_id"}},"trigger_conditions":{"description":"Additional compound conditions beyond auto-generated filtering. Merged with the auto-generated conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"entity_field":{"type":"string","format":"column-ref","description":"Column on the trigger table that holds (or references) the entity_id for billing scope. Forwarded to the composed JobTrigger."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Forwarded to the composed JobTrigger.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from"},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, optional)"},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id"}},"required":["obj_table","obj_field"]},"queue_name":{"type":"string","description":"Job queue name for extraction tasks","default":"extraction"},"max_attempts":{"type":"integer","description":"Maximum number of retry attempts","default":5},"priority":{"type":"integer","description":"Job priority (lower = higher priority)","default":0}}}'::jsonb, + '{"extraction","files","processing","jobs","text"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'ProcessFileEmbedding', + 'data_file_embedding', + 'process', + 'File Embedding', + 'Generic, MIME-scoped embedding node for file tables. Supports two modes: direct (whole-file to single vector, e.g. CLIP for images) when extraction is omitted, or extract (file to text to chunks to per-chunk vectors) when extraction config is provided. Composes SearchVector + JobTrigger + ProcessChunks (enabled by default in extract mode) internally. Multiple instances can coexist on the same table with different MIME scopes, field names, and embedding strategies.', + '{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the vector embedding column","default":"embedding"},"dimensions":{"type":"integer","description":"Vector dimensions (e.g. 512 for CLIP, 768 for nomic, 1536 for ada-002)","default":768},"index_method":{"type":"string","enum":["hnsw","ivfflat"],"description":"Index type for similarity search","default":"hnsw"},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric","default":"cosine"},"index_options":{"type":"object","description":"Index-specific options. HNSW: {m, ef_construction}. IVFFlat: {lists}.","default":{}},"embedding_model":{"type":"string","description":"Embedding model identifier (e.g. \"nomic-embed-text\", \"text-embedding-3-small\", \"clip-vit-base-patch32\"). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config (llm_module / env vars)."},"embedding_provider":{"type":"string","description":"Embedding provider name (e.g. \"ollama\", \"openai\"). When null, the worker falls back to runtime config."},"mime_patterns":{"type":"array","items":{"type":"string"},"description":"MIME type LIKE patterns to match. Multiple patterns are OR''d together. Examples: [''image/%''], [''application/pdf'', ''text/%''], [''audio/%''].","default":["image/%"]},"task_identifier":{"type":"string","description":"Job task identifier for the worker. In direct mode this is the embedding worker; in extract mode this is the extraction worker.","default":"process_file_embedding"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Trigger events that fire the job","default":["INSERT"]},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Custom payload key-to-column mapping for the job trigger","default":{"file_id":"id","key":"key","mime_type":"mime_type","bucket_id":"bucket_id"}},"trigger_conditions":{"description":"Additional compound conditions beyond auto-generated filtering. Merged with the auto-generated conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"entity_field":{"type":"string","format":"column-ref","description":"Column on the trigger table that holds (or references) the entity_id for billing scope. Forwarded to the composed JobTrigger."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Forwarded to the composed JobTrigger.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from"},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, optional)"},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id"}},"required":["obj_table","obj_field"]},"extraction":{"type":"object","description":"Text extraction configuration. When present, the generator creates extraction output fields on the table and configures SearchVector with source_fields + stale tracking. When absent, the node operates in direct mode (single vector per file, no text extraction).","properties":{"text_field":{"type":"string","format":"column-ref","description":"Field to store extracted text/markdown","default":"extracted_text"},"metadata_field":{"type":"string","format":"column-ref","description":"JSONB field for extraction metadata (page count, language, etc.)","default":"extracted_metadata"}}},"include_chunks":{"type":"boolean","description":"Whether to create a chunks table via ProcessChunks. Defaults to true when extraction is provided, false in direct mode. Set explicitly to override."},"chunks":{"type":"object","description":"Chunking configuration passed through to ProcessChunks. When include_chunks is true (or defaults to true in extract mode), these params configure the chunks table, embedding dimensions, strategy, etc.","default":{},"properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"paragraph"},"metadata_fields":{"type":"array","items":{"type":"string"},"description":"Field names from parent to copy into chunk metadata"},"search_indexes":{"type":"array","items":{"type":"string","enum":["fulltext","bm25","trigram"]},"description":"Text search indexes to create on the chunks content column. Omit to mirror the parent table''s text search indexes. Set explicitly to override."},"enqueue_chunking_job":{"type":"boolean","description":"Whether to auto-enqueue a chunking job on insert/update","default":true},"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"}}}}}'::jsonb, + '{"embedding","vector","ai","composition","jobs","multimodal","files"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'ProcessImageEmbedding', + 'data_image_embedding', + 'process', + 'Image Embedding', + 'Image-specific preset of ProcessFileEmbedding. Delegates to ProcessFileEmbedding with image-oriented defaults: dimensions=512 (CLIP), mime_patterns=[''image/%''], task_identifier=''process_image_embedding'', direct mode (no extraction). Accepts all ProcessFileEmbedding parameters — any overrides are forwarded through.', + '{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the vector embedding column","default":"embedding"},"dimensions":{"type":"integer","description":"Vector dimensions (default 512 for CLIP-style image embeddings)","default":512},"index_method":{"type":"string","enum":["hnsw","ivfflat"],"description":"Index type for similarity search","default":"hnsw"},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric","default":"cosine"},"index_options":{"type":"object","description":"Index-specific options. HNSW: {m, ef_construction}. IVFFlat: {lists}.","default":{}},"embedding_model":{"type":"string","description":"Embedding model identifier (e.g. \"clip-vit-base-patch32\"). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config (llm_module / env vars)."},"embedding_provider":{"type":"string","description":"Embedding provider name (e.g. \"ollama\", \"openai\"). When null, the worker falls back to runtime config."},"mime_patterns":{"type":"array","items":{"type":"string"},"description":"MIME type LIKE patterns to match. Multiple patterns are OR''d together.","default":["image/%"]},"task_identifier":{"type":"string","description":"Job task identifier for the image embedding worker","default":"process_image_embedding"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Trigger events that fire the job","default":["INSERT"]},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Custom payload key-to-column mapping for the job trigger","default":{"file_id":"id","key":"key","mime_type":"mime_type","bucket_id":"bucket_id"}},"trigger_conditions":{"description":"Additional compound conditions beyond auto-generated filtering. Merged with the auto-generated conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"entity_field":{"type":"string","format":"column-ref","description":"Column on the trigger table that holds (or references) the entity_id for billing scope. Forwarded to the composed JobTrigger."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Forwarded to the composed JobTrigger.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from"},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, optional)"},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id"}},"required":["obj_table","obj_field"]},"extraction":{"type":"object","description":"Text extraction configuration. Forwarded to ProcessFileEmbedding. When present, enables extract mode (e.g., OCR for images).","properties":{"text_field":{"type":"string","format":"column-ref","description":"Field to store extracted text","default":"extracted_text"},"metadata_field":{"type":"string","format":"column-ref","description":"JSONB field for extraction metadata","default":"extracted_metadata"}}},"chunks":{"type":"object","description":"Chunking configuration. Forwarded to ProcessFileEmbedding. Only meaningful when extraction is also provided.","properties":{"content_field_name":{"type":"string","format":"column-ref","default":"content"},"chunk_size":{"type":"integer","default":1000},"chunk_overlap":{"type":"integer","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"default":"paragraph"},"metadata_fields":{"type":"object"},"enqueue_chunking_job":{"type":"boolean","default":true},"chunking_task_name":{"type":"string","default":"generate_chunks"}}}}}'::jsonb, + '{"embedding","image","vector","ai","composition","jobs"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'ProcessImageVersions', + 'process_image_versions', + 'process', + 'Image Versions', + 'Creates a job trigger for image variant generation. Fires when an image file is uploaded (status = ''uploaded'') or on INSERT. The external worker generates resized, cropped, or reformatted versions (thumbnails, previews, WebP conversions, etc.) and stores them as new file records linked to the source image.', + '{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"required":["versions"],"properties":{"versions":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Version identifier (e.g., \"thumb\", \"preview\", \"hero\")"},"width":{"type":"integer","description":"Target width in pixels"},"height":{"type":"integer","description":"Target height in pixels"},"fit":{"type":"string","enum":["cover","contain","fill","inside","outside"],"description":"Resize fitting strategy","default":"cover"},"format":{"type":"string","enum":["jpeg","png","webp","avif"],"description":"Output image format","default":"webp"},"quality":{"type":"integer","description":"Output quality (1-100)","default":80}},"required":["name"]},"description":"Array of version definitions. Each version specifies dimensions, format, and quality for a generated image variant. Required — the blueprint must explicitly define what variants to generate.","minItems":1},"mime_patterns":{"type":"array","items":{"type":"string"},"description":"MIME type LIKE patterns to match. Defaults to all image types.","default":["image/%"]},"task_identifier":{"type":"string","description":"Job task identifier for the image processing worker","default":"process_image_versions"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Trigger events that fire the job","default":["INSERT"]},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Custom payload key-to-column mapping for the job trigger","default":{"file_id":"id","key":"key","mime_type":"mime_type","bucket_id":"bucket_id"}},"trigger_conditions":{"description":"Additional compound conditions beyond auto-generated filtering. Merged with the auto-generated conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"entity_field":{"type":"string","format":"column-ref","description":"Column on the trigger table that holds (or references) the entity_id for billing scope. Forwarded to the composed JobTrigger."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Forwarded to the composed JobTrigger.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from"},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, optional)"},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id"}},"required":["obj_table","obj_field"]},"queue_name":{"type":"string","description":"Job queue name for image processing tasks","default":"image_processing"},"max_attempts":{"type":"integer","description":"Maximum number of retry attempts","default":5},"priority":{"type":"integer","description":"Job priority (lower = higher priority)","default":0}}}'::jsonb, + '{"images","processing","jobs","resize","thumbnails","files"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'RelationBelongsTo', + 'relation_belongs_to', + 'relation', + 'Belongs To', + 'Creates a foreign key field on the source table referencing the target table. Auto-derives the FK field name from the target table name using inflection (e.g., projects derives project_id). delete_action is required and must be explicitly provided by the caller.', + '{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"Table that will have the FK field added"},"target_table_id":{"type":"string","format":"uuid","description":"Table being referenced by the FK"},"field_name":{"type":"string","format":"column-ref","description":"FK field name on the source table. Auto-derived from target table name if omitted (e.g., projects → project_id)"},"delete_action":{"type":"string","enum":["c","r","n","d","a"],"description":"FK delete action: c=CASCADE, r=RESTRICT, n=SET NULL, d=SET DEFAULT, a=NO ACTION. Required."},"is_required":{"type":"boolean","description":"Whether the FK field is NOT NULL","default":true}},"required":["source_table_id","target_table_id","delete_action"]}'::jsonb, + '{"relation","foreign_key","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'RelationHasMany', + 'relation_has_many', + 'relation', + 'Has Many', + 'Creates a foreign key field on the target table referencing the source table. Inverse of RelationBelongsTo — same FK, different perspective. "projects has many tasks" creates tasks.project_id. Auto-derives the FK field name from the source table name using inflection. delete_action is required and must be explicitly provided by the caller.', + '{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"Parent table being referenced by the FK (e.g., projects in projects has many tasks)"},"target_table_id":{"type":"string","format":"uuid","description":"Child table that receives the FK field (e.g., tasks in projects has many tasks)"},"field_name":{"type":"string","format":"column-ref","description":"FK field name on the target table. Auto-derived from source table name if omitted (e.g., projects derives project_id)"},"delete_action":{"type":"string","enum":["c","r","n","d","a"],"description":"FK delete action: c=CASCADE, r=RESTRICT, n=SET NULL, d=SET DEFAULT, a=NO ACTION. Required."},"is_required":{"type":"boolean","description":"Whether the FK field is NOT NULL","default":true}},"required":["source_table_id","target_table_id","delete_action"]}'::jsonb, + '{"relation","foreign_key","has_many","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'RelationHasOne', + 'relation_has_one', + 'relation', + 'Has One', + 'Creates a foreign key field with a unique constraint on the source table referencing the target table. Enforces 1:1 cardinality. Auto-derives the FK field name from the target table name using inflection. delete_action is required and must be explicitly provided by the caller.', + '{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"Table that will have the FK field and unique constraint"},"target_table_id":{"type":"string","format":"uuid","description":"Table being referenced by the FK"},"field_name":{"type":"string","format":"column-ref","description":"FK field name on the source table. Auto-derived from target table name if omitted (e.g., users → user_id)"},"delete_action":{"type":"string","enum":["c","r","n","d","a"],"description":"FK delete action: c=CASCADE, r=RESTRICT, n=SET NULL, d=SET DEFAULT, a=NO ACTION. Required."},"is_required":{"type":"boolean","description":"Whether the FK field is NOT NULL","default":true}},"required":["source_table_id","target_table_id","delete_action"]}'::jsonb, + '{"relation","foreign_key","unique","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'RelationManyToMany', + 'relation_many_to_many', + 'relation', + 'Many to Many', + 'Creates a junction table between source and target tables with auto-derived naming and FK fields. The trigger creates a bare table (no implicit DataId), adds FK fields to both tables, optionally creates a composite PK (use_composite_key), then forwards all security config to secure_table_provision as-is. The trigger never injects values the caller did not provide. Junction table FKs always CASCADE on delete.', + '{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"First table in the M:N relationship"},"target_table_id":{"type":"string","format":"uuid","description":"Second table in the M:N relationship"},"junction_table_id":{"type":"string","format":"uuid","description":"Existing junction table to use. If uuid_nil(), a new bare table is created"},"junction_table_name":{"type":"string","description":"Junction table name. Auto-derived from both table names if omitted (e.g., projects + tags derives project_tags)"},"source_field_name":{"type":"string","format":"column-ref","description":"FK field name on junction for source table. Auto-derived if omitted (e.g., projects derives project_id)"},"target_field_name":{"type":"string","format":"column-ref","description":"FK field name on junction for target table. Auto-derived if omitted (e.g., tags derives tag_id)"},"use_composite_key":{"type":"boolean","description":"When true, creates a composite PK from the two FK fields. When false, no PK is created by the trigger (use nodes with DataId for UUID PK). Mutually exclusive with nodes containing DataId.","default":false},"nodes":{"type":"array","items":{"type":"object"},"description":"Array of node objects for field creation on junction table. Each object has a $type key (e.g. DataId, DataEntityMembership) and optional data keys. Forwarded to secure_table_provision as-is. Empty array means no additional fields."},"grants":{"type":"array","items":{"type":"object","properties":{"roles":{"type":"array","items":{"type":"string"}},"privileges":{"type":"array","items":{"type":"array","items":{"type":"string"}}}},"required":["roles","privileges"]},"description":"Unified grant objects for the junction table. Each entry is { roles: string[], privileges: string[][] }. Forwarded to secure_table_provision as-is. Default: []"},"policies":{"type":"array","items":{"type":"object","properties":{"$type":{"type":"string"},"data":{"type":"object"},"privileges":{"type":"array","items":{"type":"string"}},"policy_role":{"type":"string"},"permissive":{"type":"boolean"},"policy_name":{"type":"string"}},"required":["$type"]},"description":"RLS policy objects for the junction table. Each entry has $type (Authz* generator), optional data, privileges, policy_role, permissive, policy_name. Forwarded to secure_table_provision as-is. Default: []"}},"required":["source_table_id","target_table_id"]}'::jsonb, + '{"relation","junction","many_to_many","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'RelationSpatial', + 'relation_spatial', + 'relation', + 'Spatial Relation', + 'Declares a spatial predicate between two existing geometry/geography columns. Inserts a metaschema_public.spatial_relation row; the sync_spatial_relation_tags trigger then projects a @spatialRelation smart tag onto the owner column so graphile-postgis'' PostgisSpatialRelationsPlugin can expose it as a cross-table filter in GraphQL. Metadata-only: both source_field and target_field must already exist on their tables. Idempotent on (source_table_id, name). One direction per tag — author two RelationSpatial entries if symmetry is desired.', + '{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"Table that owns the relation (the @spatialRelation tag is emitted on the owner column of this table)"},"source_field_id":{"type":"string","format":"uuid","description":"Geometry/geography column on source_table that carries the @spatialRelation smart tag"},"target_table_id":{"type":"string","format":"uuid","description":"Table being referenced by the spatial predicate"},"target_field_id":{"type":"string","format":"uuid","description":"Geometry/geography column on target_table that the predicate is evaluated against"},"name":{"type":"string","description":"Relation name (stable, snake_case). Becomes the generated filter field name in GraphQL (e.g. nearby_clinic). Unique per (source_table_id, name) — idempotency key."},"operator":{"type":"string","enum":["st_contains","st_within","st_intersects","st_covers","st_coveredby","st_overlaps","st_touches","st_dwithin"],"description":"PostGIS spatial predicate. One of the 8 whitelisted operators. st_dwithin requires param_name."},"param_name":{"type":"string","description":"Parameter name for parametric operators (currently only st_dwithin, which needs a distance argument). Must be NULL for all other operators. Enforced by table CHECK."}},"required":["source_table_id","source_field_id","target_table_id","target_field_id","name","operator"]}'::jsonb, + '{"relation","spatial","postgis","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'SearchBm25', + 'search_bm25', + 'search', + 'BM25 Search', + 'Creates a BM25 index on an existing text column using pg_textsearch. Enables statistical relevance ranking with configurable k1 and b parameters. The BM25 index is auto-detected by graphile-search.', + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of existing text column to index with BM25"},"text_config":{"type":"string","description":"PostgreSQL text search configuration for BM25","default":"english"},"k1":{"type":"number","description":"BM25 k1 parameter: term frequency saturation (typical: 1.2-2.0)","default":null},"b":{"type":"number","description":"BM25 b parameter: document length normalization (0=none, 1=full, typical: 0.75)","default":null},"search_score_weight":{"type":"number","description":"Weight for this algorithm in composite searchScore","default":1}},"required":["field_name"]}'::jsonb, + '{"search","bm25","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'SearchFullText', + 'search_full_text', + 'search', + 'Full-Text Search', + 'Adds a tsvector column with GIN index and automatic trigger population from source fields. Enables PostgreSQL full-text search with configurable weights and language support. Leverages the existing metaschema full_text_search infrastructure.', + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the tsvector column","default":"search"},"source_fields":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","format":"column-ref","description":"Name of the source column"},"weight":{"type":"string","enum":["A","B","C","D"],"description":"tsvector weight class (A=highest, D=lowest)","default":"D"},"lang":{"type":"string","description":"PostgreSQL text search configuration","default":"english"}},"required":["field"]},"description":"Source columns that feed the tsvector. Each has a field name, weight (A-D), and language config."},"lang_column":{"type":"string","format":"column-ref","description":"Column name whose value determines the text search configuration per row. When set, the tsvector trigger uses NEW.::regconfig instead of a static language, enabling dynamic per-row language stemming. The per-field lang values in source_fields are used as fallback defaults for the langs array but the trigger reads from this column at runtime."},"search_score_weight":{"type":"number","description":"Weight for this algorithm in composite searchScore","default":1}},"required":["source_fields"]}'::jsonb, + '{"search","fts","tsvector","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'SearchSpatial', + 'search_spatial', + 'search', + 'Spatial Search', + 'Adds a PostGIS geometry or geography column with a spatial index (GiST or SP-GiST). Supports configurable geometry types (Point, Polygon, etc.), SRID, and dimensionality. The graphile-postgis plugin auto-detects geometry/geography columns by codec type for spatial filtering (ST_Contains, ST_DWithin, bbox operators).', + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the geometry/geography column","default":"geom"},"geometry_type":{"type":"string","enum":["Point","LineString","Polygon","MultiPoint","MultiLineString","MultiPolygon","GeometryCollection","Geometry"],"description":"PostGIS geometry type constraint","default":"Point"},"srid":{"type":"integer","description":"Spatial Reference System Identifier (e.g. 4326 for WGS84)","default":4326},"dimension":{"type":"integer","enum":[2,3,4],"description":"Coordinate dimension (2=XY, 3=XYZ, 4=XYZM)","default":2},"use_geography":{"type":"boolean","description":"Use geography type instead of geometry (for geodetic calculations on the sphere)","default":false},"index_method":{"type":"string","enum":["gist","spgist"],"description":"Spatial index method","default":"gist"}}}'::jsonb, + '{"spatial","postgis","geometry","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'SearchSpatialAggregate', + 'search_spatial_aggregate', + 'search', + 'Spatial Aggregate Search', + 'Creates a derived/materialized geometry field on the parent table that automatically aggregates geometries from a source (child) table via triggers. When child rows are inserted/updated/deleted, the parent aggregate field is recalculated using the specified PostGIS aggregation function (ST_Union, ST_Collect, ST_ConvexHull, ST_ConcaveHull). Useful for materializing spatial boundaries from collections of points or polygons.', + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the aggregate geometry column on the parent table","default":"geom_aggregate"},"source_table_id":{"type":"string","format":"uuid","description":"UUID of the source (child) table containing individual geometries"},"source_geom_field":{"type":"string","format":"column-ref","description":"Name of the geometry column on the source table","default":"geom"},"source_fk_field":{"type":"string","format":"column-ref","description":"Name of the foreign key column on the source table pointing to the parent"},"aggregate_function":{"type":"string","enum":["union","collect","convex_hull","concave_hull"],"description":"PostGIS aggregation function: union (ST_Union, merges overlapping), collect (ST_Collect, groups without merging), convex_hull (smallest convex polygon), concave_hull (tighter boundary)","default":"union"},"geometry_type":{"type":"string","enum":["Point","LineString","Polygon","MultiPoint","MultiLineString","MultiPolygon","GeometryCollection","Geometry"],"description":"Output geometry type constraint for the aggregate field","default":"MultiPolygon"},"srid":{"type":"integer","description":"Spatial Reference System Identifier (e.g. 4326 for WGS84)","default":4326},"dimension":{"type":"integer","enum":[2,3,4],"description":"Coordinate dimension (2=XY, 3=XYZ, 4=XYZM)","default":2},"use_geography":{"type":"boolean","description":"Use geography type instead of geometry","default":false},"index_method":{"type":"string","enum":["gist","spgist"],"description":"Spatial index method for the aggregate field","default":"gist"}},"required":["source_table_id","source_fk_field"]}'::jsonb, + '{"spatial","postgis","geometry","aggregate","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'SearchTrgm', + 'search_trgm', + 'search', + 'Trigram Search', + 'Creates GIN trigram indexes (gin_trgm_ops) on specified text/citext fields for fuzzy LIKE/ILIKE/similarity search. Adds @trgmSearch smart tag for PostGraphile integration. Fields must already exist on the table.', + '{"type":"object","properties":{"fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names to create trigram indexes on (fields must already exist on the table)"}},"required":["fields"]}'::jsonb, + '{"search","trigram","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'SearchUnified', + 'search_unified', + 'search', + 'Unified Search', + 'Composite node type that orchestrates multiple search modalities (full-text search, BM25, embeddings, trigram) on a single table. Configures per-table search score weights, normalization strategy, and recency boost via the @searchConfig smart tag.', + '{"type":"object","properties":{"full_text_search":{"type":"object","description":"SearchFullText parameters. Omit to skip FTS setup.","properties":{"field_name":{"type":"string","format":"column-ref","default":"search"},"source_fields":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","format":"column-ref"},"weight":{"type":"string","enum":["A","B","C","D"]},"lang":{"type":"string"}},"required":["field"]}},"search_score_weight":{"type":"number","default":1}}},"bm25":{"type":"object","description":"SearchBm25 parameters. Omit to skip BM25 setup.","properties":{"field_name":{"type":"string","format":"column-ref"},"text_config":{"type":"string","default":"english"},"k1":{"type":"number"},"b":{"type":"number"},"search_score_weight":{"type":"number","default":1}}},"embedding":{"type":"object","description":"SearchVector parameters. Omit to skip embedding setup.","properties":{"field_name":{"type":"string","format":"column-ref","default":"embedding"},"dimensions":{"type":"integer","default":768},"index_method":{"type":"string","enum":["hnsw","ivfflat"]},"metric":{"type":"string","enum":["cosine","l2","ip"]},"source_fields":{"type":"array","items":{"type":"string","format":"column-ref"}},"embedding_model":{"type":"string","description":"Embedding model identifier. When null, the worker falls back to runtime config."},"embedding_provider":{"type":"string","description":"Embedding provider name. When null, the worker falls back to runtime config."},"search_score_weight":{"type":"number","default":1},"chunks":{"type":"object","description":"Chunking configuration for long-text embedding. Creates an embedding_chunks record that drives automatic text splitting and per-chunk embedding. Omit to skip chunking.","properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"fixed"},"metadata_fields":{"type":"object","description":"Metadata fields from parent to copy into chunks"},"enqueue_chunking_job":{"type":"boolean","description":"Whether to auto-enqueue a chunking job on insert/update","default":true},"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"}}}}},"embedding_text_field":{"type":"string","format":"column-ref","description":"Name of the composite text field created for embedding input","default":"embedding_text"},"composite_format":{"type":"string","enum":["labeled","plain"],"description":"Output format for the composite text field","default":"labeled"},"trgm_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names to tag with @trgmSearch for fuzzy/typo-tolerant matching"},"search_config":{"type":"object","description":"Unified search score configuration written to @searchConfig smart tag","properties":{"weights":{"type":"object","description":"Per-algorithm weights: {tsv: 1.5, bm25: 1.0, pgvector: 0.8, trgm: 0.3}"},"normalization":{"type":"string","enum":["linear","sigmoid"],"description":"Score normalization strategy","default":"linear"},"boost_recent":{"type":"boolean","description":"Enable recency boost for search results","default":false},"boost_recency_field":{"type":"string","format":"column-ref","description":"Timestamp field for recency boost (e.g. created_at, updated_at)"},"boost_recency_decay":{"type":"number","description":"Decay rate for recency boost (0-1, lower = faster decay)","default":0.5}}}}}'::jsonb, + '{"search","composite","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'SearchVector', + 'search_vector', + 'search', + 'Vector Search', + 'Adds a vector embedding column with HNSW or IVFFlat index for similarity search. Supports configurable dimensions, distance metrics (cosine, l2, ip), per-field {field_name}_updated_at timestamp tracking (read-only in GraphQL), and automatic job enqueue triggers for embedding generation.', + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the vector column","default":"embedding"},"dimensions":{"type":"integer","description":"Vector dimensions (e.g. 384, 768, 1536, 3072)","default":768},"index_method":{"type":"string","enum":["hnsw","ivfflat"],"description":"Index type for similarity search","default":"hnsw"},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric (cosine, l2, ip)","default":"cosine"},"index_options":{"type":"object","description":"Index-specific options. HNSW: {m, ef_construction}. IVFFlat: {lists}.","default":{}},"source_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Column names that feed the embedding. Used by stale trigger to detect content changes."},"embedding_model":{"type":"string","description":"Embedding model identifier (e.g. \"nomic-embed-text\", \"text-embedding-3-small\"). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config (llm_module / env vars)."},"embedding_provider":{"type":"string","description":"Embedding provider name (e.g. \"ollama\", \"openai\"). When null, the worker falls back to runtime config."},"enqueue_job":{"type":"boolean","description":"Auto-create trigger that enqueues embedding generation jobs","default":true},"job_task_name":{"type":"string","format":"function-ref","description":"Task identifier for the job queue. Must match a registered function definition when function_module is installed.","default":"generate_embedding"},"chunks":{"type":"object","description":"Chunking configuration for long-text embedding. Creates an embedding_chunks record that drives automatic text splitting and per-chunk embedding. Omit to skip chunking.","properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"fixed"},"metadata_fields":{"type":"object","description":"Metadata fields from parent to copy into chunks"},"enqueue_chunking_job":{"type":"boolean","description":"Whether to auto-enqueue a chunking job on insert/update","default":true},"chunking_task_name":{"type":"string","format":"function-ref","description":"Task identifier for the chunking job queue. Must match a registered function definition when function_module is installed.","default":"generate_chunks"}}}}}'::jsonb, + '{"embedding","vector","ai","schema"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'ViewAggregated', + 'view_aggregated', + 'view', + 'Aggregated View', + 'View with GROUP BY and aggregate functions. Useful for summary/reporting views.', + '{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"UUID of the source table"},"group_by_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names to group by"},"aggregates":{"type":"array","items":{"type":"object","properties":{"function":{"type":"string","enum":["COUNT","SUM","AVG","MIN","MAX"]},"field":{"type":"string","format":"column-ref","description":"Field to aggregate (or * for COUNT)"},"alias":{"type":"string","format":"column-ref","description":"Output column name"}},"required":["function","alias"]},"description":"Array of aggregate specifications"}},"required":["source_table_id","group_by_fields","aggregates"]}'::jsonb, + '{"view","aggregate","reporting"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'ViewComposite', + 'view_composite', + 'view', + 'Composite View', + 'Advanced view using composite AST for the query. Use when other node types are insufficient (CTEs, UNIONs, complex subqueries, etc.).', + '{"type":"object","properties":{"query_ast":{"type":"object","description":"Composite SELECT query AST (JSONB)"}},"required":["query_ast"]}'::jsonb, + '{"view","advanced","composite","ast"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'ViewFilteredTable', + 'view_filtered_table', + 'view', + 'Filtered Table', + 'Table projection with an Authz* filter baked into the view definition. The view only returns records matching the filter.', + '{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"UUID of the source table"},"filter_type":{"type":"string","description":"Authz* node type name (e.g., AuthzDirectOwner, AuthzPublishable)"},"filter_data":{"type":"object","description":"Parameters for the Authz* filter type"},"field_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Optional array of field UUIDs to include (alternative to field_names)"},"field_names":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Optional array of field names to include (alternative to field_ids)"}},"required":["source_table_id","filter_type"]}'::jsonb, + '{"view","filter","authz"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'ViewJoinedTables', + 'view_joined_tables', + 'view', + 'Joined Tables', + 'View that joins multiple tables together. Supports INNER, LEFT, RIGHT, and FULL joins.', + '{"type":"object","properties":{"primary_table_id":{"type":"string","format":"uuid","description":"UUID of the primary (left-most) table"},"primary_columns":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Optional array of column names to include from the primary table"},"joins":{"type":"array","items":{"type":"object","properties":{"table_id":{"type":"string","format":"uuid","description":"UUID of the joined table"},"join_type":{"type":"string","enum":["INNER","LEFT","RIGHT","FULL"]},"primary_field":{"type":"string","format":"column-ref","description":"Field on primary table"},"join_field":{"type":"string","format":"column-ref","description":"Field on joined table"},"columns":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Optional column names to include from this joined table"}},"required":["table_id","primary_field","join_field"]},"description":"Array of join specifications"},"field_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Optional array of field UUIDs to include (alternative to per-table columns)"}},"required":["primary_table_id","joins"]}'::jsonb, + '{"view","join"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'ViewTableProjection', + 'view_table_projection', + 'view', + 'Table Projection', + 'Simple column selection from a single source table. Projects all or specific fields.', + '{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"UUID of the source table to project from"},"field_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Optional array of field UUIDs to include (all fields if omitted)"},"field_names":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Optional array of field names to include (alternative to field_ids)"}},"required":["source_table_id"]}'::jsonb, + '{"view","projection"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/node_type_registry/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/node_type_registry/table.sql new file mode 100644 index 00000000..e2cd74e5 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/node_type_registry/table.sql @@ -0,0 +1,20 @@ +-- Deploy schemas/metaschema_public/tables/node_type_registry/table to pg + +-- requires: schemas/metaschema_public/schema + +BEGIN; + +CREATE TABLE metaschema_public.node_type_registry ( + name text PRIMARY KEY, + slug text NOT NULL UNIQUE, + category text NOT NULL, + display_name text, + description text, + parameter_schema jsonb NOT NULL DEFAULT '{}'::jsonb, + tags text[] NOT NULL DEFAULT '{}'::text[] +); + +CREATE INDEX node_type_registry_category_idx + ON metaschema_public.node_type_registry (category); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/partition/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/partition/table.sql new file mode 100644 index 00000000..6385452e --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/partition/table.sql @@ -0,0 +1,47 @@ +-- Deploy schemas/metaschema_public/tables/partition/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/database/table +-- requires: schemas/metaschema_public/tables/table/table +-- requires: schemas/metaschema_public/tables/field/table + +BEGIN; + +CREATE TABLE metaschema_public.partition ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + database_id uuid NOT NULL, + table_id uuid NOT NULL, + strategy text NOT NULL CHECK (strategy IN ('range', 'list', 'hash')), + partition_key_id uuid NOT NULL, + "interval" text, + retention text, + retention_keep_table boolean NOT NULL DEFAULT true, + premake int NOT NULL DEFAULT 2, + naming_pattern text NOT NULL DEFAULT '{parent}_{bounds}', + is_parented boolean NOT NULL DEFAULT false, + + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + + CONSTRAINT partition_database_fkey + FOREIGN KEY (database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + + CONSTRAINT partition_table_fkey + FOREIGN KEY (table_id) + REFERENCES metaschema_public.table (id) + ON DELETE CASCADE, + + CONSTRAINT partition_key_field_fkey + FOREIGN KEY (partition_key_id) + REFERENCES metaschema_public.field (id), + + CONSTRAINT partition_table_unique + UNIQUE (table_id) +); + +CREATE INDEX partition_database_id_idx + ON metaschema_public.partition (database_id); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/policy/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/policy/table.sql new file mode 100644 index 00000000..4feb8e22 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/policy/table.sql @@ -0,0 +1,49 @@ +-- Deploy schemas/metaschema_public/tables/policy/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/table/table +-- requires: schemas/metaschema_public/types/object_category + +BEGIN; + +CREATE TABLE metaschema_public.policy ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + + table_id uuid NOT NULL, + name text, + grantee_name text, + privilege text, + + -- using_expression text, + -- check_expression text, + -- policy_text text, + + permissive boolean default true, + disabled boolean default false, + + policy_type text, + data jsonb, + + smart_tags jsonb, + + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + + tags citext[] NOT NULL DEFAULT '{}', + + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + UNIQUE (table_id, name) +); + + +CREATE INDEX policy_table_id_idx ON metaschema_public.policy ( table_id ); +CREATE INDEX policy_database_id_idx ON metaschema_public.policy ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/primary_key_constraint/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/primary_key_constraint/table.sql new file mode 100644 index 00000000..07d88be5 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/primary_key_constraint/table.sql @@ -0,0 +1,39 @@ +-- Deploy schemas/metaschema_public/tables/primary_key_constraint/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/types/object_category + +BEGIN; + +CREATE TABLE metaschema_public.primary_key_constraint ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + + table_id uuid NOT NULL, + name text, + type text, + field_ids uuid[] NOT NULL, + + smart_tags jsonb, + + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + + tags citext[] NOT NULL DEFAULT '{}', + + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + UNIQUE(table_id, name), + CHECK (field_ids <> '{}') +); + + +CREATE INDEX primary_key_constraint_table_id_idx ON metaschema_public.primary_key_constraint ( table_id ); +CREATE INDEX primary_key_constraint_database_id_idx ON metaschema_public.primary_key_constraint ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/schema/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/schema/table.sql new file mode 100644 index 00000000..24ded8cc --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/schema/table.sql @@ -0,0 +1,45 @@ +-- Deploy schemas/metaschema_public/tables/schema/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/database/table +-- requires: schemas/metaschema_public/types/object_category + +BEGIN; + +CREATE TABLE metaschema_public.schema ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + + database_id uuid NOT NULL, + name text NOT NULL, + schema_name text NOT NULL, + label text, + description text, + + smart_tags jsonb, + + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + + tags citext[] NOT NULL DEFAULT '{}', + + is_public boolean NOT NULL DEFAULT TRUE, + + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + + UNIQUE (database_id, name), + UNIQUE (schema_name) +); + +-- TODO: build out services +-- COMMENT ON COLUMN metaschema_public.schema.schema_name IS '@omit'; + +ALTER TABLE metaschema_public.schema + ADD CONSTRAINT schema_namechk CHECK (char_length(name) > 2); + +CREATE INDEX schema_database_id_idx ON metaschema_public.schema ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/schema_grant/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/schema_grant/table.sql new file mode 100644 index 00000000..1bc79edf --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/schema_grant/table.sql @@ -0,0 +1,28 @@ +-- Deploy schemas/metaschema_public/tables/schema_grant/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/schema/table + +BEGIN; + +CREATE TABLE metaschema_public.schema_grant ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + + schema_id uuid NOT NULL, + grantee_name text NOT NULL, + + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + + -- + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE + +); + + +CREATE INDEX schema_grant_schema_id_idx ON metaschema_public.schema_grant ( schema_id ); +CREATE INDEX schema_grant_database_id_idx ON metaschema_public.schema_grant ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/spatial_relation/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/spatial_relation/table.sql new file mode 100644 index 00000000..0a559cbc --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/spatial_relation/table.sql @@ -0,0 +1,68 @@ +-- Deploy schemas/metaschema_public/tables/spatial_relation/table to pg + +-- requires: schemas/metaschema_public/tables/field/table +-- requires: schemas/metaschema_public/tables/table/table +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/types/object_category + +BEGIN; + +CREATE TABLE metaschema_public.spatial_relation ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + + -- owner side + table_id uuid NOT NULL, + field_id uuid NOT NULL, + + -- target side + ref_table_id uuid NOT NULL, + ref_field_id uuid NOT NULL, + + -- relation shape + name text NOT NULL, + operator text NOT NULL, + param_name text NULL, + + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + + tags citext[] NOT NULL DEFAULT '{}', + + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT field_fkey FOREIGN KEY (field_id) REFERENCES metaschema_public.field (id) ON DELETE CASCADE, + CONSTRAINT ref_table_fkey FOREIGN KEY (ref_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT ref_field_fkey FOREIGN KEY (ref_field_id) REFERENCES metaschema_public.field (id) ON DELETE CASCADE, + + UNIQUE (table_id, name), + + CHECK (operator IN ( + 'st_contains', + 'st_within', + 'st_covers', + 'st_coveredby', + 'st_intersects', + 'st_equals', + 'st_bbox_intersects', + 'st_dwithin' + )), + + CHECK ( + (operator = 'st_dwithin' AND param_name IS NOT NULL) + OR + (operator <> 'st_dwithin' AND param_name IS NULL) + ) +); + +CREATE INDEX spatial_relation_table_id_idx ON metaschema_public.spatial_relation ( table_id ); +CREATE INDEX spatial_relation_field_id_idx ON metaschema_public.spatial_relation ( field_id ); +CREATE INDEX spatial_relation_database_id_idx ON metaschema_public.spatial_relation ( database_id ); +CREATE INDEX spatial_relation_ref_table_id_idx ON metaschema_public.spatial_relation ( ref_table_id ); +CREATE INDEX spatial_relation_ref_field_id_idx ON metaschema_public.spatial_relation ( ref_field_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx.sql new file mode 100644 index 00000000..c981dec4 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx.sql @@ -0,0 +1,20 @@ +-- Deploy schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx to pg +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_private/schema +-- requires: schemas/metaschema_public/tables/table/table + +BEGIN; + +CREATE FUNCTION metaschema_private.table_name_hash (name text) + RETURNS bytea + AS $BODY$ + SELECT + DECODE(MD5(LOWER(inflection.plural (name))), 'hex'); +$BODY$ +LANGUAGE sql +IMMUTABLE; + +CREATE UNIQUE INDEX databases_table_unique_name_idx ON metaschema_public.table (database_id, schema_id, metaschema_private.table_name_hash (name)); + +COMMIT; + diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/table/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/table/table.sql new file mode 100644 index 00000000..d9db4e9c --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/table/table.sql @@ -0,0 +1,58 @@ +-- Deploy schemas/metaschema_public/tables/table/table to pg +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/database/table +-- requires: schemas/metaschema_public/tables/schema/table +-- requires: schemas/metaschema_public/types/object_category + +BEGIN; + +CREATE TABLE metaschema_public.table ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + + schema_id uuid NOT NULL, + + name text NOT NULL, + + label text, + description text, + + smart_tags jsonb, + + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + + use_rls boolean NOT NULL DEFAULT FALSE, + + timestamps boolean NOT NULL DEFAULT FALSE, + peoplestamps boolean NOT NULL DEFAULT FALSE, + + plural_name text, + singular_name text, + + tags citext[] NOT NULL DEFAULT '{}', + + partitioned boolean NOT NULL DEFAULT false, + partition_strategy text DEFAULT NULL, + partition_key_names text[] DEFAULT NULL, + partition_key_types text[] DEFAULT NULL, + + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + + UNIQUE (database_id, schema_id, name) +); + +ALTER TABLE metaschema_public.table ADD COLUMN + inherits_id uuid NULL REFERENCES metaschema_public.table(id); + + +CREATE INDEX table_schema_id_idx ON metaschema_public.table ( schema_id ); +CREATE INDEX table_database_id_idx ON metaschema_public.table ( database_id ); + +COMMIT; + diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/table_grant/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/table_grant/table.sql new file mode 100644 index 00000000..04858394 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/table_grant/table.sql @@ -0,0 +1,39 @@ +-- Deploy schemas/metaschema_public/tables/table_grant/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/table/table + +BEGIN; + +CREATE TABLE metaschema_public.table_grant ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + + table_id uuid NOT NULL, + privilege text NOT NULL, + grantee_name text NOT NULL, + field_ids uuid[], + + -- true = GRANT, false = REVOKE + is_grant boolean NOT NULL DEFAULT true, + + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + + -- + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + + +CREATE INDEX table_grant_table_id_idx ON metaschema_public.table_grant ( table_id ); +CREATE INDEX table_grant_database_id_idx ON metaschema_public.table_grant ( database_id ); + +CREATE UNIQUE INDEX table_grant_unique_idx ON metaschema_public.table_grant ( + table_id, + privilege, + grantee_name, + COALESCE(field_ids, '{}'::uuid[]) +); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/trigger/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/trigger/table.sql new file mode 100644 index 00000000..c4ffc522 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/trigger/table.sql @@ -0,0 +1,41 @@ +-- Deploy schemas/metaschema_public/tables/trigger/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/table/table +-- requires: schemas/metaschema_public/types/object_category + +BEGIN; + +-- https://www.postgresql.org/docs/12/sql-createtrigger.html + +CREATE TABLE metaschema_public.trigger ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + + table_id uuid NOT NULL, + name text NOT NULL, + event text, -- INSERT, UPDATE, DELETE, or TRUNCATE + function_name text, + + smart_tags jsonb, + + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + + tags citext[] NOT NULL DEFAULT '{}', + + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + UNIQUE(table_id, name) +); + + +CREATE INDEX trigger_table_id_idx ON metaschema_public.trigger ( table_id ); +CREATE INDEX trigger_database_id_idx ON metaschema_public.trigger ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/trigger_function/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/trigger_function/table.sql new file mode 100644 index 00000000..161e09fd --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/trigger_function/table.sql @@ -0,0 +1,25 @@ +-- Deploy schemas/metaschema_public/tables/trigger_function/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/database/table + +BEGIN; + +CREATE TABLE metaschema_public.trigger_function ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + name text NOT NULL, + code text, + + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + + -- + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + UNIQUE (database_id, name) +); + +CREATE INDEX trigger_function_database_id_idx ON metaschema_public.trigger_function ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/unique_constraint/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/unique_constraint/table.sql new file mode 100644 index 00000000..01be08e4 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/unique_constraint/table.sql @@ -0,0 +1,44 @@ +-- Deploy schemas/metaschema_public/tables/unique_constraint/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/database/table +-- requires: schemas/metaschema_public/tables/table/table +-- requires: schemas/metaschema_public/types/object_category + +BEGIN; + +CREATE TABLE metaschema_public.unique_constraint ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + + table_id uuid NOT NULL, + name text, + description text, + smart_tags jsonb, + type text, + field_ids uuid[] NOT NULL, + + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + + tags citext[] NOT NULL DEFAULT '{}', + + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + -- TODO these are unique across schema, NOT table. We'll need to update this to have database_id + -- for portability + + UNIQUE (table_id, name), + CHECK (field_ids <> '{}') +); + + +CREATE INDEX unique_constraint_table_id_idx ON metaschema_public.unique_constraint ( table_id ); +CREATE INDEX unique_constraint_database_id_idx ON metaschema_public.unique_constraint ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/view/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/view/table.sql new file mode 100644 index 00000000..4fc9b6be --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/view/table.sql @@ -0,0 +1,56 @@ +-- Deploy schemas/metaschema_public/tables/view/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/schema/table +-- requires: schemas/metaschema_public/tables/table/table +-- requires: schemas/metaschema_public/tables/database/table +-- requires: schemas/metaschema_public/types/object_category + +BEGIN; + +CREATE TABLE metaschema_public.view ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + + schema_id uuid NOT NULL, + name text NOT NULL, + + -- Primary/source table for the view (nullable for ViewComposite) + -- For ViewTableProjection, ViewFilteredTable, ViewAggregated: the source table + -- For ViewJoinedTables: the primary (left-most) table + -- For ViewComposite: NULL (no table reference) + table_id uuid, + + -- View query definition using View* node types + view_type text NOT NULL, + data jsonb DEFAULT '{}', + + -- Optional filter using Authz* node types (baked into view WHERE clause) + filter_type text, + filter_data jsonb DEFAULT '{}', + + -- View options + security_invoker boolean DEFAULT true, + is_read_only boolean DEFAULT true, + + smart_tags jsonb, + + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + + tags citext[] NOT NULL DEFAULT '{}', + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + UNIQUE (schema_id, name) +); + + +CREATE INDEX view_schema_id_idx ON metaschema_public.view ( schema_id ); +CREATE INDEX view_database_id_idx ON metaschema_public.view ( database_id ); +CREATE INDEX view_table_id_idx ON metaschema_public.view ( table_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/view_grant/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/view_grant/table.sql new file mode 100644 index 00000000..a87f7e25 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/view_grant/table.sql @@ -0,0 +1,32 @@ +-- Deploy schemas/metaschema_public/tables/view_grant/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/view/table +-- requires: schemas/metaschema_public/tables/database/table + +BEGIN; + +CREATE TABLE metaschema_public.view_grant ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + + view_id uuid NOT NULL, + grantee_name text NOT NULL, + privilege text NOT NULL, + + with_grant_option boolean DEFAULT false, + + -- true = GRANT, false = REVOKE + is_grant boolean NOT NULL DEFAULT true, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT view_fkey FOREIGN KEY (view_id) REFERENCES metaschema_public.view (id) ON DELETE CASCADE, + + UNIQUE (view_id, grantee_name, privilege, is_grant) +); + + +CREATE INDEX view_grant_view_id_idx ON metaschema_public.view_grant ( view_id ); +CREATE INDEX view_grant_database_id_idx ON metaschema_public.view_grant ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/view_rule/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/view_rule/table.sql new file mode 100644 index 00000000..116203ea --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/view_rule/table.sql @@ -0,0 +1,32 @@ +-- Deploy schemas/metaschema_public/tables/view_rule/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/view/table +-- requires: schemas/metaschema_public/tables/database/table + +BEGIN; + +CREATE TABLE metaschema_public.view_rule ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + + view_id uuid NOT NULL, + name text NOT NULL, + event text NOT NULL, + action text NOT NULL DEFAULT 'NOTHING', + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT view_fkey FOREIGN KEY (view_id) REFERENCES metaschema_public.view (id) ON DELETE CASCADE, + + UNIQUE (view_id, name) +); + +COMMENT ON TABLE metaschema_public.view_rule IS 'DO INSTEAD rules for views (e.g., read-only enforcement)'; +COMMENT ON COLUMN metaschema_public.view_rule.event IS 'INSERT, UPDATE, or DELETE'; +COMMENT ON COLUMN metaschema_public.view_rule.action IS 'NOTHING (for read-only) or custom action'; + + +CREATE INDEX view_rule_view_id_idx ON metaschema_public.view_rule ( view_id ); +CREATE INDEX view_rule_database_id_idx ON metaschema_public.view_rule ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/view_table/table.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/view_table/table.sql new file mode 100644 index 00000000..6992e061 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/tables/view_table/table.sql @@ -0,0 +1,31 @@ +-- Deploy schemas/metaschema_public/tables/view_table/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/view/table +-- requires: schemas/metaschema_public/tables/table/table + +BEGIN; + +-- Junction table linking views to their joined tables (for ViewJoinedTables) +-- This provides referential integrity for views that reference multiple tables. +-- The primary table is stored in view.table_id; this table stores additional joined tables. +CREATE TABLE metaschema_public.view_table ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + view_id uuid NOT NULL, + table_id uuid NOT NULL, + + -- Order of joins (0 = first join, 1 = second join, etc.) + join_order int NOT NULL DEFAULT 0, + + CONSTRAINT view_fkey FOREIGN KEY (view_id) REFERENCES metaschema_public.view (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + UNIQUE (view_id, table_id) +); + +COMMENT ON TABLE metaschema_public.view_table IS 'Junction table linking views to their joined tables for referential integrity'; + +CREATE INDEX view_table_view_id_idx ON metaschema_public.view_table ( view_id ); +CREATE INDEX view_table_table_id_idx ON metaschema_public.view_table ( table_id ); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/types/object_category.sql b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/types/object_category.sql new file mode 100644 index 00000000..9480d9f5 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/deploy/schemas/metaschema_public/types/object_category.sql @@ -0,0 +1,13 @@ +-- Deploy schemas/metaschema_public/types/object_category to pg + +-- requires: schemas/metaschema_public/schema + +BEGIN; + +-- Unified category type for all metaschema objects (tables, fields, procedures, triggers, indexes, policies, constraints, etc.) +-- 'core' - system-level objects (id fields, entity_id, actor_id, etc.) +-- 'module' - objects created by modules (users, permissions, memberships, etc.) +-- 'app' - user-defined application objects +CREATE TYPE metaschema_public.object_category AS ENUM ('core', 'module', 'app'); + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/metaschema-schema.control b/extensions/@pgpm/metaschema-schema/metaschema-schema.control new file mode 100644 index 00000000..54954e70 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/metaschema-schema.control @@ -0,0 +1,7 @@ +# metaschema-schema extension +comment = 'metaschema-schema extension' +default_version = '0.26.3' +module_pathname = '$libdir/metaschema-schema' +requires = 'citext,hstore,pgpm-inflection,pgpm-database-jobs,pgpm-types,pgcrypto,plpgsql,postgis,uuid-ossp,pgpm-verify' +relocatable = false +superuser = false diff --git a/extensions/@pgpm/metaschema-schema/package.json b/extensions/@pgpm/metaschema-schema/package.json new file mode 100644 index 00000000..1148c3ce --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/package.json @@ -0,0 +1,41 @@ +{ + "name": "@pgpm/metaschema-schema", + "version": "0.26.5", + "description": "Database metadata utilities and introspection functions", + "author": "Dan Lynch ", + "contributors": [ + "Constructive " + ], + "keywords": [ + "postgresql", + "pgpm", + "metadata", + "introspection" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "bundle": "pgpm package", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@pgpm/database-jobs": "0.26.5", + "@pgpm/inflection": "0.26.0", + "@pgpm/types": "0.26.0", + "@pgpm/verify": "0.26.0" + }, + "devDependencies": { + "pgpm": "^4.23.2" + }, + "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" + }, + "gitHead": "a496a00d89c37d874f4a7207265b9972b6f05c7d" +} diff --git a/extensions/@pgpm/metaschema-schema/pgpm.plan b/extensions/@pgpm/metaschema-schema/pgpm.plan new file mode 100644 index 00000000..3f6c02ea --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/pgpm.plan @@ -0,0 +1,39 @@ +%syntax-version=1.0.0 +%project=metaschema-schema +%uri=metaschema-schema + +schemas/metaschema_private/schema [pgpm-inflection:schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx pgpm-database-jobs:schemas/app_jobs/triggers/tg_add_job_with_row pgpm-types:schemas/public/domains/url] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_private/schema +schemas/metaschema_public/schema 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/schema +schemas/metaschema_public/types/object_category [schemas/metaschema_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/types/object_category +schemas/metaschema_public/tables/database/table [schemas/metaschema_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/database/table +schemas/metaschema_public/tables/schema/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/types/object_category] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/schema/table +schemas/metaschema_public/tables/table/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/schema/table schemas/metaschema_public/types/object_category] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/table/table +schemas/metaschema_public/tables/check_constraint/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/table/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/check_constraint/table +schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx [schemas/metaschema_private/schema schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx +schemas/metaschema_public/tables/field/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/table/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/field/table +schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx [schemas/metaschema_public/schema schemas/metaschema_public/tables/field/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx +schemas/metaschema_public/tables/foreign_key_constraint/table [schemas/metaschema_public/tables/field/table schemas/metaschema_public/tables/table/table schemas/metaschema_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/foreign_key_constraint/table +schemas/metaschema_public/tables/full_text_search/table [schemas/metaschema_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/full_text_search/table +schemas/metaschema_public/tables/index/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/table/table schemas/metaschema_public/tables/database/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/index/table +schemas/metaschema_public/tables/policy/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/table/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/policy/table +schemas/metaschema_public/tables/primary_key_constraint/table [schemas/metaschema_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/primary_key_constraint/table +schemas/metaschema_public/tables/schema_grant/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/schema/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/schema_grant/table +schemas/metaschema_public/tables/table_grant/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/table/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/table_grant/table +schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx [schemas/metaschema_public/schema schemas/metaschema_private/schema schemas/metaschema_public/tables/table/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx +schemas/metaschema_public/tables/trigger_function/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/trigger_function/table +schemas/metaschema_public/tables/trigger/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/table/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/trigger/table +schemas/metaschema_public/tables/unique_constraint/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/table/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/unique_constraint/table +schemas/metaschema_public/tables/view/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/schema/table schemas/metaschema_public/tables/table/table schemas/metaschema_public/tables/database/table schemas/metaschema_public/types/object_category] 2026-01-23T00:00:00Z devin # add schemas/metaschema_public/tables/view/table +schemas/metaschema_public/tables/view_table/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/view/table schemas/metaschema_public/tables/table/table] 2026-01-23T00:00:00Z devin # add schemas/metaschema_public/tables/view_table/table +schemas/metaschema_public/tables/view_grant/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/view/table schemas/metaschema_public/tables/database/table] 2026-01-23T00:00:00Z devin # add schemas/metaschema_public/tables/view_grant/table +schemas/metaschema_public/tables/view_rule/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/view/table schemas/metaschema_public/tables/database/table] 2026-01-23T00:00:00Z devin # add schemas/metaschema_public/tables/view_rule/table +schemas/metaschema_public/tables/default_privilege/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/schema/table schemas/metaschema_public/tables/database/table] 2026-02-27T00:00:00Z Constructive # add schemas/metaschema_public/tables/default_privilege/table +schemas/metaschema_public/tables/enum/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/schema/table schemas/metaschema_public/types/object_category] 2026-03-15T00:00:00Z devin # add schemas/metaschema_public/tables/enum/table +schemas/metaschema_public/tables/embedding_chunks/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/table/table schemas/metaschema_public/tables/field/table] 2026-03-19T00:00:00Z devin # add schemas/metaschema_public/tables/embedding_chunks/table + +schemas/metaschema_public/tables/spatial_relation/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/table/table schemas/metaschema_public/tables/field/table schemas/metaschema_public/types/object_category] 2026-04-17T00:00:00Z devin # add schemas/metaschema_public/tables/spatial_relation/table +schemas/metaschema_public/tables/node_type_registry/table [schemas/metaschema_public/schema] 2026-04-30T00:00:00Z Constructive # add schemas/metaschema_public/tables/node_type_registry/table +schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed [schemas/metaschema_public/schema schemas/metaschema_public/tables/node_type_registry/table] 2026-04-30T00:00:01Z Constructive # seed node_type_registry data from upstream TS registry +schemas/metaschema_public/tables/function/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/schema/table] 2026-05-09T00:00:00Z devin # add metaschema_public.function table for tracking generated SQL functions +schemas/metaschema_public/tables/partition/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/table/table schemas/metaschema_public/tables/field/table] 2026-05-26T00:00:00Z Constructive # add metaschema_public.partition table for pg_partman lifecycle config +schemas/metaschema_public/tables/composite_type/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/schema/table schemas/metaschema_public/types/object_category] 2026-05-29T00:00:00Z devin # add metaschema_public.composite_type table for generated composite types diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_private/schema.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_private/schema.sql new file mode 100644 index 00000000..ac975528 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_private/schema.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_private/schema from pg + +BEGIN; + +DROP SCHEMA metaschema_private CASCADE; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/schema.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/schema.sql new file mode 100644 index 00000000..de7b232c --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/schema.sql @@ -0,0 +1,6 @@ + +BEGIN; + +DROP SCHEMA metaschema_public CASCADE; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/check_constraint/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/check_constraint/table.sql new file mode 100644 index 00000000..afa34f68 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/check_constraint/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/check_constraint/table from pg + +BEGIN; + +DROP TABLE metaschema_public.check_constraint; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/composite_type/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/composite_type/table.sql new file mode 100644 index 00000000..bc31bc06 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/composite_type/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/composite_type/table from pg + +BEGIN; + +DROP TABLE metaschema_public.composite_type; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx.sql new file mode 100644 index 00000000..3410c5a8 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx.sql @@ -0,0 +1,6 @@ + +BEGIN; + +DROP INDEX metaschema_public.databases_database_unique_name_idx; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/database/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/database/table.sql new file mode 100644 index 00000000..b76c8ade --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/database/table.sql @@ -0,0 +1,6 @@ + +BEGIN; + +DROP TABLE metaschema_public.database; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/default_privilege/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/default_privilege/table.sql new file mode 100644 index 00000000..186ea256 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/default_privilege/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/default_privilege/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_public.default_privilege; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/embedding_chunks/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/embedding_chunks/table.sql new file mode 100644 index 00000000..2c1919e8 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/embedding_chunks/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/embedding_chunks/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_public.embedding_chunks; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/enum/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/enum/table.sql new file mode 100644 index 00000000..2ece6210 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/enum/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/enum/table from pg + +BEGIN; + +DROP TABLE metaschema_public.enum; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx.sql new file mode 100644 index 00000000..959162b0 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx from pg + +BEGIN; + +DROP INDEX metaschema_public.databases_field_uniq_names_idx; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/field/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/field/table.sql new file mode 100644 index 00000000..ee7d49d0 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/field/table.sql @@ -0,0 +1,8 @@ + +BEGIN; + +DROP INDEX metaschema_public.field_database_id_idx; +DROP INDEX metaschema_public.field_table_id_idx; +DROP TABLE metaschema_public.field; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/foreign_key_constraint/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/foreign_key_constraint/table.sql new file mode 100644 index 00000000..46f68219 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/foreign_key_constraint/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/foreign_key_constraint/table from pg + +BEGIN; + +DROP TABLE metaschema_public.foreign_key_constraint; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/full_text_search/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/full_text_search/table.sql new file mode 100644 index 00000000..178a8f7e --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/full_text_search/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/full_text_search/table from pg + +BEGIN; + +DROP TABLE metaschema_public.full_text_search; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/function/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/function/table.sql new file mode 100644 index 00000000..b7a13ef0 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/function/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/function/table from pg + +BEGIN; + +DROP TABLE metaschema_public.function; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/index/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/index/table.sql new file mode 100644 index 00000000..f041cb38 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/index/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/index/table from pg + +BEGIN; + +DROP TABLE metaschema_public.index; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql new file mode 100644 index 00000000..1fd02531 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql @@ -0,0 +1,10 @@ +-- Revert schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed from pg +-- +-- GENERATED FILE — DO NOT EDIT +-- Regenerate with: cd packages/node-type-registry && pnpm generate + +BEGIN; + +DELETE FROM metaschema_public.node_type_registry; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/node_type_registry/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/node_type_registry/table.sql new file mode 100644 index 00000000..7e0f75fe --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/node_type_registry/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/node_type_registry/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_public.node_type_registry; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/partition/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/partition/table.sql new file mode 100644 index 00000000..976337d0 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/partition/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/partition/table from pg + +BEGIN; + +DROP TABLE metaschema_public.partition; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/policy/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/policy/table.sql new file mode 100644 index 00000000..654cb590 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/policy/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/policy/table from pg + +BEGIN; + +DROP TABLE metaschema_public.policy; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/primary_key_constraint/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/primary_key_constraint/table.sql new file mode 100644 index 00000000..6ece54e8 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/primary_key_constraint/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/primary_key_constraint/table from pg + +BEGIN; + +DROP TABLE metaschema_public.primary_key_constraint; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/schema/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/schema/table.sql new file mode 100644 index 00000000..d10b1c9d --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/schema/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/schema/table from pg + +BEGIN; + +DROP TABLE metaschema_public.schema; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/schema_grant/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/schema_grant/table.sql new file mode 100644 index 00000000..85a7e2f0 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/schema_grant/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/schema_grant/table from pg + +BEGIN; + +DROP TABLE metaschema_public.schema_grant; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/spatial_relation/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/spatial_relation/table.sql new file mode 100644 index 00000000..b0a74110 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/spatial_relation/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/spatial_relation/table from pg + +BEGIN; + +DROP TABLE metaschema_public.spatial_relation; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx.sql new file mode 100644 index 00000000..fad5449f --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx.sql @@ -0,0 +1,6 @@ + +BEGIN; + +DROP INDEX metaschema_public.databases_table_unique_name_idx; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/table/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/table/table.sql new file mode 100644 index 00000000..b911a39d --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/table/table.sql @@ -0,0 +1,6 @@ + +BEGIN; + +DROP TABLE metaschema_public.table; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/table_grant/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/table_grant/table.sql new file mode 100644 index 00000000..80fcd59f --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/table_grant/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/table_grant/table from pg + +BEGIN; + +DROP TABLE metaschema_public.table_grant; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/trigger/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/trigger/table.sql new file mode 100644 index 00000000..70e15e22 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/trigger/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/trigger/table from pg + +BEGIN; + +DROP TABLE metaschema_public.trigger; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/trigger_function/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/trigger_function/table.sql new file mode 100644 index 00000000..9ccb04e6 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/trigger_function/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/trigger_function/table from pg + +BEGIN; + +DROP TABLE metaschema_public.trigger_function; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/unique_constraint/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/unique_constraint/table.sql new file mode 100644 index 00000000..5edc1688 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/unique_constraint/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/unique_constraint/table from pg + +BEGIN; + +DROP TABLE metaschema_public.unique_constraint; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/view/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/view/table.sql new file mode 100644 index 00000000..422f467b --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/view/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/view/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_public.view; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/view_grant/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/view_grant/table.sql new file mode 100644 index 00000000..52fdd9fb --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/view_grant/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/view_grant/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_public.view_grant; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/view_rule/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/view_rule/table.sql new file mode 100644 index 00000000..76c3265d --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/view_rule/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/view_rule/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_public.view_rule; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/view_table/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/view_table/table.sql new file mode 100644 index 00000000..08df9345 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/tables/view_table/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/view_table/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_public.view_table; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/types/object_category.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/types/object_category.sql new file mode 100644 index 00000000..68174dd4 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/metaschema_public/types/object_category.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/types/object_category from pg + +BEGIN; + +DROP TYPE metaschema_public.object_category; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/services_private/schema.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/services_private/schema.sql new file mode 100644 index 00000000..710f99c9 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/services_private/schema.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_private/schema from pg + +BEGIN; + +DROP SCHEMA services_private; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/schema.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/schema.sql new file mode 100644 index 00000000..3fd696ac --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/schema.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/schema from pg + +BEGIN; + +DROP SCHEMA services_public; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/api_modules/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/api_modules/table.sql new file mode 100644 index 00000000..65543be1 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/api_modules/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/api_modules/table from pg + +BEGIN; + +DROP TABLE services_public.api_modules; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/api_schemas/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/api_schemas/table.sql new file mode 100644 index 00000000..8a310db7 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/api_schemas/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/api_schemas/table from pg + +BEGIN; + +DROP TABLE services_public.api_schemas; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/apis/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/apis/table.sql new file mode 100644 index 00000000..2feff0a6 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/apis/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/apis/table from pg + +BEGIN; + +DROP TABLE services_public.apis; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/apps/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/apps/table.sql new file mode 100644 index 00000000..816bf6d3 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/apps/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/apps/table from pg + +BEGIN; + +DROP TABLE services_public.apps; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/domains/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/domains/table.sql new file mode 100644 index 00000000..44b47a3e --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/domains/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/domains/table from pg + +BEGIN; + +DROP TABLE services_public.domains; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/site_metadata/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/site_metadata/table.sql new file mode 100644 index 00000000..cef080d5 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/site_metadata/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/site_metadata/table from pg + +BEGIN; + +DROP TABLE services_public.site_metadata; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/site_modules/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/site_modules/table.sql new file mode 100644 index 00000000..a63f2042 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/site_modules/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/site_modules/table from pg + +BEGIN; + +DROP TABLE services_public.site_modules; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/site_themes/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/site_themes/table.sql new file mode 100644 index 00000000..21f2965c --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/site_themes/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/site_themes/table from pg + +BEGIN; + +DROP TABLE services_public.site_themes; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/sites/table.sql b/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/sites/table.sql new file mode 100644 index 00000000..913178bb --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/revert/schemas/services_public/tables/sites/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/sites/table from pg + +BEGIN; + +DROP TABLE services_public.sites; + +COMMIT; diff --git a/extensions/@pgpm/metaschema-schema/sql/metaschema-schema--0.26.3.sql b/extensions/@pgpm/metaschema-schema/sql/metaschema-schema--0.26.3.sql new file mode 100644 index 00000000..e20b0a3e --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/sql/metaschema-schema--0.26.3.sql @@ -0,0 +1,1724 @@ +\echo Use "CREATE EXTENSION metaschema-schema" to load this file. \quit +CREATE SCHEMA metaschema_private; + +GRANT USAGE ON SCHEMA metaschema_private TO authenticated; + +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_private + GRANT ALL ON TABLES TO authenticated; + +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_private + GRANT ALL ON SEQUENCES TO authenticated; + +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_private + GRANT ALL ON FUNCTIONS TO authenticated; + +CREATE SCHEMA metaschema_public; + +GRANT USAGE ON SCHEMA metaschema_public TO authenticated; + +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_public + GRANT ALL ON TABLES TO authenticated; + +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_public + GRANT ALL ON SEQUENCES TO authenticated; + +ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_public + GRANT ALL ON FUNCTIONS TO authenticated; + +CREATE TYPE metaschema_public.object_category AS ENUM ('core', 'module', 'app'); + +CREATE TABLE metaschema_public.database ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + owner_id uuid, + schema_hash text, + name text, + label text, + hash uuid, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + UNIQUE (schema_hash) +); + +ALTER TABLE metaschema_public.database + ADD CONSTRAINT db_namechk + CHECK (char_length(name) > 2); + +COMMENT ON COLUMN metaschema_public.database.schema_hash IS '@omit'; + +CREATE TABLE metaschema_public.schema ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + name text NOT NULL, + schema_name text NOT NULL, + label text, + description text, + smart_tags jsonb, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', + is_public boolean NOT NULL DEFAULT true, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + UNIQUE (database_id, name), + UNIQUE (schema_name) +); + +ALTER TABLE metaschema_public.schema + ADD CONSTRAINT schema_namechk + CHECK (char_length(name) > 2); + +CREATE INDEX schema_database_id_idx ON metaschema_public.schema (database_id); + +CREATE TABLE metaschema_public."table" ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + schema_id uuid NOT NULL, + name text NOT NULL, + label text, + description text, + smart_tags jsonb, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + use_rls boolean NOT NULL DEFAULT false, + timestamps boolean NOT NULL DEFAULT false, + peoplestamps boolean NOT NULL DEFAULT false, + plural_name text, + singular_name text, + tags citext[] NOT NULL DEFAULT '{}', + partitioned boolean NOT NULL DEFAULT false, + partition_strategy text DEFAULT NULL, + partition_key_names text[] DEFAULT NULL, + partition_key_types text[] DEFAULT NULL, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + UNIQUE (database_id, schema_id, name) +); + +ALTER TABLE metaschema_public."table" + ADD COLUMN inherits_id uuid + NULL + REFERENCES metaschema_public."table" (id); + +CREATE INDEX table_schema_id_idx ON metaschema_public."table" (schema_id); + +CREATE INDEX table_database_id_idx ON metaschema_public."table" (database_id); + +CREATE TABLE metaschema_public.check_constraint ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL, + name text, + type text, + field_ids uuid[] NOT NULL, + expr jsonb, + smart_tags jsonb, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + UNIQUE (table_id, name), + CHECK (field_ids <> '{}') +); + +CREATE INDEX check_constraint_table_id_idx ON metaschema_public.check_constraint (table_id); + +CREATE INDEX check_constraint_database_id_idx ON metaschema_public.check_constraint (database_id); + +CREATE FUNCTION metaschema_private.database_name_hash(name text) RETURNS bytea AS $EOFCODE$ + SELECT + DECODE(MD5(LOWER(inflection.plural (name))), 'hex'); +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE UNIQUE INDEX databases_database_unique_name_idx ON metaschema_public.database (owner_id, (metaschema_private.database_name_hash(name))); + +CREATE TABLE metaschema_public.field ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL, + name text NOT NULL, + label text, + description text, + smart_tags jsonb, + is_required boolean NOT NULL DEFAULT false, + api_required boolean NOT NULL DEFAULT false, + default_value jsonb NULL DEFAULT NULL, + type jsonb NOT NULL, + field_order int NOT NULL DEFAULT 0, + regexp text DEFAULT NULL, + chk jsonb DEFAULT NULL, + chk_expr jsonb DEFAULT NULL, + min double precision DEFAULT NULL, + max double precision DEFAULT NULL, + tags citext[] NOT NULL DEFAULT '{}', + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + UNIQUE (table_id, name) +); + +CREATE INDEX field_table_id_idx ON metaschema_public.field (table_id); + +CREATE INDEX field_database_id_idx ON metaschema_public.field (database_id); + +CREATE UNIQUE INDEX databases_field_uniq_names_idx ON metaschema_public.field (table_id, (decode(md5(lower(CASE + WHEN (type ->> 'name') = 'uuid' THEN regexp_replace(name, '^(.+?)(_row_id|_id|_uuid|_fk|_pk)$', E'\\1', 'i') + ELSE name +END)), 'hex'))); + +CREATE TABLE metaschema_public.foreign_key_constraint ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL, + name text, + description text, + smart_tags jsonb, + type text, + field_ids uuid[] NOT NULL, + ref_table_id uuid NOT NULL REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + ref_field_ids uuid[] NOT NULL, + delete_action char(1) DEFAULT 'c', + update_action char(1) DEFAULT 'a', + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + UNIQUE (table_id, name), + CHECK (field_ids <> '{}'), + CHECK (ref_field_ids <> '{}') +); + +CREATE INDEX foreign_key_constraint_table_id_idx ON metaschema_public.foreign_key_constraint (table_id); + +CREATE INDEX foreign_key_constraint_database_id_idx ON metaschema_public.foreign_key_constraint (database_id); + +CREATE TABLE metaschema_public.full_text_search ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL, + field_id uuid NOT NULL, + field_ids uuid[] NOT NULL, + weights text[] NOT NULL, + langs text[] NOT NULL, + lang_column text, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CHECK ( + cardinality(field_ids) = cardinality(weights) + AND cardinality(weights) = cardinality(langs) + ) +); + +CREATE INDEX full_text_search_table_id_idx ON metaschema_public.full_text_search (table_id); + +CREATE INDEX full_text_search_database_id_idx ON metaschema_public.full_text_search (database_id); + +CREATE TABLE metaschema_public.index ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + table_id uuid NOT NULL, + name text NOT NULL DEFAULT '', + field_ids uuid[], + include_field_ids uuid[], + access_method text NOT NULL DEFAULT 'BTREE', + index_params jsonb, + where_clause jsonb, + is_unique boolean NOT NULL DEFAULT false, + options jsonb, + op_classes text[], + smart_tags jsonb, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + UNIQUE (database_id, name) +); + +CREATE INDEX index_table_id_idx ON metaschema_public.index (table_id); + +CREATE INDEX index_database_id_idx ON metaschema_public.index (database_id); + +CREATE TABLE metaschema_public.policy ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL, + name text, + grantee_name text, + privilege text, + permissive boolean DEFAULT true, + disabled boolean DEFAULT false, + policy_type text, + data jsonb, + smart_tags jsonb, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + UNIQUE (table_id, name) +); + +CREATE INDEX policy_table_id_idx ON metaschema_public.policy (table_id); + +CREATE INDEX policy_database_id_idx ON metaschema_public.policy (database_id); + +CREATE TABLE metaschema_public.primary_key_constraint ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL, + name text, + type text, + field_ids uuid[] NOT NULL, + smart_tags jsonb, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + UNIQUE (table_id, name), + CHECK (field_ids <> '{}') +); + +CREATE INDEX primary_key_constraint_table_id_idx ON metaschema_public.primary_key_constraint (table_id); + +CREATE INDEX primary_key_constraint_database_id_idx ON metaschema_public.primary_key_constraint (database_id); + +CREATE TABLE metaschema_public.schema_grant ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + schema_id uuid NOT NULL, + grantee_name text NOT NULL, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE +); + +CREATE INDEX schema_grant_schema_id_idx ON metaschema_public.schema_grant (schema_id); + +CREATE INDEX schema_grant_database_id_idx ON metaschema_public.schema_grant (database_id); + +CREATE TABLE metaschema_public.table_grant ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL, + privilege text NOT NULL, + grantee_name text NOT NULL, + field_ids uuid[], + is_grant boolean NOT NULL DEFAULT true, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX table_grant_table_id_idx ON metaschema_public.table_grant (table_id); + +CREATE INDEX table_grant_database_id_idx ON metaschema_public.table_grant (database_id); + +CREATE UNIQUE INDEX table_grant_unique_idx ON metaschema_public.table_grant (table_id, privilege, grantee_name, (COALESCE(field_ids, CAST('{}' AS uuid[])))); + +CREATE FUNCTION metaschema_private.table_name_hash(name text) RETURNS bytea AS $EOFCODE$ + SELECT + DECODE(MD5(LOWER(inflection.plural (name))), 'hex'); +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE UNIQUE INDEX databases_table_unique_name_idx ON metaschema_public."table" (database_id, schema_id, (metaschema_private.table_name_hash(name))); + +CREATE TABLE metaschema_public.trigger_function ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + name text NOT NULL, + code text, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + UNIQUE (database_id, name) +); + +CREATE INDEX trigger_function_database_id_idx ON metaschema_public.trigger_function (database_id); + +CREATE TABLE metaschema_public.trigger ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL, + name text NOT NULL, + event text, + function_name text, + smart_tags jsonb, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + UNIQUE (table_id, name) +); + +CREATE INDEX trigger_table_id_idx ON metaschema_public.trigger (table_id); + +CREATE INDEX trigger_database_id_idx ON metaschema_public.trigger (database_id); + +CREATE TABLE metaschema_public.unique_constraint ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL, + name text, + description text, + smart_tags jsonb, + type text, + field_ids uuid[] NOT NULL, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + UNIQUE (table_id, name), + CHECK (field_ids <> '{}') +); + +CREATE INDEX unique_constraint_table_id_idx ON metaschema_public.unique_constraint (table_id); + +CREATE INDEX unique_constraint_database_id_idx ON metaschema_public.unique_constraint (database_id); + +CREATE TABLE metaschema_public.view ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + schema_id uuid NOT NULL, + name text NOT NULL, + table_id uuid, + view_type text NOT NULL, + data jsonb DEFAULT '{}', + filter_type text, + filter_data jsonb DEFAULT '{}', + security_invoker boolean DEFAULT true, + is_read_only boolean DEFAULT true, + smart_tags jsonb, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + UNIQUE (schema_id, name) +); + +CREATE INDEX view_schema_id_idx ON metaschema_public.view (schema_id); + +CREATE INDEX view_database_id_idx ON metaschema_public.view (database_id); + +CREATE INDEX view_table_id_idx ON metaschema_public.view (table_id); + +CREATE TABLE metaschema_public.view_table ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + view_id uuid NOT NULL, + table_id uuid NOT NULL, + join_order int NOT NULL DEFAULT 0, + CONSTRAINT view_fkey + FOREIGN KEY(view_id) + REFERENCES metaschema_public.view (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + UNIQUE (view_id, table_id) +); + +COMMENT ON TABLE metaschema_public.view_table IS 'Junction table linking views to their joined tables for referential integrity'; + +CREATE INDEX view_table_view_id_idx ON metaschema_public.view_table (view_id); + +CREATE INDEX view_table_table_id_idx ON metaschema_public.view_table (table_id); + +CREATE TABLE metaschema_public.view_grant ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + view_id uuid NOT NULL, + grantee_name text NOT NULL, + privilege text NOT NULL, + with_grant_option boolean DEFAULT false, + is_grant boolean NOT NULL DEFAULT true, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT view_fkey + FOREIGN KEY(view_id) + REFERENCES metaschema_public.view (id) + ON DELETE CASCADE, + UNIQUE (view_id, grantee_name, privilege, is_grant) +); + +CREATE INDEX view_grant_view_id_idx ON metaschema_public.view_grant (view_id); + +CREATE INDEX view_grant_database_id_idx ON metaschema_public.view_grant (database_id); + +CREATE TABLE metaschema_public.view_rule ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + view_id uuid NOT NULL, + name text NOT NULL, + event text NOT NULL, + action text NOT NULL DEFAULT 'NOTHING', + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT view_fkey + FOREIGN KEY(view_id) + REFERENCES metaschema_public.view (id) + ON DELETE CASCADE, + UNIQUE (view_id, name) +); + +COMMENT ON TABLE metaschema_public.view_rule IS 'DO INSTEAD rules for views (e.g., read-only enforcement)'; + +COMMENT ON COLUMN metaschema_public.view_rule.event IS 'INSERT, UPDATE, or DELETE'; + +COMMENT ON COLUMN metaschema_public.view_rule.action IS 'NOTHING (for read-only) or custom action'; + +CREATE INDEX view_rule_view_id_idx ON metaschema_public.view_rule (view_id); + +CREATE INDEX view_rule_database_id_idx ON metaschema_public.view_rule (database_id); + +CREATE TABLE metaschema_public.default_privilege ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + schema_id uuid NOT NULL, + object_type text NOT NULL, + privilege text NOT NULL, + grantee_name text NOT NULL, + is_grant boolean NOT NULL DEFAULT true, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + UNIQUE (schema_id, object_type, privilege, grantee_name, is_grant) +); + +CREATE INDEX default_privilege_schema_id_idx ON metaschema_public.default_privilege (schema_id); + +CREATE INDEX default_privilege_database_id_idx ON metaschema_public.default_privilege (database_id); + +CREATE TABLE metaschema_public.enum ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL, + name text NOT NULL, + label text, + description text, + values text[] NOT NULL DEFAULT '{}', + smart_tags jsonb, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + UNIQUE (schema_id, name) +); + +CREATE INDEX enum_schema_id_idx ON metaschema_public.enum (schema_id); + +CREATE INDEX enum_database_id_idx ON metaschema_public.enum (database_id); + +CREATE TABLE metaschema_public.embedding_chunks ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL, + embedding_field_id uuid, + chunks_table_id uuid, + chunks_table_name text, + content_field_name text NOT NULL DEFAULT 'content', + dimensions int NOT NULL DEFAULT 768, + metric text NOT NULL DEFAULT 'cosine', + chunk_size int NOT NULL DEFAULT 1000, + chunk_overlap int NOT NULL DEFAULT 200, + chunk_strategy text NOT NULL DEFAULT 'fixed', + metadata_fields jsonb, + search_indexes jsonb, + enqueue_chunking_job boolean NOT NULL DEFAULT true, + chunking_task_name text NOT NULL DEFAULT 'generate_chunks', + embedding_model text, + embedding_provider text, + parent_fk_field_id uuid, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT chunks_table_fkey + FOREIGN KEY(chunks_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE SET NULL, + CONSTRAINT embedding_field_fkey + FOREIGN KEY(embedding_field_id) + REFERENCES metaschema_public.field (id) + ON DELETE SET NULL, + CONSTRAINT parent_fk_field_fkey + FOREIGN KEY(parent_fk_field_id) + REFERENCES metaschema_public.field (id) + ON DELETE SET NULL, + CONSTRAINT valid_metric + CHECK (metric IN ('cosine', 'l2', 'ip')), + CONSTRAINT valid_chunk_strategy + CHECK (chunk_strategy IN ('fixed', 'sentence', 'paragraph', 'semantic')), + CONSTRAINT valid_dimensions + CHECK (dimensions > 0), + CONSTRAINT valid_chunk_size + CHECK (chunk_size > 0), + CONSTRAINT valid_chunk_overlap + CHECK ( + chunk_overlap >= 0 + AND chunk_overlap < chunk_size + ) +); + +CREATE INDEX embedding_chunks_table_id_idx ON metaschema_public.embedding_chunks (table_id); + +CREATE INDEX embedding_chunks_database_id_idx ON metaschema_public.embedding_chunks (database_id); + +CREATE INDEX embedding_chunks_chunks_table_id_idx ON metaschema_public.embedding_chunks (chunks_table_id); + +CREATE TABLE metaschema_public.spatial_relation ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL, + field_id uuid NOT NULL, + ref_table_id uuid NOT NULL, + ref_field_id uuid NOT NULL, + name text NOT NULL, + operator text NOT NULL, + param_name text NULL, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT field_fkey + FOREIGN KEY(field_id) + REFERENCES metaschema_public.field (id) + ON DELETE CASCADE, + CONSTRAINT ref_table_fkey + FOREIGN KEY(ref_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT ref_field_fkey + FOREIGN KEY(ref_field_id) + REFERENCES metaschema_public.field (id) + ON DELETE CASCADE, + UNIQUE (table_id, name), + CHECK (operator IN ('st_contains', 'st_within', 'st_covers', 'st_coveredby', 'st_intersects', 'st_equals', 'st_bbox_intersects', 'st_dwithin')), + CHECK ( + (operator = 'st_dwithin' + AND param_name IS NOT NULL) + OR (operator <> 'st_dwithin' + AND param_name IS NULL) + ) +); + +CREATE INDEX spatial_relation_table_id_idx ON metaschema_public.spatial_relation (table_id); + +CREATE INDEX spatial_relation_field_id_idx ON metaschema_public.spatial_relation (field_id); + +CREATE INDEX spatial_relation_database_id_idx ON metaschema_public.spatial_relation (database_id); + +CREATE INDEX spatial_relation_ref_table_id_idx ON metaschema_public.spatial_relation (ref_table_id); + +CREATE INDEX spatial_relation_ref_field_id_idx ON metaschema_public.spatial_relation (ref_field_id); + +CREATE TABLE metaschema_public.node_type_registry ( + name text PRIMARY KEY, + slug text NOT NULL UNIQUE, + category text NOT NULL, + display_name text, + description text, + parameter_schema jsonb NOT NULL DEFAULT '{}'::jsonb, + tags text[] NOT NULL DEFAULT CAST('{}' AS text[]) +); + +CREATE INDEX node_type_registry_category_idx ON metaschema_public.node_type_registry (category); + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('AuthzAllowAll', 'authz_allow_all', 'authz', 'Public Access', 'Allows all access. Generates TRUE expression.', '{"type":"object","properties":{}}'::jsonb, CAST('{"authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('AuthzAppMembership', 'authz_app_membership_check', 'authz', 'App Membership Check', 'App-level membership check (hardcoded membership_type=1). Verifies the user has app membership (optionally with specific permission) without binding to any entity from the row. Uses EXISTS subquery against SPRT table. For entity-scoped checks (org, channel, etc.), use AuthzEntityMembership instead.', CAST('{"type":"object","properties":{"permission":{"type":"string","description":"Single permission name to check (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check (ORed together into mask)"},"is_admin":{"type":"boolean","description":"If true, require is_admin flag"},"is_owner":{"type":"boolean","description":"If true, require is_owner flag"}},"required":[]}' AS jsonb), CAST('{"membership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('AuthzComposite', 'authz_composite', 'authz', 'Composite Policy', 'Composite authorization policy that combines multiple authorization nodes using boolean logic (AND/OR). The data field contains a JSONB AST with nested authorization nodes.', '{"type":"object","description":"A composite policy containing nested authorization nodes combined with boolean logic","properties":{"BoolExpr":{"type":"object","description":"Boolean expression combining multiple authorization nodes","properties":{"boolop":{"type":"string","enum":["AND_EXPR","OR_EXPR","NOT_EXPR"],"description":"Boolean operator: AND_EXPR, OR_EXPR, or NOT_EXPR"},"args":{"type":"array","description":"Array of authorization nodes to combine","items":{"type":"object"}}}}}}'::jsonb, CAST('{"composite","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('AuthzDenyAll', 'authz_deny_all', 'authz', 'No Access', 'Denies all access. Generates FALSE expression.', '{"type":"object","properties":{}}'::jsonb, CAST('{"authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('AuthzDirectOwner', 'authz_direct_owner', 'authz', 'Direct Ownership', 'Direct equality comparison between a table column and the current user ID. Simplest authorization pattern with no subqueries.', CAST('{"type":"object","properties":{"entity_field":{"type":"string","format":"column-ref","description":"Column name containing the owner user ID (e.g., owner_id)"}},"required":["entity_field"]}' AS jsonb), CAST('{"ownership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('AuthzDirectOwnerAny', 'authz_direct_owner_any', 'authz', 'Multi-Owner Access', 'OR logic for multiple ownership fields. Checks if current user matches any of the specified fields.', '{"type":"object","properties":{"entity_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Array of column names to check for ownership"}},"required":["entity_fields"]}'::jsonb, CAST('{"ownership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('AuthzEntityMembership', 'authz_entity_membership', 'authz', 'Entity Membership', 'Membership check scoped by a field on the row through the SPRT table. Verifies user has membership in the entity referenced by the row.', CAST('{"type":"object","properties":{"entity_field":{"type":"string","format":"column-ref","description":"Column name referencing the entity (e.g., entity_id, org_id)"},"sel_field":{"type":"string","description":"SPRT column to select for the entity match","default":"entity_id"},"membership_type":{"type":["integer","string"],"description":"Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"},"entity_type":{"type":"string","description":"Entity type prefix (e.g. ''channel'', ''department''). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."},"permission":{"type":"string","description":"Single permission name to check (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check (ORed together into mask)"},"is_admin":{"type":"boolean","description":"If true, require is_admin flag"},"is_owner":{"type":"boolean","description":"If true, require is_owner flag"}},"required":["entity_field"]}' AS jsonb), CAST('{"membership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('AuthzFilePath', 'authz_file_path', 'authz', 'File Path Share', 'Path-scoped file sharing via ltree containment. Grants access when a path_shares row matches the current user, bucket, and an ancestor path with the required permission.', CAST('{"type":"object","properties":{"shares_schema":{"type":"string","description":"Schema of the path_shares table"},"shares_table":{"type":"string","description":"Name of the path_shares table"},"files_schema":{"type":"string","description":"Schema of the files table (used to qualify column references inside the EXISTS subquery)"},"files_table":{"type":"string","description":"Name of the files table (used to qualify column references inside the EXISTS subquery)"},"permission_field":{"type":"string","format":"column-ref","description":"Boolean column on the path_shares table that grants the required permission (e.g. can_read, can_write)"},"bucket_field":{"type":"string","format":"column-ref","description":"Column on the files table referencing the bucket","default":"bucket_id"},"path_field":{"type":"string","format":"column-ref","description":"Ltree column on the files table representing the file path","default":"path"}},"required":["shares_schema","shares_table","files_table","permission_field"]}' AS jsonb), CAST('{"storage","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('AuthzMemberList', 'authz_member_list', 'authz', 'Member List', 'Check if current user is in an array column on the same row.', '{"type":"object","properties":{"array_field":{"type":"string","format":"column-ref","description":"Column name containing the array of user IDs"}},"required":["array_field"]}'::jsonb, CAST('{"ownership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('AuthzMemberOwner', 'authz_member_owner', 'authz', 'Member Owner', 'Compound policy: the row must be owned by the current user (owner_field = current_user_id) AND the current user must be a member of the entity referenced by entity_field. Combines direct ownership with entity membership — the actor can only access rows they own within entities they belong to.', CAST('{"type":"object","properties":{"owner_field":{"type":"string","format":"column-ref","description":"Column name containing the owner user ID (e.g., owner_id)","default":"owner_id"},"entity_field":{"type":"string","format":"column-ref","description":"Column name referencing the entity (e.g., entity_id)","default":"entity_id"},"sel_field":{"type":"string","description":"SPRT column to select for the entity match","default":"entity_id"},"membership_type":{"type":["integer","string"],"description":"Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"},"entity_type":{"type":"string","description":"Entity type prefix (e.g. ''channel'', ''department''). Resolved to membership_type integer via memberships_module lookup."},"permission":{"type":"string","description":"Single permission name to check (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check (ORed together into mask)"}},"required":["owner_field","entity_field"]}' AS jsonb), CAST('{"ownership","membership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('AuthzNotReadOnly', 'authz_not_read_only', 'authz', 'Not Read-Only', 'Restrictive policy that blocks read-only members from mutations. Checks actor_id + is_read_only IS NOT TRUE on the SPRT. Designed to run as a restrictive counterpart after a permissive AuthzEntityMembership policy has already verified membership.', CAST('{"type":"object","properties":{"entity_field":{"type":"string","format":"column-ref","description":"Column name referencing the entity (e.g., entity_id, org_id)"},"membership_type":{"type":["integer","string"],"description":"Scope: 2=org, 3+=dynamic entity types. Must be >= 2 (entity-scoped)."}},"required":["entity_field"]}' AS jsonb), CAST('{"membership","authz","restrictive"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('AuthzOrgHierarchy', 'authz_org_hierarchy', 'authz', 'Org Hierarchy', 'Organizational hierarchy visibility using closure table. Managers can see subordinate data or subordinates can see manager data.', CAST('{"type":"object","properties":{"direction":{"type":"string","enum":["up","down"],"description":"down=manager sees subordinates, up=subordinate sees managers"},"entity_field":{"type":"string","format":"column-ref","description":"Field referencing the org entity","default":"entity_id"},"anchor_field":{"type":"string","format":"column-ref","description":"Field referencing the user (e.g., owner_id)"},"max_depth":{"type":"integer","description":"Optional max depth to limit visibility"}},"required":["direction","anchor_field"]}' AS jsonb), CAST('{"membership","hierarchy","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('AuthzPeerOwnership', 'authz_peer_ownership', 'authz', 'Peer Ownership', 'Peer visibility through shared entity membership. Authorizes access to user-owned rows when the owner and current user are both members of the same entity. Self-joins the SPRT table to find peers.', CAST('{"type":"object","properties":{"owner_field":{"type":"string","format":"column-ref","description":"Column name on protected table referencing the owning user (e.g., owner_id)"},"membership_type":{"type":["integer","string"],"description":"Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"},"entity_type":{"type":"string","description":"Entity type prefix (e.g. ''channel'', ''department''). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."},"permission":{"type":"string","description":"Single permission name to check on the current user membership (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check on the current user membership (ORed together into mask)"},"is_admin":{"type":"boolean","description":"If true, require is_admin flag on current user membership"},"is_owner":{"type":"boolean","description":"If true, require is_owner flag on current user membership"}},"required":["owner_field"]}' AS jsonb), CAST('{"membership","peer","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('AuthzPublishable', 'authz_publishable', 'authz', 'Published Content', 'Published state access control. Restricts access to records that are published.', CAST('{"type":"object","properties":{"is_published_field":{"type":"string","format":"column-ref","description":"Boolean field indicating published state","default":"is_published"},"published_at_field":{"type":"string","format":"column-ref","description":"Timestamp field for publish time","default":"published_at"},"require_published_at":{"type":"boolean","description":"Require published_at to be non-null and <= now()","default":true}}}' AS jsonb), CAST('{"temporal","publishing","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('AuthzRelatedEntityMembership', 'authz_related_entity_membership', 'authz', 'Related Entity Membership', 'JOIN-based membership verification through related tables. Joins SPRT table with another table to verify membership.', CAST('{"type":"object","properties":{"entity_field":{"type":"string","format":"column-ref","description":"Column name on protected table referencing the join table"},"sel_field":{"type":"string","description":"SPRT column to select for the entity match","default":"entity_id"},"sprt_join_field":{"type":"string","description":"SPRT column to join on with the related table","default":"entity_id"},"membership_type":{"type":["integer","string"],"description":"Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"},"entity_type":{"type":"string","description":"Entity type prefix (e.g. ''channel'', ''department''). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."},"obj_table_id":{"type":"string","format":"uuid","description":"UUID of the join table (alternative to obj_schema/obj_table)"},"obj_schema":{"type":"string","description":"Schema of the join table (or use obj_table_id)"},"obj_table":{"type":"string","description":"Name of the join table (or use obj_table_id)"},"obj_field_id":{"type":"string","format":"uuid","description":"UUID of field on join table (alternative to obj_field)"},"obj_field":{"type":"string","format":"column-ref","description":"Field name on join table to match against SPRT entity_id"},"permission":{"type":"string","description":"Single permission name to check (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check (ORed together into mask)"},"is_admin":{"type":"boolean","description":"If true, require is_admin flag"},"is_owner":{"type":"boolean","description":"If true, require is_owner flag"}},"required":["entity_field"]}' AS jsonb), CAST('{"membership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('AuthzRelatedMemberList', 'authz_related_member_list', 'authz', 'Related Member List', 'Array membership check in a related table.', '{"type":"object","properties":{"owned_schema":{"type":"string","description":"Schema of the related table"},"owned_table":{"type":"string","description":"Name of the related table"},"owned_table_key":{"type":"string","format":"column-ref","description":"Array column in related table"},"owned_table_ref_key":{"type":"string","format":"column-ref","description":"FK column in related table"},"this_object_key":{"type":"string","format":"column-ref","description":"PK column in protected table"}},"required":["owned_schema","owned_table","owned_table_key","owned_table_ref_key","this_object_key"]}'::jsonb, CAST('{"ownership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('AuthzRelatedPeerOwnership', 'authz_related_peer_ownership', 'authz', 'Related Peer Ownership', 'Peer visibility through shared entity membership via a related table. Like AuthzPeerOwnership but the owning user is resolved through a FK JOIN to a related table. Combines SPRT self-join with object table JOIN.', CAST('{"type":"object","properties":{"entity_field":{"type":"string","format":"column-ref","description":"Column name on protected table referencing the related table (e.g., message_id)"},"membership_type":{"type":["integer","string"],"description":"Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"},"entity_type":{"type":"string","description":"Entity type prefix (e.g. ''channel'', ''department''). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."},"obj_table_id":{"type":"string","format":"uuid","description":"UUID of the related table (alternative to obj_schema/obj_table)"},"obj_schema":{"type":"string","description":"Schema of the related table (or use obj_table_id)"},"obj_table":{"type":"string","description":"Name of the related table (or use obj_table_id)"},"obj_field_id":{"type":"string","format":"uuid","description":"UUID of field on related table containing the owner user ID (alternative to obj_field)"},"obj_field":{"type":"string","format":"column-ref","description":"Field name on related table containing the owner user ID (e.g., sender_id)"},"obj_ref_field":{"type":"string","format":"column-ref","description":"Field on related table to select for matching entity_field","default":"id"},"permission":{"type":"string","description":"Single permission name to check on the current user membership (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check on the current user membership (ORed together into mask)"},"is_admin":{"type":"boolean","description":"If true, require is_admin flag on current user membership"},"is_owner":{"type":"boolean","description":"If true, require is_owner flag on current user membership"}},"required":["entity_field"]}' AS jsonb), CAST('{"membership","peer","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('AuthzTemporal', 'authz_temporal', 'authz', 'Temporal Access', 'Time-window based access control. Restricts access based on valid_from and/or valid_until timestamps. At least one of valid_from_field or valid_until_field must be provided.', CAST('{"type":"object","properties":{"valid_from_field":{"type":"string","format":"column-ref","description":"Column for start time (at least one of valid_from_field or valid_until_field required)"},"valid_until_field":{"type":"string","format":"column-ref","description":"Column for end time (at least one of valid_from_field or valid_until_field required)"},"valid_from_inclusive":{"type":"boolean","description":"Include start boundary","default":true},"valid_until_inclusive":{"type":"boolean","description":"Include end boundary","default":false}},"anyOf":[{"required":["valid_from_field"]},{"required":["valid_until_field"]}]}' AS jsonb), CAST('{"temporal","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('CheckGreaterThan', 'check_greater_than', 'check', 'Check Greater Than', 'Adds a CHECK constraint that validates a column value is greater than a threshold (single-column: column > value) or that one column is greater than another (cross-column: columns[0] > columns[1]). Compiled via AST helpers.', CAST('{"type":"object","properties":{"column":{"type":"string","format":"column-ref","description":"Single column to compare against value (mutually exclusive with columns)"},"value":{"type":"number","description":"Threshold value for single-column comparison (column > value)","default":0},"columns":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Two columns for cross-column comparison (columns[0] > columns[1])","minItems":2,"maxItems":2}}}' AS jsonb), CAST('{"check","constraint","validation","comparison"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('CheckLessThan', 'check_less_than', 'check', 'Check Less Than', 'Adds a CHECK constraint that validates a column value is less than a threshold (single-column: column < value) or that one column is less than another (cross-column: columns[0] < columns[1]). Compiled via AST helpers.', CAST('{"type":"object","properties":{"column":{"type":"string","format":"column-ref","description":"Single column to compare against value (mutually exclusive with columns)"},"value":{"type":"number","description":"Threshold value for single-column comparison (column < value)"},"columns":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Two columns for cross-column comparison (columns[0] < columns[1])","minItems":2,"maxItems":2}}}' AS jsonb), CAST('{"check","constraint","validation","comparison"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('CheckNotEqual', 'check_not_equal', 'check', 'Check Not Equal', 'Adds a CHECK constraint that validates two columns are not equal (columns[0] != columns[1]). Useful for preventing self-referencing rows. Compiled via AST helpers.', '{"type":"object","properties":{"columns":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Two columns that must not be equal","minItems":2,"maxItems":2}},"required":["columns"]}'::jsonb, CAST('{"check","constraint","validation","inequality"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('CheckOneOf', 'check_one_of', 'check', 'Check One Of', 'Adds a CHECK constraint that validates a column value is one of an allowed set (e.g. tier IN (''free'', ''paid'', ''custom'')). Compiled to column = ANY(ARRAY[...]) via AST helpers.', '{"type":"object","properties":{"column":{"type":"string","format":"column-ref","description":"Column to validate against the allowed values"},"values":{"type":"array","items":{"type":"string"},"description":"Array of allowed values for the column"}},"required":["column","values"]}'::jsonb, CAST('{"check","constraint","validation","enum"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataArchivable', 'data_archivable', 'data', 'Archivable', 'Adds user-reversible archive support with is_archived boolean and archived_at timestamp, plus a partial index for efficient active-row queries.', '{"type":"object","properties":{"is_archived_field":{"type":"string","format":"column-ref","description":"Column name for the archive boolean flag","default":"is_archived"},"archived_at_field":{"type":"string","format":"column-ref","description":"Column name for the archive timestamp","default":"archived_at"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true}}}'::jsonb, CAST('{"schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataBulk', 'data_bulk', 'data', 'Bulk Operations', 'Enables bulk mutation smart tags on a table. When provisioned, adds @behavior tags for the selected bulk operations (insert, upsert, update, delete). Requires the graphile-bulk-mutations plugin.', CAST('{"type":"object","properties":{"insert":{"type":"boolean","description":"Enable bulk insert (+bulkInsert)","default":true},"upsert":{"type":"boolean","description":"Enable bulk upsert (+bulkUpsert)","default":false},"update":{"type":"boolean","description":"Enable bulk update (+bulkUpdate)","default":false},"delete":{"type":"boolean","description":"Enable bulk delete (+bulkDelete)","default":false}}}' AS jsonb), CAST('{"bulk","mutations","graphile"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataCompositeField', 'data_composite_field', 'data', 'Composite Field', 'Creates a derived text field that automatically concatenates multiple source fields via BEFORE INSERT/UPDATE triggers. Used to produce a unified text representation (e.g., embedding_text) from multiple columns on a table. The trigger fires with ''_000'' prefix to run before Search* triggers alphabetically.', CAST('{"type":"object","properties":{"target":{"type":"string","format":"column-ref","description":"Name of the derived text field to create","default":"embedding_text"},"source_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Array of source field names to concatenate into the target field"},"format":{"type":"string","enum":["labeled","plain"],"description":"Output format: ''labeled'' (field_name: value) or ''plain'' (values only)","default":"labeled"}},"required":["source_fields"]}' AS jsonb), CAST('{"transform","behavior"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataDirectOwner', 'data_direct_owner', 'data', 'Ownership', 'Adds ownership column for direct user ownership. Enables AuthzDirectOwner authorization.', '{"type":"object","properties":{"owner_field_name":{"type":"string","format":"column-ref","description":"Column name for owner ID","default":"owner_id"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds a foreign key constraint from owner_id to the users table","default":true},"create_index":{"type":"boolean","description":"If true, creates a B-tree index on the owner column","default":true}}}'::jsonb, CAST('{"ownership","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataEntityMembership', 'data_entity_membership', 'data', 'Entity Membership', 'Adds entity reference for organization/group scoping. Enables AuthzEntityMembership, AuthzMembership, AuthzOrgHierarchy authorization.', '{"type":"object","properties":{"entity_field_name":{"type":"string","format":"column-ref","description":"Column name for entity ID","default":"entity_id"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds a foreign key constraint from entity_id to the users table","default":true},"create_index":{"type":"boolean","description":"If true, creates a B-tree index on the entity column","default":true}}}'::jsonb, CAST('{"membership","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataForceCurrentUser', 'data_force_current_user', 'data', 'Force Current User', 'BEFORE INSERT trigger that forces a field to the value of jwt_public.current_user_id(). Prevents clients from spoofing the actor/uploader identity. The field value is always overwritten regardless of what the client provides.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the field to force to current_user_id()","default":"actor_id"}}}' AS jsonb), CAST('{"trigger","security","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataI18n', 'data_i18n', 'data', 'Internationalization', 'Creates a companion _translations table with lang_code + translatable fields. Copies SELECT policies and column-ref fields from the base table. Adds @i18n smart comment so the Graphile i18n plugin discovers it. Requires i18n_module to be provisioned for the database.', CAST('{"type":"object","properties":{"fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names on the base table to make translatable. Each field is duplicated on the translation table with the same type."},"table_suffix":{"type":"string","description":"Suffix for the translation table name","default":"_translations"},"lang_code_type":{"type":"string","enum":["citext","text"],"description":"Type for the lang_code column","default":"citext"},"copy_mutation_policies":{"type":"boolean","description":"Whether to also copy INSERT/UPDATE/DELETE policies (not just SELECT). Default true — translations should be editable by the same users who can edit the base row.","default":true},"search":{"type":"object","description":"SearchFullText configuration for the translations table. When provided, creates a tsvector column on the translations table with lang_column=lang_code for dynamic per-row language stemming.","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the tsvector column on the translations table","default":"search"},"source_fields":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","format":"column-ref","description":"Name of the translatable source column"},"weight":{"type":"string","enum":["A","B","C","D"],"description":"tsvector weight class (A=highest, D=lowest)","default":"D"}},"required":["field"]},"description":"Translatable columns that feed the tsvector. Language is determined dynamically from the lang_code column of each row."},"search_score_weight":{"type":"number","description":"Weight for this algorithm in composite searchScore","default":1}},"required":["source_fields"]}},"required":["fields"]}' AS jsonb), CAST('{"i18n","translation","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataId', 'data_id', 'data', 'Primary Key ID', 'Adds a UUID primary key column with auto-generation default (uuidv7). This is the standard primary key pattern for all tables.', '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the primary key","default":"id"}}}'::jsonb, CAST('{"primary_key","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataImmutableFields', 'data_immutable_fields', 'data', 'Immutable Fields', 'BEFORE UPDATE trigger that prevents changes to a list of specified fields after INSERT. Raises an exception if any of the listed fields have changed. Unlike FieldImmutable (single-field), this handles multiple fields in a single trigger for efficiency.', CAST(E'{"type":"object","properties":{"fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names that cannot be modified after INSERT (e.g. [\\"key\\", \\"bucket_id\\", \\"owner_id\\"])"}},"required":["fields"]}' AS jsonb), CAST('{"trigger","constraint","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataInflection', 'data_inflection', 'data', 'Inflection', 'Transforms field values using inflection operations (snake_case, camelCase, slugify, plural, singular, etc). Attaches BEFORE INSERT and BEFORE UPDATE triggers. References fields by name in data jsonb.', '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the field to transform"},"ops":{"type":"array","items":{"type":"string","enum":["plural","singular","camel","pascal","dashed","slugify","underscore","lower","upper"]},"description":"Inflection operations to apply in order"}},"required":["field_name","ops"]}'::jsonb, CAST('{"transform","behavior"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataInheritFromParent', 'data_inherit_from_parent', 'data', 'Inherit From Parent', 'BEFORE INSERT trigger that copies specified fields from a parent table via a foreign key. The parent row is looked up through RLS (SECURITY INVOKER), so the insert fails if the caller cannot see the parent. Used by the storage module to inherit owner_id and is_public from buckets to files.', CAST(E'{"type":"object","properties":{"parent_fk_field":{"type":"string","format":"column-ref","description":"Name of the FK field on this table that references the parent (e.g. bucket_id)"},"fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names to copy from the parent row (e.g. [\\"owner_id\\", \\"is_public\\"])"},"parent_table":{"type":"string","description":"Parent table name (optional fallback if FK not yet registered in metaschema)"},"parent_schema":{"type":"string","description":"Parent table schema (optional, defaults to same schema as child table)"}},"required":["parent_fk_field","fields"]}' AS jsonb), CAST('{"trigger","inheritance","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataJsonb', 'data_jsonb', 'data', 'JSONB Field', 'Adds a JSONB column with optional GIN index for containment queries (@>, ?, ?|, ?&). Standard pattern for semi-structured metadata.', '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the JSONB field","default":"metadata"},"default_value":{"type":"object","description":"Default value as a FieldDefault object","default":{"value":{},"cast":{"name":"jsonb"}}},"is_required":{"type":"boolean","description":"Whether the column has a NOT NULL constraint","default":false},"create_index":{"type":"boolean","description":"Whether to create a GIN index","default":true}}}'::jsonb, CAST('{"jsonb","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataMemberOwner', 'data_member_owner', 'data', 'Member Owner', 'Adds owner_id and entity_id columns with a compound AuthzMemberOwner policy. The actor must own the row (owner_id = current_user_id()) AND be a member of the entity (entity_id in SPRT). Use for private data within an entity scope — e.g., personal chat threads that belong to the company but only the author can see.', '{"type":"object","properties":{"owner_field_name":{"type":"string","format":"column-ref","description":"Column name for the owner reference","default":"owner_id"},"entity_field_name":{"type":"string","format":"column-ref","description":"Column name for the entity reference","default":"entity_id"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds foreign key constraints from owner_id and entity_id to the users table","default":true},"create_index":{"type":"boolean","description":"If true, creates B-tree indexes on the owner and entity columns","default":true},"membership_type":{"type":"integer","description":"Membership type for SPRT resolution. Required for entity-scoped provisioning.","default":null}}}'::jsonb, CAST('{"ownership","membership","security","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataOwnedFields', 'data_owned_fields', 'data', 'Owned Fields', 'Restricts which user can modify specific columns in shared objects. Creates an AFTER UPDATE trigger that throws OWNED_PROPS when a non-owner tries to change protected fields. References fields by name in data jsonb.', CAST('{"type":"object","properties":{"role_key_field_name":{"type":"string","format":"column-ref","description":"Name of the field identifying the owner (e.g. sender_id)"},"protected_field_names":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Names of fields only this owner can modify"}},"required":["role_key_field_name","protected_field_names"]}' AS jsonb), CAST('{"ownership","constraint","behavior"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataOwnershipInEntity', 'data_ownership_in_entity', 'data', 'Ownership In Entity', 'Combines direct ownership with entity scoping. Adds both owner_id and entity_id columns. Enables AuthzDirectOwner, AuthzEntityMembership, and AuthzOrgHierarchy authorization. Particularly useful for OrgHierarchy where a user owns a row (owner_id) within an entity (entity_id), and managers above can see subordinate-owned records via the hierarchy closure table.', '{"type":"object","properties":{"owner_field_name":{"type":"string","format":"column-ref","description":"Column name for the owner reference","default":"owner_id"},"entity_field_name":{"type":"string","format":"column-ref","description":"Column name for the entity reference","default":"entity_id"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds foreign key constraints from owner_id and entity_id to the users table","default":true},"create_index":{"type":"boolean","description":"If true, creates B-tree indexes on the owner and entity columns","default":true}}}'::jsonb, CAST('{"ownership","membership","hierarchy","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataPeoplestamps', 'data_peoplestamps', 'data', 'Peoplestamps', 'Adds user tracking for creates/updates with created_by and updated_by columns.', '{"type":"object","properties":{"created_by_field":{"type":"string","format":"column-ref","description":"Column name for the creating user reference","default":"created_by"},"updated_by_field":{"type":"string","format":"column-ref","description":"Column name for the last-updating user reference","default":"updated_by"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds foreign key constraints from created_by and updated_by to the users table","default":false},"create_index":{"type":"boolean","description":"If true, creates B-tree indexes on the peoplestamp columns","default":true}}}'::jsonb, CAST('{"timestamps","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataPublishable', 'data_publishable', 'data', 'Publishable', 'Adds publish state columns (is_published, published_at) for content visibility. Enables AuthzPublishable and AuthzTemporal authorization.', '{"type":"object","properties":{"is_published_field_name":{"type":"string","format":"column-ref","description":"Column name for the published boolean flag","default":"is_published"},"published_at_field_name":{"type":"string","format":"column-ref","description":"Column name for the publish timestamp","default":"published_at"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true}}}'::jsonb, CAST('{"publishing","temporal","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataRealtime', 'data_realtime', 'data', 'Realtime Subscriptions', 'Creates per-table subscriber tables in subscriptions_public with RLS policies derived from source table SELECT policies. Attaches statement-level triggers to emit changes to subscribers.', CAST('{"type":"object","properties":{"operations":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE","DELETE"]},"description":"Which DML operations to track with emit_change triggers","default":["INSERT","UPDATE","DELETE"]},"subscriber_table_name":{"type":"string","description":"Custom name for the subscriber table (defaults to {source_table}_subscriber)"}}}' AS jsonb), CAST('{"realtime","subscriptions","triggers"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataSlug', 'data_slug', 'data', 'Slug', 'Auto-generates URL-friendly slugs from field values on insert/update. Attaches BEFORE INSERT and BEFORE UPDATE triggers that call inflection.slugify() on the target field. References fields by name in data jsonb.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the field to slugify","default":"slug"},"source_field_name":{"type":"string","format":"column-ref","description":"Optional source field name (defaults to field_name)"}},"required":[]}' AS jsonb), CAST('{"transform","behavior"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataSoftDelete', 'data_soft_delete', 'data', 'Soft Delete', 'Adds soft delete support with deleted_at and is_deleted columns.', '{"type":"object","properties":{"deleted_at_field":{"type":"string","format":"column-ref","description":"Column name for the soft-delete timestamp","default":"deleted_at"},"is_deleted_field":{"type":"string","format":"column-ref","description":"Column name for the soft-delete boolean flag","default":"is_deleted"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true}}}'::jsonb, CAST('{"schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataStatusField', 'data_status_field', 'data', 'Status Field', 'Adds a status column with B-tree index for efficient equality filtering and sorting. Optionally constrains values via CHECK constraint when allowed_values is provided.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the status field","default":"status"},"type":{"type":"object","description":"Column type as a FieldType object","default":{"name":"text"}},"default_value":{"type":"string","description":"Default value expression (e.g., active)"},"is_required":{"type":"boolean","description":"Whether the column has a NOT NULL constraint","default":true},"allowed_values":{"type":"array","items":{"type":"string"},"description":"If provided, creates a CHECK constraint restricting the column to these values"}}}' AS jsonb), CAST('{"status","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataTags', 'data_tags', 'data', 'Tags', 'Adds a citext[] tags column with GIN index for efficient array containment queries (@>, &&). Standard tagging pattern for categorization and filtering.', '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the tags array","default":"tags"},"default_value":{"type":"object","description":"Default value as a FieldDefault object","default":{"value":[],"cast":{"name":"citext","array_dimensions":1}}},"is_required":{"type":"boolean","description":"Whether the column has a NOT NULL constraint","default":false}}}'::jsonb, CAST('{"tags","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataTimestamps', 'data_timestamps', 'data', 'Timestamps', 'Adds automatic timestamp tracking with created_at and updated_at columns.', '{"type":"object","properties":{"created_at_field":{"type":"string","format":"column-ref","description":"Column name for the creation timestamp","default":"created_at"},"updated_at_field":{"type":"string","format":"column-ref","description":"Column name for the last-updated timestamp","default":"updated_at"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true}}}'::jsonb, CAST('{"timestamps","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('EventReferral', 'event_referral', 'event', 'Event Referral', 'Creates triggers that record events for the referrer (inviter) when their invitees perform actions on a watched table. Resolves the referrer automatically via the invites module''s claimed_invites table using the membership_type context. Supports the same compound condition system as EventTracker. Use with achievements to unlock levels and grant credits based on invitee activity.', CAST(E'{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"event_name":{"type":"string","description":"Event type name to record for the referrer (e.g., \\"invitee_uploaded_avatar\\", \\"invitee_completed_onboarding\\")"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE","DELETE"]},"description":"DML events that trigger recording","default":["INSERT"]},"actor_field":{"type":"string","format":"column-ref","description":"Column containing the invitee (actor) ID on the source table — used to look up the referrer via claimed_invites.receiver_id","default":"owner_id"},"entity_field":{"type":"string","format":"column-ref","description":"Column containing the entity ID (org/group) for entity-scoped referral events. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup. Omit for user-only events."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]},"max_depth":{"type":"integer","description":"Maximum depth to walk up the invite chain. Default 1 (direct inviter only). Set 2–10 to enable multi-level referral rewards. App-level only — must not be combined with entity_field.","default":1,"minimum":1,"maximum":10},"auto_register_type":{"type":"boolean","description":"Automatically register the event_name in event_types during provisioning","default":true},"condition_field":{"type":"string","format":"column-ref","description":"Column name for conditional WHEN clause (fires only when field equals condition_value)"},"condition_value":{"type":"string","description":"Value to compare against condition_field in WHEN clause"},"conditions":{"description":"Compound conditions for the trigger WHEN clause. Accepts a single leaf condition, an array of conditions (implicitly AND), or a nested combinator tree ({AND: [...], OR: [...], NOT: {...}}). Each leaf is {field, op, value?, row?, ref?}. Column types are resolved automatically from the table schema. Cannot be combined with condition_field or watch_fields.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"watch_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"For UPDATE triggers, only fire when these fields change (uses DISTINCT FROM)"}},"required":["event_name"]}' AS jsonb), CAST('{"events","referral","invites","analytics","tracking"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('EventTracker', 'event_tracker', 'event', 'Event Tracker', 'Creates triggers that record events via the events module when table rows change. Supports the same compound condition system as JobTrigger (condition_field, watch_fields, or full AND/OR/NOT conditions). Events are recorded to app_events and aggregated automatically. Use with achievements (blueprint-level) to unlock levels and grant credits based on event accumulation.', CAST(E'{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"event_name":{"type":"string","description":"Event type name to record (e.g., \\"avatar_uploaded\\", \\"order_completed\\")"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE","DELETE"]},"description":"DML events that trigger recording","default":["INSERT"]},"count":{"type":"integer","description":"Number of events to record per trigger fire","default":1},"toggle":{"type":"boolean","description":"Toggle mode: records event when condition is met, removes when condition is unmet","default":false},"actor_field":{"type":"string","format":"column-ref","description":"Column containing the actor (user) ID to attribute the event to","default":"owner_id"},"entity_field":{"type":"string","format":"column-ref","description":"Column containing the entity ID (org/group) for entity-scoped events. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup. Omit for user-only events."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]},"auto_register_type":{"type":"boolean","description":"Automatically register the event_name in event_types during provisioning","default":true},"condition_field":{"type":"string","format":"column-ref","description":"Column name for conditional WHEN clause (fires only when field equals condition_value)"},"condition_value":{"type":"string","description":"Value to compare against condition_field in WHEN clause"},"conditions":{"description":"Compound conditions for the trigger WHEN clause. Accepts a single leaf condition, an array of conditions (implicitly AND), or a nested combinator tree ({AND: [...], OR: [...], NOT: {...}}). Each leaf is {field, op, value?, row?, ref?}. Column types are resolved automatically from the table schema. Cannot be combined with condition_field or watch_fields.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"watch_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"For UPDATE triggers, only fire when these fields change (uses DISTINCT FROM)"}},"required":["event_name"]}' AS jsonb), CAST('{"events","triggers","analytics","tracking"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('JobTrigger', 'data_job_trigger', 'job', 'Job Trigger', 'Dynamically creates PostgreSQL triggers that enqueue jobs via app_jobs.add_job() when table rows are inserted, updated, or deleted. Supports configurable payload strategies (full row, row ID, selected fields, or custom mapping), conditional firing via WHEN clauses, watched field changes, and extended job options (queue, priority, delay, max attempts).', CAST(E'{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"task_identifier":{"type":"string","format":"function-ref","description":"Job task identifier passed to add_job (e.g., process_invoice, sync_to_stripe). Must match a registered function definition when function_module is installed."},"payload_strategy":{"type":"string","enum":["row","row_id","fields","custom"],"description":"How to build the job payload: row (full NEW/OLD), row_id (just id), fields (selected columns), custom (mapped columns)","default":"row_id"},"payload_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Column names to include in payload (only for fields strategy)"},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Key-to-column mapping for custom payload (e.g., {\\"invoice_id\\": \\"id\\", \\"total\\": \\"amount\\"})"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE","DELETE"]},"description":"Trigger events to create","default":["INSERT","UPDATE"]},"include_old":{"type":"boolean","description":"Include OLD row in payload (for UPDATE triggers)","default":false},"include_meta":{"type":"boolean","description":"Include table/schema metadata in payload","default":false},"condition_field":{"type":"string","format":"column-ref","description":"Column name for conditional WHEN clause (fires only when field equals condition_value)"},"condition_value":{"type":"string","description":"Value to compare against condition_field in WHEN clause"},"conditions":{"description":"Compound conditions for the trigger WHEN clause. Accepts a single leaf condition, an array of conditions (implicitly AND), or a nested combinator tree ({AND: [...], OR: [...], NOT: {...}}). Each leaf is {field, op, value?, row?, ref?}. Column types are resolved automatically from the table schema. Cannot be combined with condition_field or watch_fields.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"watch_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"For UPDATE triggers, only fire when these fields change (uses DISTINCT FROM)"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the trigger table that holds (or references) the entity_id for billing scope. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]},"job_key":{"type":"string","description":"Static job key for upsert semantics (prevents duplicate jobs)"},"queue_name":{"type":"string","description":"Job queue name for routing to specific workers"},"priority":{"type":"integer","description":"Job priority (lower = higher priority)","default":0},"run_at_delay":{"type":"string","description":"Delay before job runs as PostgreSQL interval (e.g., 30 seconds, 5 minutes)"},"max_attempts":{"type":"integer","description":"Maximum retry attempts for the job","default":25}},"required":["task_identifier"]}' AS jsonb), CAST('{"jobs","triggers","async"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('LimitEnforceAggregate', 'limit_enforce_aggregate', 'limit_enforce', 'Enforce Aggregate Counter', 'Declaratively attaches aggregate limit-tracking triggers to a table. On INSERT the named limit is incremented per entity; on DELETE it is decremented. Uses org_limit_aggregates_inc/dec for per-entity (org-level) aggregate limits rather than per-user limits. Requires a provisioned limits_module for the target database.', CAST(E'{"type":"object","properties":{"limit_name":{"type":"string","description":"Name of the aggregate limit to track (must match a default_limits entry, e.g. \\"databases\\", \\"members\\")"},"scope":{"type":"string","description":"Membership type prefix that determines which limits_module row to use. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \\"org\\", \\"data_room\\", \\"channel\\", \\"team\\").","default":"org"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for aggregate limit lookup. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]},"events":{"type":"array","items":{"type":"string","enum":["INSERT","DELETE","UPDATE"]},"description":"Which DML events to attach triggers for","default":["INSERT","DELETE"]}},"required":["limit_name"]}' AS jsonb), CAST('{"limits","triggers","aggregates","enforce"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('LimitEnforceCounter', 'limit_enforce_counter', 'limit_enforce', 'Enforce Counter', 'Declaratively attaches limit-tracking triggers to a table. On INSERT the named limit is incremented; on DELETE it is decremented. Requires a provisioned limits_module for the target scope.', CAST(E'{"type":"object","properties":{"limit_name":{"type":"string","description":"Name of the limit to track (must match a default_limits entry, e.g. \\"projects\\", \\"members\\")"},"scope":{"type":"string","description":"Membership type prefix that determines which limits_module row to use. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \\"app\\", \\"org\\", \\"data_room\\", \\"channel\\", \\"team\\").","default":"app"},"actor_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the actor or entity id used for limit lookup","default":"owner_id"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for entity context resolution. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]},"events":{"type":"array","items":{"type":"string","enum":["INSERT","DELETE","UPDATE"]},"description":"Which DML events to attach triggers for","default":["INSERT","DELETE"]}},"required":["limit_name"]}' AS jsonb), CAST('{"limits","triggers","enforce"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('LimitEnforceFeature', 'limit_enforce_feature', 'limit_enforce', 'Enforce Feature Flag', 'Gates a table behind a feature flag backed by the cap tables. Attaches a BEFORE INSERT trigger that checks whether the named feature cap value is > 0. Features are modeled as caps with max=0 (disabled) or max=1 (enabled) in limit_caps / limit_caps_defaults tables. Resolution: COALESCE(per-entity cap, scope default, 0).', CAST(E'{"type":"object","properties":{"feature_name":{"type":"string","description":"Cap name representing this feature (must match a limit_caps_defaults entry with max=0 or max=1)"},"scope":{"type":"string","description":"Membership type prefix that determines which limits_module row to use. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \\"app\\", \\"org\\", \\"data_room\\", \\"channel\\", \\"team\\").","default":"app"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for per-entity cap lookups (only used for org scope). For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]}},"required":["feature_name"]}' AS jsonb), CAST('{"limits","triggers","feature-flags","enforce","caps"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('LimitEnforceRate', 'limit_enforce_rate', 'limit_enforce', 'Enforce Rate Limit', 'Attaches a BEFORE trigger that calls check_rate_limit() to enforce sliding-window rate limits before allowing mutations. The function checks all three scopes (entity, actor-in-entity, actor) in a single call; which scopes are actually enforced is controlled by what rows exist in rate_window_limits (plan-based config). Requires a provisioned meter_rate_limits_module and billing_module for the target database.', CAST(E'{"type":"object","properties":{"meter_slug":{"type":"string","description":"Slug of the billing meter to check rate limits against (must match a meters table entry, e.g. \\"messaging\\", \\"inference\\")"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for rate limiting. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]},"actor_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the actor id (user) for rate limiting","default":"owner_id"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Which DML events to enforce rate limits on (DELETE is excluded since it reduces load)","default":["INSERT"]}},"required":["meter_slug"]}' AS jsonb), CAST('{"rate-limits","triggers","enforce","metering","abuse-protection"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('LimitTrackUsage', 'limit_track_usage', 'limit_track', 'Track Usage', 'Declaratively attaches billing usage-recording triggers to a table. On INSERT the named meter is incremented via record_usage; on DELETE it is decremented (reversal). On UPDATE, if the entity_field changes, the old entity is decremented and the new entity is incremented. Requires a provisioned billing_module for the target database.', CAST(E'{"type":"object","properties":{"meter_slug":{"type":"string","description":"Slug of the billing meter to record usage against (must match a meters table entry, e.g. \\"databases\\", \\"seats\\")"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for billing. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]},"quantity":{"type":"integer","description":"Units to record per event (default 1)","default":1},"events":{"type":"array","items":{"type":"string","enum":["INSERT","DELETE","UPDATE"]},"description":"Which DML events to attach triggers for","default":["INSERT","DELETE"]}},"required":["meter_slug"]}' AS jsonb), CAST('{"billing","triggers","metering","usage","track"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('LimitWarningAggregate', 'limit_warning_aggregate', 'limit_warning', 'Warning Aggregate', 'Attaches an AFTER INSERT trigger that checks if the entity''s aggregate usage has crossed any warning threshold configured in the limit_warnings table. If a threshold is reached for the first time, enqueues a background job (e.g. email notification). Uses limit_warning_state for one-time dedup per warning/actor/entity triple. Requires a provisioned limits_module with limit_warnings and aggregate limits enabled.', CAST(E'{"type":"object","properties":{"limit_name":{"type":"string","description":"Name of the aggregate limit to watch (must match a limit_warnings.name entry, e.g. \\"databases\\", \\"members\\")"},"scope":{"type":"string","description":"Membership type prefix that determines which limits_module row to use. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \\"org\\", \\"data_room\\", \\"channel\\", \\"team\\").","default":"org"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for aggregate limit lookup. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]}},"required":["limit_name"]}' AS jsonb), CAST('{"limits","triggers","aggregates","warning","notifications"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('LimitWarningCounter', 'limit_warning_counter', 'limit_warning', 'Warning Counter', 'Attaches an AFTER INSERT trigger that checks if the actor''s current usage has crossed any warning threshold configured in the limit_warnings table. If a threshold is reached for the first time, enqueues a background job (e.g. email notification). Uses limit_warning_state for one-time dedup per warning/actor pair. Requires a provisioned limits_module with limit_warnings enabled.', CAST(E'{"type":"object","properties":{"limit_name":{"type":"string","description":"Name of the limit to watch (must match a limit_warnings.name entry, e.g. \\"projects\\", \\"members\\")"},"scope":{"type":"string","description":"Membership type prefix that determines which limits_module row to use. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \\"app\\", \\"org\\", \\"data_room\\", \\"channel\\", \\"team\\").","default":"app"},"actor_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the actor id for limit lookup","default":"owner_id"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id. When provided, entity_id is included in the job payload and dedup state. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]}},"required":["limit_name"]}' AS jsonb), CAST('{"limits","triggers","warning","notifications"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('LimitWarningRate', 'limit_warning_rate', 'limit_warning', 'Warning Rate Limit', 'Attaches an AFTER INSERT trigger that checks if the actor''s current request count in the active sliding window has crossed any warning threshold configured in the limit_warnings table. If a threshold is reached for the first time, enqueues a background job (e.g. email notification). Uses limit_warning_state for one-time dedup per warning/actor pair. Requires both a limits_module with limit_warnings enabled and a rate_limit_meters_module.', CAST(E'{"type":"object","properties":{"meter_slug":{"type":"string","description":"Slug of the billing meter to check rate limits against (must match a meters table entry)"},"scope":{"type":"string","description":"Membership type prefix that determines which limits_module row to use for warnings and warning_state tables. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \\"app\\", \\"org\\", \\"data_room\\", \\"channel\\", \\"team\\").","default":"app"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for rate limit lookup. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]},"actor_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the actor id for rate limit lookup","default":"owner_id"}},"required":["meter_slug"]}' AS jsonb), CAST('{"rate-limits","triggers","warning","notifications","metering"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('ProcessChunks', 'data_chunks', 'process', 'Chunks', 'Creates a chunked-embedding child table for any parent table. Provisions the chunks table with content, chunk_index, embedding vector, metadata, HNSW index, inherited RLS, and optional job trigger for automatic text splitting. Composed internally by ProcessFileEmbedding (enabled by default in extract mode) but can also be used standalone.', CAST(E'{"type":"object","properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"paragraph"},"dimensions":{"type":"integer","description":"Vector dimensions for per-chunk embeddings","default":768},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric for the HNSW index on chunk embeddings","default":"cosine"},"embedding_model":{"type":"string","description":"Embedding model identifier for per-chunk embeddings. When null, the worker falls back to runtime config (llm_module / env vars)."},"embedding_provider":{"type":"string","description":"Embedding provider name (e.g. \\"ollama\\", \\"openai\\"). When null, the worker falls back to runtime config."},"chunks_table_name":{"type":"string","description":"Override the chunks table name. Defaults to {parent_table}_chunks."},"metadata_fields":{"type":"array","items":{"type":"string"},"description":"Field names from the parent table to copy into chunk metadata"},"search_indexes":{"type":"array","items":{"type":"string","enum":["fulltext","bm25","trigram"]},"description":"Text search indexes to create on the chunks content column. Omit to mirror the parent table''s text search indexes. Set explicitly to override (e.g. [\\"fulltext\\", \\"bm25\\"])."},"entity_field":{"type":"string","format":"column-ref","description":"Column on the parent table that holds (or references) the entity_id for billing scope. Forwarded to the chunking job trigger."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Forwarded to the chunking job trigger.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from"},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, optional)"},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id"}},"required":["obj_table","obj_field"]},"enqueue_chunking_job":{"type":"boolean","description":"Whether to create a job trigger that auto-enqueues chunking on parent INSERT/UPDATE","default":true},"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"}}}' AS jsonb), CAST('{"embedding","chunks","vector","ai","rag"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('ProcessExtraction', 'process_extraction', 'process', 'File Extraction', 'Creates extraction output fields and a job trigger for file text extraction. Fires when a file is uploaded (status = ''uploaded'') or on INSERT. The external worker extracts text/metadata from the file (PDF, DOCX, HTML, etc.) and writes the result back to the configured output fields. Typically used upstream of ProcessFileEmbedding or ProcessChunks.', CAST(E'{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"text_field":{"type":"string","format":"column-ref","description":"Field to store extracted text/markdown","default":"extracted_text"},"metadata_field":{"type":"string","format":"column-ref","description":"JSONB field for extraction metadata (page count, language, etc.)","default":"extracted_metadata"},"extraction_model":{"type":"string","description":"Extraction model identifier (e.g. a vision model for OCR, an LLM for structured extraction). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config."},"extraction_provider":{"type":"string","description":"Extraction provider name (e.g. \\"ollama\\", \\"openai\\"). When null, the worker falls back to runtime config."},"mime_patterns":{"type":"array","items":{"type":"string"},"description":"MIME type LIKE patterns to match. Multiple patterns are OR''d together. Examples: [''application/pdf'', ''text/%''], [''application/vnd.openxmlformats%''].","default":["application/pdf","text/%"]},"task_identifier":{"type":"string","description":"Job task identifier for the extraction worker","default":"extract_file_text"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Trigger events that fire the job","default":["INSERT"]},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Custom payload key-to-column mapping for the job trigger","default":{"file_id":"id","key":"key","mime_type":"mime_type","bucket_id":"bucket_id"}},"trigger_conditions":{"description":"Additional compound conditions beyond auto-generated filtering. Merged with the auto-generated conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"entity_field":{"type":"string","format":"column-ref","description":"Column on the trigger table that holds (or references) the entity_id for billing scope. Forwarded to the composed JobTrigger."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Forwarded to the composed JobTrigger.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from"},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, optional)"},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id"}},"required":["obj_table","obj_field"]},"queue_name":{"type":"string","description":"Job queue name for extraction tasks","default":"extraction"},"max_attempts":{"type":"integer","description":"Maximum number of retry attempts","default":5},"priority":{"type":"integer","description":"Job priority (lower = higher priority)","default":0}}}' AS jsonb), CAST('{"extraction","files","processing","jobs","text"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('ProcessFileEmbedding', 'data_file_embedding', 'process', 'File Embedding', 'Generic, MIME-scoped embedding node for file tables. Supports two modes: direct (whole-file to single vector, e.g. CLIP for images) when extraction is omitted, or extract (file to text to chunks to per-chunk vectors) when extraction config is provided. Composes SearchVector + JobTrigger + ProcessChunks (enabled by default in extract mode) internally. Multiple instances can coexist on the same table with different MIME scopes, field names, and embedding strategies.', CAST(E'{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the vector embedding column","default":"embedding"},"dimensions":{"type":"integer","description":"Vector dimensions (e.g. 512 for CLIP, 768 for nomic, 1536 for ada-002)","default":768},"index_method":{"type":"string","enum":["hnsw","ivfflat"],"description":"Index type for similarity search","default":"hnsw"},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric","default":"cosine"},"index_options":{"type":"object","description":"Index-specific options. HNSW: {m, ef_construction}. IVFFlat: {lists}.","default":{}},"embedding_model":{"type":"string","description":"Embedding model identifier (e.g. \\"nomic-embed-text\\", \\"text-embedding-3-small\\", \\"clip-vit-base-patch32\\"). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config (llm_module / env vars)."},"embedding_provider":{"type":"string","description":"Embedding provider name (e.g. \\"ollama\\", \\"openai\\"). When null, the worker falls back to runtime config."},"mime_patterns":{"type":"array","items":{"type":"string"},"description":"MIME type LIKE patterns to match. Multiple patterns are OR''d together. Examples: [''image/%''], [''application/pdf'', ''text/%''], [''audio/%''].","default":["image/%"]},"task_identifier":{"type":"string","description":"Job task identifier for the worker. In direct mode this is the embedding worker; in extract mode this is the extraction worker.","default":"process_file_embedding"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Trigger events that fire the job","default":["INSERT"]},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Custom payload key-to-column mapping for the job trigger","default":{"file_id":"id","key":"key","mime_type":"mime_type","bucket_id":"bucket_id"}},"trigger_conditions":{"description":"Additional compound conditions beyond auto-generated filtering. Merged with the auto-generated conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"entity_field":{"type":"string","format":"column-ref","description":"Column on the trigger table that holds (or references) the entity_id for billing scope. Forwarded to the composed JobTrigger."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Forwarded to the composed JobTrigger.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from"},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, optional)"},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id"}},"required":["obj_table","obj_field"]},"extraction":{"type":"object","description":"Text extraction configuration. When present, the generator creates extraction output fields on the table and configures SearchVector with source_fields + stale tracking. When absent, the node operates in direct mode (single vector per file, no text extraction).","properties":{"text_field":{"type":"string","format":"column-ref","description":"Field to store extracted text/markdown","default":"extracted_text"},"metadata_field":{"type":"string","format":"column-ref","description":"JSONB field for extraction metadata (page count, language, etc.)","default":"extracted_metadata"}}},"include_chunks":{"type":"boolean","description":"Whether to create a chunks table via ProcessChunks. Defaults to true when extraction is provided, false in direct mode. Set explicitly to override."},"chunks":{"type":"object","description":"Chunking configuration passed through to ProcessChunks. When include_chunks is true (or defaults to true in extract mode), these params configure the chunks table, embedding dimensions, strategy, etc.","default":{},"properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"paragraph"},"metadata_fields":{"type":"array","items":{"type":"string"},"description":"Field names from parent to copy into chunk metadata"},"search_indexes":{"type":"array","items":{"type":"string","enum":["fulltext","bm25","trigram"]},"description":"Text search indexes to create on the chunks content column. Omit to mirror the parent table''s text search indexes. Set explicitly to override."},"enqueue_chunking_job":{"type":"boolean","description":"Whether to auto-enqueue a chunking job on insert/update","default":true},"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"}}}}}' AS jsonb), CAST('{"embedding","vector","ai","composition","jobs","multimodal","files"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('ProcessImageEmbedding', 'data_image_embedding', 'process', 'Image Embedding', 'Image-specific preset of ProcessFileEmbedding. Delegates to ProcessFileEmbedding with image-oriented defaults: dimensions=512 (CLIP), mime_patterns=[''image/%''], task_identifier=''process_image_embedding'', direct mode (no extraction). Accepts all ProcessFileEmbedding parameters — any overrides are forwarded through.', CAST(E'{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the vector embedding column","default":"embedding"},"dimensions":{"type":"integer","description":"Vector dimensions (default 512 for CLIP-style image embeddings)","default":512},"index_method":{"type":"string","enum":["hnsw","ivfflat"],"description":"Index type for similarity search","default":"hnsw"},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric","default":"cosine"},"index_options":{"type":"object","description":"Index-specific options. HNSW: {m, ef_construction}. IVFFlat: {lists}.","default":{}},"embedding_model":{"type":"string","description":"Embedding model identifier (e.g. \\"clip-vit-base-patch32\\"). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config (llm_module / env vars)."},"embedding_provider":{"type":"string","description":"Embedding provider name (e.g. \\"ollama\\", \\"openai\\"). When null, the worker falls back to runtime config."},"mime_patterns":{"type":"array","items":{"type":"string"},"description":"MIME type LIKE patterns to match. Multiple patterns are OR''d together.","default":["image/%"]},"task_identifier":{"type":"string","description":"Job task identifier for the image embedding worker","default":"process_image_embedding"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Trigger events that fire the job","default":["INSERT"]},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Custom payload key-to-column mapping for the job trigger","default":{"file_id":"id","key":"key","mime_type":"mime_type","bucket_id":"bucket_id"}},"trigger_conditions":{"description":"Additional compound conditions beyond auto-generated filtering. Merged with the auto-generated conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"entity_field":{"type":"string","format":"column-ref","description":"Column on the trigger table that holds (or references) the entity_id for billing scope. Forwarded to the composed JobTrigger."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Forwarded to the composed JobTrigger.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from"},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, optional)"},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id"}},"required":["obj_table","obj_field"]},"extraction":{"type":"object","description":"Text extraction configuration. Forwarded to ProcessFileEmbedding. When present, enables extract mode (e.g., OCR for images).","properties":{"text_field":{"type":"string","format":"column-ref","description":"Field to store extracted text","default":"extracted_text"},"metadata_field":{"type":"string","format":"column-ref","description":"JSONB field for extraction metadata","default":"extracted_metadata"}}},"chunks":{"type":"object","description":"Chunking configuration. Forwarded to ProcessFileEmbedding. Only meaningful when extraction is also provided.","properties":{"content_field_name":{"type":"string","format":"column-ref","default":"content"},"chunk_size":{"type":"integer","default":1000},"chunk_overlap":{"type":"integer","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"default":"paragraph"},"metadata_fields":{"type":"object"},"enqueue_chunking_job":{"type":"boolean","default":true},"chunking_task_name":{"type":"string","default":"generate_chunks"}}}}}' AS jsonb), CAST('{"embedding","image","vector","ai","composition","jobs"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('ProcessImageVersions', 'process_image_versions', 'process', 'Image Versions', 'Creates a job trigger for image variant generation. Fires when an image file is uploaded (status = ''uploaded'') or on INSERT. The external worker generates resized, cropped, or reformatted versions (thumbnails, previews, WebP conversions, etc.) and stores them as new file records linked to the source image.', CAST(E'{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"required":["versions"],"properties":{"versions":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Version identifier (e.g., \\"thumb\\", \\"preview\\", \\"hero\\")"},"width":{"type":"integer","description":"Target width in pixels"},"height":{"type":"integer","description":"Target height in pixels"},"fit":{"type":"string","enum":["cover","contain","fill","inside","outside"],"description":"Resize fitting strategy","default":"cover"},"format":{"type":"string","enum":["jpeg","png","webp","avif"],"description":"Output image format","default":"webp"},"quality":{"type":"integer","description":"Output quality (1-100)","default":80}},"required":["name"]},"description":"Array of version definitions. Each version specifies dimensions, format, and quality for a generated image variant. Required — the blueprint must explicitly define what variants to generate.","minItems":1},"mime_patterns":{"type":"array","items":{"type":"string"},"description":"MIME type LIKE patterns to match. Defaults to all image types.","default":["image/%"]},"task_identifier":{"type":"string","description":"Job task identifier for the image processing worker","default":"process_image_versions"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Trigger events that fire the job","default":["INSERT"]},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Custom payload key-to-column mapping for the job trigger","default":{"file_id":"id","key":"key","mime_type":"mime_type","bucket_id":"bucket_id"}},"trigger_conditions":{"description":"Additional compound conditions beyond auto-generated filtering. Merged with the auto-generated conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"entity_field":{"type":"string","format":"column-ref","description":"Column on the trigger table that holds (or references) the entity_id for billing scope. Forwarded to the composed JobTrigger."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Forwarded to the composed JobTrigger.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from"},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, optional)"},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id"}},"required":["obj_table","obj_field"]},"queue_name":{"type":"string","description":"Job queue name for image processing tasks","default":"image_processing"},"max_attempts":{"type":"integer","description":"Maximum number of retry attempts","default":5},"priority":{"type":"integer","description":"Job priority (lower = higher priority)","default":0}}}' AS jsonb), CAST('{"images","processing","jobs","resize","thumbnails","files"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('RelationBelongsTo', 'relation_belongs_to', 'relation', 'Belongs To', 'Creates a foreign key field on the source table referencing the target table. Auto-derives the FK field name from the target table name using inflection (e.g., projects derives project_id). delete_action is required and must be explicitly provided by the caller.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"Table that will have the FK field added"},"target_table_id":{"type":"string","format":"uuid","description":"Table being referenced by the FK"},"field_name":{"type":"string","format":"column-ref","description":"FK field name on the source table. Auto-derived from target table name if omitted (e.g., projects → project_id)"},"delete_action":{"type":"string","enum":["c","r","n","d","a"],"description":"FK delete action: c=CASCADE, r=RESTRICT, n=SET NULL, d=SET DEFAULT, a=NO ACTION. Required."},"is_required":{"type":"boolean","description":"Whether the FK field is NOT NULL","default":true}},"required":["source_table_id","target_table_id","delete_action"]}' AS jsonb), CAST('{"relation","foreign_key","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('RelationHasMany', 'relation_has_many', 'relation', 'Has Many', 'Creates a foreign key field on the target table referencing the source table. Inverse of RelationBelongsTo — same FK, different perspective. "projects has many tasks" creates tasks.project_id. Auto-derives the FK field name from the source table name using inflection. delete_action is required and must be explicitly provided by the caller.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"Parent table being referenced by the FK (e.g., projects in projects has many tasks)"},"target_table_id":{"type":"string","format":"uuid","description":"Child table that receives the FK field (e.g., tasks in projects has many tasks)"},"field_name":{"type":"string","format":"column-ref","description":"FK field name on the target table. Auto-derived from source table name if omitted (e.g., projects derives project_id)"},"delete_action":{"type":"string","enum":["c","r","n","d","a"],"description":"FK delete action: c=CASCADE, r=RESTRICT, n=SET NULL, d=SET DEFAULT, a=NO ACTION. Required."},"is_required":{"type":"boolean","description":"Whether the FK field is NOT NULL","default":true}},"required":["source_table_id","target_table_id","delete_action"]}' AS jsonb), CAST('{"relation","foreign_key","has_many","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('RelationHasOne', 'relation_has_one', 'relation', 'Has One', 'Creates a foreign key field with a unique constraint on the source table referencing the target table. Enforces 1:1 cardinality. Auto-derives the FK field name from the target table name using inflection. delete_action is required and must be explicitly provided by the caller.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"Table that will have the FK field and unique constraint"},"target_table_id":{"type":"string","format":"uuid","description":"Table being referenced by the FK"},"field_name":{"type":"string","format":"column-ref","description":"FK field name on the source table. Auto-derived from target table name if omitted (e.g., users → user_id)"},"delete_action":{"type":"string","enum":["c","r","n","d","a"],"description":"FK delete action: c=CASCADE, r=RESTRICT, n=SET NULL, d=SET DEFAULT, a=NO ACTION. Required."},"is_required":{"type":"boolean","description":"Whether the FK field is NOT NULL","default":true}},"required":["source_table_id","target_table_id","delete_action"]}' AS jsonb), CAST('{"relation","foreign_key","unique","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('RelationManyToMany', 'relation_many_to_many', 'relation', 'Many to Many', 'Creates a junction table between source and target tables with auto-derived naming and FK fields. The trigger creates a bare table (no implicit DataId), adds FK fields to both tables, optionally creates a composite PK (use_composite_key), then forwards all security config to secure_table_provision as-is. The trigger never injects values the caller did not provide. Junction table FKs always CASCADE on delete.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"First table in the M:N relationship"},"target_table_id":{"type":"string","format":"uuid","description":"Second table in the M:N relationship"},"junction_table_id":{"type":"string","format":"uuid","description":"Existing junction table to use. If uuid_nil(), a new bare table is created"},"junction_table_name":{"type":"string","description":"Junction table name. Auto-derived from both table names if omitted (e.g., projects + tags derives project_tags)"},"source_field_name":{"type":"string","format":"column-ref","description":"FK field name on junction for source table. Auto-derived if omitted (e.g., projects derives project_id)"},"target_field_name":{"type":"string","format":"column-ref","description":"FK field name on junction for target table. Auto-derived if omitted (e.g., tags derives tag_id)"},"use_composite_key":{"type":"boolean","description":"When true, creates a composite PK from the two FK fields. When false, no PK is created by the trigger (use nodes with DataId for UUID PK). Mutually exclusive with nodes containing DataId.","default":false},"nodes":{"type":"array","items":{"type":"object"},"description":"Array of node objects for field creation on junction table. Each object has a $type key (e.g. DataId, DataEntityMembership) and optional data keys. Forwarded to secure_table_provision as-is. Empty array means no additional fields."},"grants":{"type":"array","items":{"type":"object","properties":{"roles":{"type":"array","items":{"type":"string"}},"privileges":{"type":"array","items":{"type":"array","items":{"type":"string"}}}},"required":["roles","privileges"]},"description":"Unified grant objects for the junction table. Each entry is { roles: string[], privileges: string[][] }. Forwarded to secure_table_provision as-is. Default: []"},"policies":{"type":"array","items":{"type":"object","properties":{"$type":{"type":"string"},"data":{"type":"object"},"privileges":{"type":"array","items":{"type":"string"}},"policy_role":{"type":"string"},"permissive":{"type":"boolean"},"policy_name":{"type":"string"}},"required":["$type"]},"description":"RLS policy objects for the junction table. Each entry has $type (Authz* generator), optional data, privileges, policy_role, permissive, policy_name. Forwarded to secure_table_provision as-is. Default: []"}},"required":["source_table_id","target_table_id"]}' AS jsonb), CAST('{"relation","junction","many_to_many","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('RelationSpatial', 'relation_spatial', 'relation', 'Spatial Relation', 'Declares a spatial predicate between two existing geometry/geography columns. Inserts a metaschema_public.spatial_relation row; the sync_spatial_relation_tags trigger then projects a @spatialRelation smart tag onto the owner column so graphile-postgis'' PostgisSpatialRelationsPlugin can expose it as a cross-table filter in GraphQL. Metadata-only: both source_field and target_field must already exist on their tables. Idempotent on (source_table_id, name). One direction per tag — author two RelationSpatial entries if symmetry is desired.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"Table that owns the relation (the @spatialRelation tag is emitted on the owner column of this table)"},"source_field_id":{"type":"string","format":"uuid","description":"Geometry/geography column on source_table that carries the @spatialRelation smart tag"},"target_table_id":{"type":"string","format":"uuid","description":"Table being referenced by the spatial predicate"},"target_field_id":{"type":"string","format":"uuid","description":"Geometry/geography column on target_table that the predicate is evaluated against"},"name":{"type":"string","description":"Relation name (stable, snake_case). Becomes the generated filter field name in GraphQL (e.g. nearby_clinic). Unique per (source_table_id, name) — idempotency key."},"operator":{"type":"string","enum":["st_contains","st_within","st_intersects","st_covers","st_coveredby","st_overlaps","st_touches","st_dwithin"],"description":"PostGIS spatial predicate. One of the 8 whitelisted operators. st_dwithin requires param_name."},"param_name":{"type":"string","description":"Parameter name for parametric operators (currently only st_dwithin, which needs a distance argument). Must be NULL for all other operators. Enforced by table CHECK."}},"required":["source_table_id","source_field_id","target_table_id","target_field_id","name","operator"]}' AS jsonb), CAST('{"relation","spatial","postgis","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('SearchBm25', 'search_bm25', 'search', 'BM25 Search', 'Creates a BM25 index on an existing text column using pg_textsearch. Enables statistical relevance ranking with configurable k1 and b parameters. The BM25 index is auto-detected by graphile-search.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of existing text column to index with BM25"},"text_config":{"type":"string","description":"PostgreSQL text search configuration for BM25","default":"english"},"k1":{"type":"number","description":"BM25 k1 parameter: term frequency saturation (typical: 1.2-2.0)","default":null},"b":{"type":"number","description":"BM25 b parameter: document length normalization (0=none, 1=full, typical: 0.75)","default":null},"search_score_weight":{"type":"number","description":"Weight for this algorithm in composite searchScore","default":1}},"required":["field_name"]}' AS jsonb), CAST('{"search","bm25","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('SearchFullText', 'search_full_text', 'search', 'Full-Text Search', 'Adds a tsvector column with GIN index and automatic trigger population from source fields. Enables PostgreSQL full-text search with configurable weights and language support. Leverages the existing metaschema full_text_search infrastructure.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the tsvector column","default":"search"},"source_fields":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","format":"column-ref","description":"Name of the source column"},"weight":{"type":"string","enum":["A","B","C","D"],"description":"tsvector weight class (A=highest, D=lowest)","default":"D"},"lang":{"type":"string","description":"PostgreSQL text search configuration","default":"english"}},"required":["field"]},"description":"Source columns that feed the tsvector. Each has a field name, weight (A-D), and language config."},"lang_column":{"type":"string","format":"column-ref","description":"Column name whose value determines the text search configuration per row. When set, the tsvector trigger uses NEW.::regconfig instead of a static language, enabling dynamic per-row language stemming. The per-field lang values in source_fields are used as fallback defaults for the langs array but the trigger reads from this column at runtime."},"search_score_weight":{"type":"number","description":"Weight for this algorithm in composite searchScore","default":1}},"required":["source_fields"]}' AS jsonb), CAST('{"search","fts","tsvector","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('SearchSpatial', 'search_spatial', 'search', 'Spatial Search', 'Adds a PostGIS geometry or geography column with a spatial index (GiST or SP-GiST). Supports configurable geometry types (Point, Polygon, etc.), SRID, and dimensionality. The graphile-postgis plugin auto-detects geometry/geography columns by codec type for spatial filtering (ST_Contains, ST_DWithin, bbox operators).', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the geometry/geography column","default":"geom"},"geometry_type":{"type":"string","enum":["Point","LineString","Polygon","MultiPoint","MultiLineString","MultiPolygon","GeometryCollection","Geometry"],"description":"PostGIS geometry type constraint","default":"Point"},"srid":{"type":"integer","description":"Spatial Reference System Identifier (e.g. 4326 for WGS84)","default":4326},"dimension":{"type":"integer","enum":[2,3,4],"description":"Coordinate dimension (2=XY, 3=XYZ, 4=XYZM)","default":2},"use_geography":{"type":"boolean","description":"Use geography type instead of geometry (for geodetic calculations on the sphere)","default":false},"index_method":{"type":"string","enum":["gist","spgist"],"description":"Spatial index method","default":"gist"}}}' AS jsonb), CAST('{"spatial","postgis","geometry","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('SearchSpatialAggregate', 'search_spatial_aggregate', 'search', 'Spatial Aggregate Search', 'Creates a derived/materialized geometry field on the parent table that automatically aggregates geometries from a source (child) table via triggers. When child rows are inserted/updated/deleted, the parent aggregate field is recalculated using the specified PostGIS aggregation function (ST_Union, ST_Collect, ST_ConvexHull, ST_ConcaveHull). Useful for materializing spatial boundaries from collections of points or polygons.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the aggregate geometry column on the parent table","default":"geom_aggregate"},"source_table_id":{"type":"string","format":"uuid","description":"UUID of the source (child) table containing individual geometries"},"source_geom_field":{"type":"string","format":"column-ref","description":"Name of the geometry column on the source table","default":"geom"},"source_fk_field":{"type":"string","format":"column-ref","description":"Name of the foreign key column on the source table pointing to the parent"},"aggregate_function":{"type":"string","enum":["union","collect","convex_hull","concave_hull"],"description":"PostGIS aggregation function: union (ST_Union, merges overlapping), collect (ST_Collect, groups without merging), convex_hull (smallest convex polygon), concave_hull (tighter boundary)","default":"union"},"geometry_type":{"type":"string","enum":["Point","LineString","Polygon","MultiPoint","MultiLineString","MultiPolygon","GeometryCollection","Geometry"],"description":"Output geometry type constraint for the aggregate field","default":"MultiPolygon"},"srid":{"type":"integer","description":"Spatial Reference System Identifier (e.g. 4326 for WGS84)","default":4326},"dimension":{"type":"integer","enum":[2,3,4],"description":"Coordinate dimension (2=XY, 3=XYZ, 4=XYZM)","default":2},"use_geography":{"type":"boolean","description":"Use geography type instead of geometry","default":false},"index_method":{"type":"string","enum":["gist","spgist"],"description":"Spatial index method for the aggregate field","default":"gist"}},"required":["source_table_id","source_fk_field"]}' AS jsonb), CAST('{"spatial","postgis","geometry","aggregate","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('SearchTrgm', 'search_trgm', 'search', 'Trigram Search', 'Creates GIN trigram indexes (gin_trgm_ops) on specified text/citext fields for fuzzy LIKE/ILIKE/similarity search. Adds @trgmSearch smart tag for PostGraphile integration. Fields must already exist on the table.', CAST('{"type":"object","properties":{"fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names to create trigram indexes on (fields must already exist on the table)"}},"required":["fields"]}' AS jsonb), CAST('{"search","trigram","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('SearchUnified', 'search_unified', 'search', 'Unified Search', 'Composite node type that orchestrates multiple search modalities (full-text search, BM25, embeddings, trigram) on a single table. Configures per-table search score weights, normalization strategy, and recency boost via the @searchConfig smart tag.', CAST('{"type":"object","properties":{"full_text_search":{"type":"object","description":"SearchFullText parameters. Omit to skip FTS setup.","properties":{"field_name":{"type":"string","format":"column-ref","default":"search"},"source_fields":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","format":"column-ref"},"weight":{"type":"string","enum":["A","B","C","D"]},"lang":{"type":"string"}},"required":["field"]}},"search_score_weight":{"type":"number","default":1}}},"bm25":{"type":"object","description":"SearchBm25 parameters. Omit to skip BM25 setup.","properties":{"field_name":{"type":"string","format":"column-ref"},"text_config":{"type":"string","default":"english"},"k1":{"type":"number"},"b":{"type":"number"},"search_score_weight":{"type":"number","default":1}}},"embedding":{"type":"object","description":"SearchVector parameters. Omit to skip embedding setup.","properties":{"field_name":{"type":"string","format":"column-ref","default":"embedding"},"dimensions":{"type":"integer","default":768},"index_method":{"type":"string","enum":["hnsw","ivfflat"]},"metric":{"type":"string","enum":["cosine","l2","ip"]},"source_fields":{"type":"array","items":{"type":"string","format":"column-ref"}},"embedding_model":{"type":"string","description":"Embedding model identifier. When null, the worker falls back to runtime config."},"embedding_provider":{"type":"string","description":"Embedding provider name. When null, the worker falls back to runtime config."},"search_score_weight":{"type":"number","default":1},"chunks":{"type":"object","description":"Chunking configuration for long-text embedding. Creates an embedding_chunks record that drives automatic text splitting and per-chunk embedding. Omit to skip chunking.","properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"fixed"},"metadata_fields":{"type":"object","description":"Metadata fields from parent to copy into chunks"},"enqueue_chunking_job":{"type":"boolean","description":"Whether to auto-enqueue a chunking job on insert/update","default":true},"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"}}}}},"embedding_text_field":{"type":"string","format":"column-ref","description":"Name of the composite text field created for embedding input","default":"embedding_text"},"composite_format":{"type":"string","enum":["labeled","plain"],"description":"Output format for the composite text field","default":"labeled"},"trgm_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names to tag with @trgmSearch for fuzzy/typo-tolerant matching"},"search_config":{"type":"object","description":"Unified search score configuration written to @searchConfig smart tag","properties":{"weights":{"type":"object","description":"Per-algorithm weights: {tsv: 1.5, bm25: 1.0, pgvector: 0.8, trgm: 0.3}"},"normalization":{"type":"string","enum":["linear","sigmoid"],"description":"Score normalization strategy","default":"linear"},"boost_recent":{"type":"boolean","description":"Enable recency boost for search results","default":false},"boost_recency_field":{"type":"string","format":"column-ref","description":"Timestamp field for recency boost (e.g. created_at, updated_at)"},"boost_recency_decay":{"type":"number","description":"Decay rate for recency boost (0-1, lower = faster decay)","default":0.5}}}}}' AS jsonb), CAST('{"search","composite","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('SearchVector', 'search_vector', 'search', 'Vector Search', 'Adds a vector embedding column with HNSW or IVFFlat index for similarity search. Supports configurable dimensions, distance metrics (cosine, l2, ip), per-field {field_name}_updated_at timestamp tracking (read-only in GraphQL), and automatic job enqueue triggers for embedding generation.', CAST(E'{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the vector column","default":"embedding"},"dimensions":{"type":"integer","description":"Vector dimensions (e.g. 384, 768, 1536, 3072)","default":768},"index_method":{"type":"string","enum":["hnsw","ivfflat"],"description":"Index type for similarity search","default":"hnsw"},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric (cosine, l2, ip)","default":"cosine"},"index_options":{"type":"object","description":"Index-specific options. HNSW: {m, ef_construction}. IVFFlat: {lists}.","default":{}},"source_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Column names that feed the embedding. Used by stale trigger to detect content changes."},"embedding_model":{"type":"string","description":"Embedding model identifier (e.g. \\"nomic-embed-text\\", \\"text-embedding-3-small\\"). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config (llm_module / env vars)."},"embedding_provider":{"type":"string","description":"Embedding provider name (e.g. \\"ollama\\", \\"openai\\"). When null, the worker falls back to runtime config."},"enqueue_job":{"type":"boolean","description":"Auto-create trigger that enqueues embedding generation jobs","default":true},"job_task_name":{"type":"string","format":"function-ref","description":"Task identifier for the job queue. Must match a registered function definition when function_module is installed.","default":"generate_embedding"},"chunks":{"type":"object","description":"Chunking configuration for long-text embedding. Creates an embedding_chunks record that drives automatic text splitting and per-chunk embedding. Omit to skip chunking.","properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"fixed"},"metadata_fields":{"type":"object","description":"Metadata fields from parent to copy into chunks"},"enqueue_chunking_job":{"type":"boolean","description":"Whether to auto-enqueue a chunking job on insert/update","default":true},"chunking_task_name":{"type":"string","format":"function-ref","description":"Task identifier for the chunking job queue. Must match a registered function definition when function_module is installed.","default":"generate_chunks"}}}}}' AS jsonb), CAST('{"embedding","vector","ai","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('ViewAggregated', 'view_aggregated', 'view', 'Aggregated View', 'View with GROUP BY and aggregate functions. Useful for summary/reporting views.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"UUID of the source table"},"group_by_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names to group by"},"aggregates":{"type":"array","items":{"type":"object","properties":{"function":{"type":"string","enum":["COUNT","SUM","AVG","MIN","MAX"]},"field":{"type":"string","format":"column-ref","description":"Field to aggregate (or * for COUNT)"},"alias":{"type":"string","format":"column-ref","description":"Output column name"}},"required":["function","alias"]},"description":"Array of aggregate specifications"}},"required":["source_table_id","group_by_fields","aggregates"]}' AS jsonb), CAST('{"view","aggregate","reporting"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('ViewComposite', 'view_composite', 'view', 'Composite View', 'Advanced view using composite AST for the query. Use when other node types are insufficient (CTEs, UNIONs, complex subqueries, etc.).', CAST('{"type":"object","properties":{"query_ast":{"type":"object","description":"Composite SELECT query AST (JSONB)"}},"required":["query_ast"]}' AS jsonb), CAST('{"view","advanced","composite","ast"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('ViewFilteredTable', 'view_filtered_table', 'view', 'Filtered Table', 'Table projection with an Authz* filter baked into the view definition. The view only returns records matching the filter.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"UUID of the source table"},"filter_type":{"type":"string","description":"Authz* node type name (e.g., AuthzDirectOwner, AuthzPublishable)"},"filter_data":{"type":"object","description":"Parameters for the Authz* filter type"},"field_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Optional array of field UUIDs to include (alternative to field_names)"},"field_names":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Optional array of field names to include (alternative to field_ids)"}},"required":["source_table_id","filter_type"]}' AS jsonb), CAST('{"view","filter","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('ViewJoinedTables', 'view_joined_tables', 'view', 'Joined Tables', 'View that joins multiple tables together. Supports INNER, LEFT, RIGHT, and FULL joins.', CAST('{"type":"object","properties":{"primary_table_id":{"type":"string","format":"uuid","description":"UUID of the primary (left-most) table"},"primary_columns":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Optional array of column names to include from the primary table"},"joins":{"type":"array","items":{"type":"object","properties":{"table_id":{"type":"string","format":"uuid","description":"UUID of the joined table"},"join_type":{"type":"string","enum":["INNER","LEFT","RIGHT","FULL"]},"primary_field":{"type":"string","format":"column-ref","description":"Field on primary table"},"join_field":{"type":"string","format":"column-ref","description":"Field on joined table"},"columns":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Optional column names to include from this joined table"}},"required":["table_id","primary_field","join_field"]},"description":"Array of join specifications"},"field_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Optional array of field UUIDs to include (alternative to per-table columns)"}},"required":["primary_table_id","joins"]}' AS jsonb), CAST('{"view","join"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('ViewTableProjection', 'view_table_projection', 'view', 'Table Projection', 'Simple column selection from a single source table. Projects all or specific fields.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"UUID of the source table to project from"},"field_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Optional array of field UUIDs to include (all fields if omitted)"},"field_names":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Optional array of field names to include (alternative to field_ids)"}},"required":["source_table_id"]}' AS jsonb), CAST('{"view","projection"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + +CREATE TABLE metaschema_public.function ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL, + name text NOT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + UNIQUE (schema_id, name) +); + +CREATE INDEX function_database_id_idx ON metaschema_public.function (database_id); + +CREATE INDEX function_schema_id_idx ON metaschema_public.function (schema_id); + +CREATE TABLE metaschema_public.partition ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + database_id uuid NOT NULL, + table_id uuid NOT NULL, + strategy text NOT NULL CHECK (strategy IN ('range', 'list', 'hash')), + partition_key_id uuid NOT NULL, + interval text, + retention text, + retention_keep_table boolean NOT NULL DEFAULT true, + premake int NOT NULL DEFAULT 2, + naming_pattern text NOT NULL DEFAULT '{parent}_{bounds}', + is_parented boolean NOT NULL DEFAULT false, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now(), + CONSTRAINT partition_database_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT partition_table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT partition_key_field_fkey + FOREIGN KEY(partition_key_id) + REFERENCES metaschema_public.field (id), + CONSTRAINT partition_table_unique + UNIQUE (table_id) +); + +CREATE INDEX partition_database_id_idx ON metaschema_public.partition (database_id); + +CREATE TABLE metaschema_public.composite_type ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL, + name text NOT NULL, + label text, + description text, + attributes jsonb NOT NULL DEFAULT '[]', + smart_tags jsonb, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + UNIQUE (schema_id, name) +); + +CREATE INDEX composite_type_schema_id_idx ON metaschema_public.composite_type (schema_id); + +CREATE INDEX composite_type_database_id_idx ON metaschema_public.composite_type (database_id); \ No newline at end of file diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_private/schema.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_private/schema.sql new file mode 100644 index 00000000..41c18562 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_private/schema.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_private/schema on pg + +BEGIN; + +SELECT verify_schema ('metaschema_private'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/schema.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/schema.sql new file mode 100644 index 00000000..7600f8c6 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/schema.sql @@ -0,0 +1,6 @@ + +BEGIN; + +SELECT verify_schema ('metaschema_public'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/check_constraint/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/check_constraint/table.sql new file mode 100644 index 00000000..4bab07c3 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/check_constraint/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/check_constraint/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_public.check_constraint'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/composite_type/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/composite_type/table.sql new file mode 100644 index 00000000..de512a0c --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/composite_type/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/composite_type/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_public.composite_type'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx.sql new file mode 100644 index 00000000..d5e64f0b --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx.sql @@ -0,0 +1,6 @@ + +BEGIN; + +SELECT verify_index ('metaschema_public.database', 'databases_database_unique_name_idx'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/database/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/database/table.sql new file mode 100644 index 00000000..2644ae25 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/database/table.sql @@ -0,0 +1,6 @@ + +BEGIN; + +SELECT verify_table ('metaschema_public.database'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/default_privilege/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/default_privilege/table.sql new file mode 100644 index 00000000..ffc657a3 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/default_privilege/table.sql @@ -0,0 +1,9 @@ +-- Verify schemas/metaschema_public/tables/default_privilege/table on pg + +BEGIN; + +SELECT id, database_id, schema_id, object_type, privilege, grantee_name, is_grant +FROM metaschema_public.default_privilege +WHERE FALSE; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/embedding_chunks/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/embedding_chunks/table.sql new file mode 100644 index 00000000..d054f598 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/embedding_chunks/table.sql @@ -0,0 +1,25 @@ +-- Verify schemas/metaschema_public/tables/embedding_chunks/table on pg + +BEGIN; + +SELECT + id, + database_id, + table_id, + embedding_field_id, + chunks_table_id, + chunks_table_name, + content_field_name, + dimensions, + metric, + chunk_size, + chunk_overlap, + chunk_strategy, + metadata_fields, + enqueue_chunking_job, + chunking_task_name, + parent_fk_field_id +FROM metaschema_public.embedding_chunks +WHERE FALSE; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/enum/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/enum/table.sql new file mode 100644 index 00000000..34ad6c30 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/enum/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/enum/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_public.enum'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx.sql new file mode 100644 index 00000000..6541e522 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx on pg + +BEGIN; + +SELECT verify_index ('metaschema_public.field', 'databases_field_uniq_names_idx'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/field/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/field/table.sql new file mode 100644 index 00000000..20da5a3a --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/field/table.sql @@ -0,0 +1,6 @@ + +BEGIN; + +SELECT verify_table ('metaschema_public.field'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/foreign_key_constraint/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/foreign_key_constraint/table.sql new file mode 100644 index 00000000..52ebe68b --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/foreign_key_constraint/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/foreign_key_constraint/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_public.foreign_key_constraint'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/full_text_search/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/full_text_search/table.sql new file mode 100644 index 00000000..23d4a04f --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/full_text_search/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/full_text_search/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_public.full_text_search'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/function/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/function/table.sql new file mode 100644 index 00000000..fc8976f2 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/function/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/function/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_public.function'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/index/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/index/table.sql new file mode 100644 index 00000000..6529f612 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/index/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/index/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_public.index'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql new file mode 100644 index 00000000..9eabe731 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed on pg + +BEGIN; + +SELECT 1 FROM metaschema_public.node_type_registry WHERE name = 'AuthzDirectOwner'; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/node_type_registry/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/node_type_registry/table.sql new file mode 100644 index 00000000..bd2cacab --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/node_type_registry/table.sql @@ -0,0 +1,9 @@ +-- Verify schemas/metaschema_public/tables/node_type_registry/table on pg + +BEGIN; + +SELECT name, slug, category, display_name, description, parameter_schema, tags +FROM metaschema_public.node_type_registry +WHERE FALSE; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/partition/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/partition/table.sql new file mode 100644 index 00000000..a37df684 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/partition/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/partition/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_public.partition'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/policy/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/policy/table.sql new file mode 100644 index 00000000..72ae6169 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/policy/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/policy/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_public.policy'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/primary_key_constraint/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/primary_key_constraint/table.sql new file mode 100644 index 00000000..22a74fc5 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/primary_key_constraint/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/primary_key_constraint/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_public.primary_key_constraint'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/schema/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/schema/table.sql new file mode 100644 index 00000000..7d37ed9c --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/schema/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/schema/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_public.schema'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/schema_grant/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/schema_grant/table.sql new file mode 100644 index 00000000..a630e80b --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/schema_grant/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/schema_grant/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_public.schema_grant'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/spatial_relation/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/spatial_relation/table.sql new file mode 100644 index 00000000..68c821b3 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/spatial_relation/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/spatial_relation/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_public.spatial_relation'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx.sql new file mode 100644 index 00000000..9e71e850 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx.sql @@ -0,0 +1,6 @@ + +BEGIN; + +SELECT verify_index ('metaschema_public.table', 'databases_table_unique_name_idx'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/table/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/table/table.sql new file mode 100644 index 00000000..cbfc4432 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/table/table.sql @@ -0,0 +1,6 @@ + +BEGIN; + +SELECT verify_table ('metaschema_public.table'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/table_grant/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/table_grant/table.sql new file mode 100644 index 00000000..77ebaac8 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/table_grant/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/table_grant/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_public.table_grant'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/trigger/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/trigger/table.sql new file mode 100644 index 00000000..5ab33b98 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/trigger/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/trigger/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_public.trigger'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/trigger_function/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/trigger_function/table.sql new file mode 100644 index 00000000..bb29f907 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/trigger_function/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/trigger_function/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_public.trigger_function'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/unique_constraint/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/unique_constraint/table.sql new file mode 100644 index 00000000..f4e6b519 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/unique_constraint/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/unique_constraint/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_public.unique_constraint'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/view/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/view/table.sql new file mode 100644 index 00000000..b35e8ab2 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/view/table.sql @@ -0,0 +1,10 @@ +-- Verify schemas/metaschema_public/tables/view/table on pg + +BEGIN; + +SELECT id, database_id, schema_id, name, view_type, data, filter_type, filter_data, + security_invoker, is_read_only, smart_tags, category, module, scope, tags +FROM metaschema_public.view +WHERE FALSE; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/view_grant/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/view_grant/table.sql new file mode 100644 index 00000000..169863fa --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/view_grant/table.sql @@ -0,0 +1,9 @@ +-- Verify schemas/metaschema_public/tables/view_grant/table on pg + +BEGIN; + +SELECT id, database_id, view_id, grantee_name, privilege, with_grant_option +FROM metaschema_public.view_grant +WHERE FALSE; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/view_rule/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/view_rule/table.sql new file mode 100644 index 00000000..f280ea01 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/view_rule/table.sql @@ -0,0 +1,9 @@ +-- Verify schemas/metaschema_public/tables/view_rule/table on pg + +BEGIN; + +SELECT id, database_id, view_id, name, event, action +FROM metaschema_public.view_rule +WHERE FALSE; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/view_table/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/view_table/table.sql new file mode 100644 index 00000000..7270a4bf --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/tables/view_table/table.sql @@ -0,0 +1,9 @@ +-- Verify schemas/metaschema_public/tables/view_table/table on pg + +BEGIN; + +SELECT id, view_id, table_id, join_order +FROM metaschema_public.view_table +WHERE FALSE; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/types/object_category.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/types/object_category.sql new file mode 100644 index 00000000..cb6b1b13 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/metaschema_public/types/object_category.sql @@ -0,0 +1,9 @@ +-- Verify schemas/metaschema_public/types/object_category on pg + +BEGIN; + +SELECT 'core'::metaschema_public.object_category; +SELECT 'module'::metaschema_public.object_category; +SELECT 'app'::metaschema_public.object_category; + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/services_private/schema.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/services_private/schema.sql new file mode 100644 index 00000000..3017b36b --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/services_private/schema.sql @@ -0,0 +1,7 @@ +-- Verify schemas/services_private/schema on pg + +BEGIN; + +SELECT verify_schema ('services_private'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/schema.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/schema.sql new file mode 100644 index 00000000..77c13499 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/schema.sql @@ -0,0 +1,7 @@ +-- Verify schemas/services_public/schema on pg + +BEGIN; + +SELECT verify_schema ('services_public'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/api_modules/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/api_modules/table.sql new file mode 100644 index 00000000..455ba0d2 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/api_modules/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/services_public/tables/api_modules/table on pg + +BEGIN; + +SELECT verify_table ('services_public.api_modules'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/api_schemas/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/api_schemas/table.sql new file mode 100644 index 00000000..49739218 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/api_schemas/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/services_public/tables/api_schemas/table on pg + +BEGIN; + +SELECT verify_table ('services_public.api_schemas'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/apis/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/apis/table.sql new file mode 100644 index 00000000..c3906e4f --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/apis/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/services_public/tables/apis/table on pg + +BEGIN; + +SELECT verify_table ('services_public.apis'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/apps/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/apps/table.sql new file mode 100644 index 00000000..e1b4c7b7 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/apps/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/services_public/tables/apps/table on pg + +BEGIN; + +SELECT verify_table ('services_public.apps'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/domains/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/domains/table.sql new file mode 100644 index 00000000..7d8cdfe6 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/domains/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/services_public/tables/domains/table on pg + +BEGIN; + +SELECT verify_table ('services_public.domains'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/site_metadata/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/site_metadata/table.sql new file mode 100644 index 00000000..7e4a9cce --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/site_metadata/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/services_public/tables/site_metadata/table on pg + +BEGIN; + +SELECT verify_table ('services_public.site_metadata'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/site_modules/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/site_modules/table.sql new file mode 100644 index 00000000..34343e49 --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/site_modules/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/services_public/tables/site_modules/table on pg + +BEGIN; + +SELECT verify_table ('services_public.site_modules'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/site_themes/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/site_themes/table.sql new file mode 100644 index 00000000..834547ce --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/site_themes/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/services_public/tables/site_themes/table on pg + +BEGIN; + +SELECT verify_table ('services_public.site_themes'); + +ROLLBACK; diff --git a/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/sites/table.sql b/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/sites/table.sql new file mode 100644 index 00000000..00ed882e --- /dev/null +++ b/extensions/@pgpm/metaschema-schema/verify/schemas/services_public/tables/sites/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/services_public/tables/sites/table on pg + +BEGIN; + +SELECT verify_table ('services_public.sites'); + +ROLLBACK; diff --git a/extensions/@pgpm/services/LICENSE b/extensions/@pgpm/services/LICENSE new file mode 100644 index 00000000..7b18c918 --- /dev/null +++ b/extensions/@pgpm/services/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2025 Dan Lynch +Copyright (c) 2025 Constructive + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/@pgpm/services/Makefile b/extensions/@pgpm/services/Makefile new file mode 100644 index 00000000..d5cd6eeb --- /dev/null +++ b/extensions/@pgpm/services/Makefile @@ -0,0 +1,6 @@ +EXTENSION = services +DATA = sql/services--0.26.3.sql + +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) diff --git a/extensions/@pgpm/services/__tests__/__snapshots__/services.test.ts.snap b/extensions/@pgpm/services/__tests__/__snapshots__/services.test.ts.snap new file mode 100644 index 00000000..892a7a90 --- /dev/null +++ b/extensions/@pgpm/services/__tests__/__snapshots__/services.test.ts.snap @@ -0,0 +1,147 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`services functionality should handle complete meta workflow with services 1`] = ` +{ + "created_at": "[DATE]", + "hash": null, + "id": "[ID]", + "label": null, + "name": "my-meta-db", + "owner_id": "[ID]", + "schema_hash": null, + "updated_at": "[DATE]", +} +`; + +exports[`services functionality should handle complete meta workflow with services 2`] = ` +{ + "anon_role": "anonymous", + "database_id": "[ID]", + "dbname": "test-database", + "id": "[ID]", + "is_public": true, + "name": "public", + "role_name": "authenticated", +} +`; + +exports[`services functionality should handle complete meta workflow with services 3`] = ` +{ + "anon_role": "administrator", + "database_id": "[ID]", + "dbname": "test-database", + "id": "[ID]", + "is_public": true, + "name": "admin", + "role_name": "administrator", +} +`; + +exports[`services functionality should handle complete meta workflow with services 4`] = ` +{ + "apple_touch_icon": null, + "database_id": "[ID]", + "dbname": "test-database", + "description": "Website Description", + "favicon": null, + "id": "[ID]", + "logo": null, + "og_image": null, + "title": "Website Title", +} +`; + +exports[`services functionality should handle complete meta workflow with services 5`] = ` +{ + "api_id": "[ID]", + "database_id": "[ID]", + "domain": "pgpm.io", + "id": "[ID]", + "site_id": null, + "subdomain": "api", +} +`; + +exports[`services functionality should handle complete meta workflow with services 6`] = ` +{ + "api_id": null, + "database_id": "[ID]", + "domain": "pgpm.io", + "id": "[ID]", + "site_id": "[ID]", + "subdomain": "app", +} +`; + +exports[`services functionality should handle complete meta workflow with services 7`] = ` +{ + "api_id": "[ID]", + "database_id": "[ID]", + "domain": "pgpm.io", + "id": "[ID]", + "site_id": null, + "subdomain": "admin", +} +`; + +exports[`services functionality should handle complete meta workflow with services 8`] = ` +{ + "data": { + "supportEmail": "support@interweb.co", + }, + "database_id": "[ID]", + "id": "[ID]", + "name": "legal-emails", + "site_id": "[ID]", +} +`; + +exports[`services functionality should handle complete meta workflow with services 9`] = ` +{ + "api_id": "[ID]", + "data": { + "authenticate": "authenticate", + "authenticate_schema": "services_private", + }, + "database_id": "[ID]", + "id": "[ID]", + "name": "rls_module", +} +`; + +exports[`services functionality should handle complete meta workflow with services 10`] = ` +{ + "data": { + "auth_schema": "services_public", + "forgot_password": "forgot_password", + "reset_password": "reset_password", + "send_verification_email": "send_verification_email", + "set_password": "set_password", + "sign_in": "login", + "sign_up": "register", + "verify_email": "verify_email", + }, + "database_id": "[ID]", + "id": "[ID]", + "name": "user_auth_module", + "site_id": "[ID]", +} +`; + +exports[`services functionality should handle complete meta workflow with services 11`] = ` +{ + "api_id": "[ID]", + "database_id": "[ID]", + "id": "[ID]", + "schema_id": "[ID]", +} +`; + +exports[`services functionality should handle complete meta workflow with services 12`] = ` +{ + "api_id": "[ID]", + "database_id": "[ID]", + "id": "[ID]", + "schema_id": "[ID]", +} +`; diff --git a/extensions/@pgpm/services/__tests__/services.test.ts b/extensions/@pgpm/services/__tests__/services.test.ts new file mode 100644 index 00000000..f722a512 --- /dev/null +++ b/extensions/@pgpm/services/__tests__/services.test.ts @@ -0,0 +1,210 @@ +import { getConnections, PgTestClient, snapshot } from 'pgsql-test'; + +let pg: PgTestClient; +let teardown: () => Promise; + +describe('services functionality', () => { + beforeAll(async () => { + ({ pg, teardown } = await getConnections()); + }); + + afterAll(async () => { + await teardown(); + }); + + beforeEach(async () => { + await pg.beforeEach(); + await pg.any(`GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO public`); + }); + + afterEach(async () => { + await pg.afterEach(); + }); + + it('should handle complete meta workflow with services', async () => { + const objs: Record = { + tables: {}, + domains: {}, + apis: {}, + sites: {} + }; + + const owner_id = '07281002-1699-4762-57e3-ab1b92243120'; + + const snap = (obj: any) => { + expect(snapshot(obj)).toMatchSnapshot(); + }; + + const snapWithNormalizedDbname = (obj: any) => { + const normalized = { + ...obj, + dbname: 'test-database' + }; + expect(snapshot(normalized)).toMatchSnapshot(); + }; + + // Step 1: Create database + const [database] = await pg.any( + `INSERT INTO metaschema_public.database (owner_id, name) + VALUES ($1, $2) + RETURNING *`, + [owner_id, 'my-meta-db'] + ); + objs.db = database; + const database_id = database.id; + expect(snapshot(database)).toMatchSnapshot(); + + // Step 2: Create APIs first (since domains reference them) + const [publicApi] = await pg.any( + `INSERT INTO services_public.apis (database_id, name, role_name, anon_role) + VALUES ($1, $2, $3, $4) + RETURNING *`, + [database_id, 'public', 'authenticated', 'anonymous'] + ); + objs.apis.public = publicApi; + snapWithNormalizedDbname(publicApi); + + const [adminApi] = await pg.any( + `INSERT INTO services_public.apis (database_id, name, role_name, anon_role) + VALUES ($1, $2, $3, $4) + RETURNING *`, + [database_id, 'admin', 'administrator', 'administrator'] + ); + objs.apis.admin = adminApi; + snapWithNormalizedDbname(adminApi); + + // Step 3: Create sites + const [appSite] = await pg.any( + `INSERT INTO services_public.sites (database_id, title, description) + VALUES ($1, $2, $3) + RETURNING *`, + [database_id, 'Website Title', 'Website Description'] + ); + objs.sites.app = appSite; + snapWithNormalizedDbname(appSite); + + // Step 4: Register domains (linking to APIs and sites) + const [apiDomain] = await pg.any( + `INSERT INTO services_public.domains (database_id, api_id, domain, subdomain) + VALUES ($1, $2, $3, $4) + RETURNING *`, + [database_id, objs.apis.public.id, 'pgpm.io', 'api'] + ); + objs.domains.api = apiDomain; + expect(snapshot(apiDomain)).toMatchSnapshot(); + + const [appDomain] = await pg.any( + `INSERT INTO services_public.domains (database_id, site_id, domain, subdomain) + VALUES ($1, $2, $3, $4) + RETURNING *`, + [database_id, objs.sites.app.id, 'pgpm.io', 'app'] + ); + objs.domains.app = appDomain; + expect(snapshot(appDomain)).toMatchSnapshot(); + + const [adminDomain] = await pg.any( + `INSERT INTO services_public.domains (database_id, api_id, domain, subdomain) + VALUES ($1, $2, $3, $4) + RETURNING *`, + [database_id, objs.apis.admin.id, 'pgpm.io', 'admin'] + ); + objs.domains.admin = adminDomain; + expect(snapshot(adminDomain)).toMatchSnapshot(); + + const [baseDomain] = await pg.any( + `INSERT INTO services_public.domains (database_id, domain) + VALUES ($1, $2) + RETURNING *`, + [database_id, 'pgpm.io'] + ); + objs.domains.base = baseDomain; + + // Step 5: Register modules + const [siteModule1] = await pg.any( + `INSERT INTO services_public.site_modules (database_id, site_id, name, data) + VALUES ($1, $2, $3, $4::jsonb) + RETURNING *`, + [database_id, objs.sites.app.id, 'legal-emails', JSON.stringify({ + supportEmail: 'support@interweb.co' + })] + ); + expect(snapshot(siteModule1)).toMatchSnapshot(); + + const [apiModule] = await pg.any( + `INSERT INTO services_public.api_modules (database_id, api_id, name, data) + VALUES ($1, $2, $3, $4::jsonb) + RETURNING *`, + [database_id, objs.apis.public.id, 'rls_module', JSON.stringify({ + authenticate_schema: 'services_private', + authenticate: 'authenticate' + })] + ); + expect(snapshot(apiModule)).toMatchSnapshot(); + + const [siteModule2] = await pg.any( + `INSERT INTO services_public.site_modules (database_id, site_id, name, data) + VALUES ($1, $2, $3, $4::jsonb) + RETURNING *`, + [database_id, objs.sites.app.id, 'user_auth_module', JSON.stringify({ + auth_schema: 'services_public', + sign_in: 'login', + sign_up: 'register', + set_password: 'set_password', + reset_password: 'reset_password', + forgot_password: 'forgot_password', + send_verification_email: 'send_verification_email', + verify_email: 'verify_email' + })] + ); + expect(snapshot(siteModule2)).toMatchSnapshot(); + + // Step 6: Schema associations + const [schema] = await pg.any( + `INSERT INTO metaschema_public.schema (database_id, schema_name, name) + VALUES ($1, $2, $3) + RETURNING *`, + [database_id, 'brand-public', 'public'] + ); + + const [publicAssoc] = await pg.any( + `INSERT INTO services_public.api_schemas (database_id, schema_id, api_id) + VALUES ($1, $2, $3) + RETURNING *`, + [database_id, schema.id, objs.apis.public.id] + ); + + const [adminAssoc] = await pg.any( + `INSERT INTO services_public.api_schemas (database_id, schema_id, api_id) + VALUES ($1, $2, $3) + RETURNING *`, + [database_id, schema.id, objs.apis.admin.id] + ); + + snap(publicAssoc); + snap(adminAssoc); + }); + + it('should register domain independently', async () => { + const owner_id = '07281002-1699-4762-57e3-ab1b92243120'; + + // Create database first + const [database] = await pg.any( + `INSERT INTO metaschema_public.database (owner_id, name) + VALUES ($1, $2) + RETURNING *`, + [owner_id, 'test-db-for-domain'] + ); + + // Then create domain + const [domain] = await pg.any( + `INSERT INTO services_public.domains (database_id, domain, subdomain) + VALUES ($1, $2, $3) + RETURNING *`, + [database.id, 'example.com', 'api'] + ); + + expect(domain.database_id).toBe(database.id); + expect(domain.domain).toBe('example.com'); + expect(domain.subdomain).toBe('api'); + }); +}); diff --git a/extensions/@pgpm/services/deploy/schemas/services_private/schema.sql b/extensions/@pgpm/services/deploy/schemas/services_private/schema.sql new file mode 100644 index 00000000..60ab34e5 --- /dev/null +++ b/extensions/@pgpm/services/deploy/schemas/services_private/schema.sql @@ -0,0 +1,14 @@ +-- Deploy schemas/services_private/schema to pg + + +BEGIN; + +CREATE SCHEMA services_private; + +GRANT USAGE ON SCHEMA services_private TO authenticated; +GRANT USAGE ON SCHEMA services_private TO administrator; +ALTER DEFAULT PRIVILEGES IN SCHEMA services_private GRANT ALL ON TABLES TO administrator; +ALTER DEFAULT PRIVILEGES IN SCHEMA services_private GRANT ALL ON SEQUENCES TO administrator; +ALTER DEFAULT PRIVILEGES IN SCHEMA services_private GRANT ALL ON FUNCTIONS TO administrator; + +COMMIT; diff --git a/extensions/@pgpm/services/deploy/schemas/services_private/triggers/enforce_api_schema_table_name_uniqueness.sql b/extensions/@pgpm/services/deploy/schemas/services_private/triggers/enforce_api_schema_table_name_uniqueness.sql new file mode 100644 index 00000000..37d06b8b --- /dev/null +++ b/extensions/@pgpm/services/deploy/schemas/services_private/triggers/enforce_api_schema_table_name_uniqueness.sql @@ -0,0 +1,47 @@ +-- Deploy schemas/services_private/triggers/enforce_api_schema_table_name_uniqueness to pg + +-- requires: schemas/services_private/schema +-- requires: schemas/services_public/tables/api_schemas/table +-- requires: metaschema-schema:schemas/metaschema_public/tables/table/table +-- requires: metaschema-schema:schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx + +BEGIN; + +-- When linking a schema to an API, check that none of its tables conflict +-- (by plural-hash) with tables already exposed through that API's other schemas. +CREATE FUNCTION services_private.tg_enforce_api_schema_table_name_uniqueness() +RETURNS TRIGGER AS $$ +DECLARE + conflicting_new_table text; + conflicting_existing_table text; +BEGIN + -- Find any table name collision between the newly linked schema + -- and any schema already linked to the same API + SELECT new_t.name, existing_t.name + INTO conflicting_new_table, conflicting_existing_table + FROM metaschema_public.table AS new_t + JOIN services_public.api_schemas AS existing_link + ON existing_link.api_id = NEW.api_id + AND existing_link.schema_id IS DISTINCT FROM NEW.schema_id + JOIN metaschema_public.table AS existing_t + ON existing_t.schema_id = existing_link.schema_id + AND metaschema_private.table_name_hash(existing_t.name) = metaschema_private.table_name_hash(new_t.name) + WHERE new_t.schema_id = NEW.schema_id + LIMIT 1; + + IF conflicting_new_table IS NOT NULL THEN + RAISE EXCEPTION 'Cannot link schema to API: table "%" conflicts with existing table "%" already exposed in this API. Table names must be unique (by plural form) across all schemas within the same API.', + conflicting_new_table, conflicting_existing_table; + END IF; + + RETURN NEW; +END; +$$ +LANGUAGE plpgsql VOLATILE; + +CREATE TRIGGER _000001_enforce_api_schema_table_name_uniqueness +BEFORE INSERT ON services_public.api_schemas +FOR EACH ROW +EXECUTE FUNCTION services_private.tg_enforce_api_schema_table_name_uniqueness(); + +COMMIT; diff --git a/extensions/@pgpm/services/deploy/schemas/services_private/triggers/enforce_api_table_name_uniqueness.sql b/extensions/@pgpm/services/deploy/schemas/services_private/triggers/enforce_api_table_name_uniqueness.sql new file mode 100644 index 00000000..e2df11de --- /dev/null +++ b/extensions/@pgpm/services/deploy/schemas/services_private/triggers/enforce_api_table_name_uniqueness.sql @@ -0,0 +1,60 @@ +-- Deploy schemas/services_private/triggers/enforce_api_table_name_uniqueness to pg + +-- requires: schemas/services_private/schema +-- requires: schemas/services_public/tables/api_schemas/table +-- requires: metaschema-schema:schemas/metaschema_public/tables/table/table +-- requires: metaschema-schema:schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx + +BEGIN; + +-- Enforce that table names are unique (by plural-hash) across all schemas within each API. +-- This allows different APIs to have tables with the same name, but prevents +-- collisions within a single API where multiple schemas are exposed together. +CREATE FUNCTION services_private.tg_enforce_api_table_name_uniqueness() +RETURNS TRIGGER AS $$ +DECLARE + new_name_hash bytea; + conflicting_api_name text; + conflicting_table_name text; +BEGIN + -- Compute the plural-hash of the new table name + new_name_hash := metaschema_private.table_name_hash(NEW.name); + + -- Check if any API that includes this table's schema also includes + -- another schema containing a table with the same name hash + SELECT a.name, t.name + INTO conflicting_api_name, conflicting_table_name + FROM services_public.api_schemas AS my_api + JOIN services_public.api_schemas AS other_api + ON other_api.api_id = my_api.api_id + AND other_api.schema_id IS DISTINCT FROM NEW.schema_id + JOIN metaschema_public.table AS t + ON t.schema_id = other_api.schema_id + AND metaschema_private.table_name_hash(t.name) = new_name_hash + JOIN services_public.apis AS a + ON a.id = my_api.api_id + WHERE my_api.schema_id = NEW.schema_id + LIMIT 1; + + IF conflicting_api_name IS NOT NULL THEN + RAISE EXCEPTION 'Table name "%" conflicts with existing table "%" in API "%". Table names must be unique (by plural form) across all schemas within the same API.', + NEW.name, conflicting_table_name, conflicting_api_name; + END IF; + + RETURN NEW; +END; +$$ +LANGUAGE plpgsql VOLATILE; + +CREATE TRIGGER _000003_enforce_api_table_name_uniqueness +BEFORE INSERT ON metaschema_public.table +FOR EACH ROW +EXECUTE FUNCTION services_private.tg_enforce_api_table_name_uniqueness(); + +CREATE TRIGGER _000003_enforce_api_table_name_uniqueness_update +BEFORE UPDATE ON metaschema_public.table +FOR EACH ROW +WHEN (NEW.name IS DISTINCT FROM OLD.name OR NEW.schema_id IS DISTINCT FROM OLD.schema_id) +EXECUTE FUNCTION services_private.tg_enforce_api_table_name_uniqueness(); + +COMMIT; diff --git a/extensions/@pgpm/services/deploy/schemas/services_public/schema.sql b/extensions/@pgpm/services/deploy/schemas/services_public/schema.sql new file mode 100644 index 00000000..1fbb4d3e --- /dev/null +++ b/extensions/@pgpm/services/deploy/schemas/services_public/schema.sql @@ -0,0 +1,15 @@ +-- Deploy schemas/services_public/schema to pg + + +BEGIN; + +CREATE SCHEMA services_public; + +GRANT USAGE ON SCHEMA services_public TO authenticated; +GRANT USAGE ON SCHEMA services_public TO administrator; +ALTER DEFAULT PRIVILEGES IN SCHEMA services_public GRANT ALL ON TABLES TO administrator; +ALTER DEFAULT PRIVILEGES IN SCHEMA services_public GRANT ALL ON SEQUENCES TO administrator; +ALTER DEFAULT PRIVILEGES IN SCHEMA services_public GRANT ALL ON FUNCTIONS TO administrator; + + +COMMIT; diff --git a/extensions/@pgpm/services/deploy/schemas/services_public/tables/api_modules/table.sql b/extensions/@pgpm/services/deploy/schemas/services_public/tables/api_modules/table.sql new file mode 100644 index 00000000..befbdb7f --- /dev/null +++ b/extensions/@pgpm/services/deploy/schemas/services_public/tables/api_modules/table.sql @@ -0,0 +1,35 @@ +-- Deploy schemas/services_public/tables/api_modules/table to pg + +-- requires: schemas/services_public/schema +-- requires: schemas/services_public/tables/apis/table +-- requires: schemas/metaschema_public/tables/database/table + +BEGIN; + +CREATE TABLE services_public.api_modules ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + api_id uuid NOT NULL, + name text NOT NULL, + data json NOT NULL, + + -- + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE + +); + +COMMENT ON TABLE services_public.api_modules IS 'Server-side module configuration for an API endpoint; stores module name and JSON settings used by the application server'; +COMMENT ON COLUMN services_public.api_modules.id IS 'Unique identifier for this API module record'; +COMMENT ON COLUMN services_public.api_modules.database_id IS 'Reference to the metaschema database'; +COMMENT ON COLUMN services_public.api_modules.api_id IS 'API this module configuration belongs to'; +COMMENT ON COLUMN services_public.api_modules.name IS 'Module name (e.g. auth, uploads, webhooks)'; +COMMENT ON COLUMN services_public.api_modules.data IS 'JSON configuration data for this module'; + +ALTER TABLE services_public.api_modules ADD CONSTRAINT api_modules_api_id_fkey FOREIGN KEY ( api_id ) REFERENCES services_public.apis ( id ); +CREATE INDEX api_modules_api_id_idx ON services_public.api_modules ( api_id ); + +CREATE INDEX api_modules_database_id_idx ON services_public.api_modules ( database_id ); + + +COMMIT; diff --git a/extensions/@pgpm/services/deploy/schemas/services_public/tables/api_schemas/table.sql b/extensions/@pgpm/services/deploy/schemas/services_public/tables/api_schemas/table.sql new file mode 100644 index 00000000..60d12de5 --- /dev/null +++ b/extensions/@pgpm/services/deploy/schemas/services_public/tables/api_schemas/table.sql @@ -0,0 +1,33 @@ +-- Deploy schemas/services_public/tables/api_schemas/table to pg + +-- requires: schemas/services_public/schema + +BEGIN; + +CREATE TABLE services_public.api_schemas ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL, + api_id uuid NOT NULL, + + -- + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT api_fkey FOREIGN KEY (api_id) REFERENCES services_public.apis (id) ON DELETE CASCADE, + unique(api_id, schema_id) +); + +COMMENT ON TABLE services_public.api_schemas IS 'Join table linking APIs to the database schemas they expose; controls which schemas are accessible through each API'; +COMMENT ON COLUMN services_public.api_schemas.id IS 'Unique identifier for this API-schema mapping'; +COMMENT ON COLUMN services_public.api_schemas.database_id IS 'Reference to the metaschema database'; +COMMENT ON COLUMN services_public.api_schemas.schema_id IS 'Metaschema schema being exposed through the API'; +COMMENT ON COLUMN services_public.api_schemas.api_id IS 'API that exposes this schema'; + + + +CREATE INDEX api_schemas_database_id_idx ON services_public.api_schemas ( database_id ); +CREATE INDEX api_schemas_schema_id_idx ON services_public.api_schemas ( schema_id ); +CREATE INDEX api_schemas_api_id_idx ON services_public.api_schemas ( api_id ); + +COMMIT; diff --git a/extensions/@pgpm/services/deploy/schemas/services_public/tables/api_settings/table.sql b/extensions/@pgpm/services/deploy/schemas/services_public/tables/api_settings/table.sql new file mode 100644 index 00000000..71d7e12f --- /dev/null +++ b/extensions/@pgpm/services/deploy/schemas/services_public/tables/api_settings/table.sql @@ -0,0 +1,59 @@ +-- Deploy schemas/services_public/tables/api_settings/table to pg + +-- requires: schemas/services_public/schema +-- requires: schemas/services_public/tables/apis/table +-- requires: schemas/services_public/tables/database_settings/table +-- requires: schemas/metaschema_public/tables/database/table + +BEGIN; + +CREATE TABLE services_public.api_settings ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + api_id uuid NOT NULL UNIQUE, + + -- Per-API overrides (NULL = inherit from database_settings) + enable_aggregates boolean, + enable_postgis boolean, + enable_search boolean, + enable_direct_uploads boolean, + enable_presigned_uploads boolean, + enable_many_to_many boolean, + enable_connection_filter boolean, + enable_ltree boolean, + enable_llm boolean, + enable_realtime boolean, + enable_bulk boolean, + enable_i18n boolean, + + -- Extensible JSON for future settings that don't warrant their own column + options jsonb NOT NULL DEFAULT '{}'::jsonb, + + -- + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT api_fkey FOREIGN KEY (api_id) REFERENCES services_public.apis (id) ON DELETE CASCADE +); + +COMMENT ON TABLE services_public.api_settings IS 'Per-API feature flag overrides; NULL columns inherit from database_settings, explicit true/false overrides the database default'; +COMMENT ON COLUMN services_public.api_settings.id IS 'Unique identifier for this API settings record'; +COMMENT ON COLUMN services_public.api_settings.database_id IS 'Reference to the metaschema database'; +COMMENT ON COLUMN services_public.api_settings.api_id IS 'API these settings override for'; +COMMENT ON COLUMN services_public.api_settings.enable_aggregates IS 'Override: enable aggregate queries (NULL = inherit from database_settings)'; +COMMENT ON COLUMN services_public.api_settings.enable_postgis IS 'Override: enable PostGIS spatial types (NULL = inherit from database_settings)'; +COMMENT ON COLUMN services_public.api_settings.enable_search IS 'Override: enable unified search (NULL = inherit from database_settings)'; +COMMENT ON COLUMN services_public.api_settings.enable_direct_uploads IS 'Override: enable direct (multipart) file uploads (NULL = inherit from database_settings)'; +COMMENT ON COLUMN services_public.api_settings.enable_presigned_uploads IS 'Override: enable presigned URL upload flow (NULL = inherit from database_settings)'; +COMMENT ON COLUMN services_public.api_settings.enable_many_to_many IS 'Override: enable many-to-many relationships (NULL = inherit from database_settings)'; +COMMENT ON COLUMN services_public.api_settings.enable_connection_filter IS 'Override: enable connection filter (NULL = inherit from database_settings)'; +COMMENT ON COLUMN services_public.api_settings.enable_ltree IS 'Override: enable ltree hierarchical data type (NULL = inherit from database_settings)'; +COMMENT ON COLUMN services_public.api_settings.enable_llm IS 'Override: enable LLM/AI integration features (NULL = inherit from database_settings)'; +COMMENT ON COLUMN services_public.api_settings.enable_realtime IS 'Override: enable realtime subscriptions (NULL = inherit from database_settings)'; +COMMENT ON COLUMN services_public.api_settings.enable_bulk IS 'Override: enable bulk mutations (NULL = inherit from database_settings)'; +COMMENT ON COLUMN services_public.api_settings.enable_i18n IS 'Override: enable internationalization plugin (NULL = inherit from database_settings)'; +COMMENT ON COLUMN services_public.api_settings.options IS 'Extensible JSON for additional per-API settings that do not have dedicated columns'; + +CREATE INDEX api_settings_database_id_idx ON services_public.api_settings (database_id); +CREATE INDEX api_settings_api_id_idx ON services_public.api_settings (api_id); + +COMMIT; diff --git a/extensions/@pgpm/services/deploy/schemas/services_public/tables/apis/table.sql b/extensions/@pgpm/services/deploy/schemas/services_public/tables/apis/table.sql new file mode 100644 index 00000000..1b068295 --- /dev/null +++ b/extensions/@pgpm/services/deploy/schemas/services_public/tables/apis/table.sql @@ -0,0 +1,34 @@ +-- Deploy schemas/services_public/tables/apis/table to pg + +-- requires: schemas/services_public/schema +-- requires: schemas/metaschema_public/tables/database/table + +BEGIN; + +CREATE TABLE services_public.apis ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + name text NOT NULL, + dbname text NOT NULL DEFAULT current_database(), + role_name text NOT NULL DEFAULT 'authenticated', + anon_role text NOT NULL DEFAULT 'anonymous', + is_public boolean NOT NULL DEFAULT true, + + -- + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + UNIQUE(database_id, name) +); + +COMMENT ON TABLE services_public.apis IS 'API endpoint configurations: each record defines a PostGraphile/PostgREST API with its database role and public access settings'; +COMMENT ON COLUMN services_public.apis.id IS 'Unique identifier for this API'; +COMMENT ON COLUMN services_public.apis.database_id IS 'Reference to the metaschema database this API serves'; +COMMENT ON COLUMN services_public.apis.name IS 'Unique name for this API within its database'; +COMMENT ON COLUMN services_public.apis.dbname IS 'PostgreSQL database name to connect to'; +COMMENT ON COLUMN services_public.apis.role_name IS 'PostgreSQL role used for authenticated requests'; +COMMENT ON COLUMN services_public.apis.anon_role IS 'PostgreSQL role used for anonymous/unauthenticated requests'; +COMMENT ON COLUMN services_public.apis.is_public IS 'Whether this API is publicly accessible without authentication'; + +CREATE INDEX apis_database_id_idx ON services_public.apis ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/services/deploy/schemas/services_public/tables/apps/table.sql b/extensions/@pgpm/services/deploy/schemas/services_public/tables/apps/table.sql new file mode 100644 index 00000000..b9856e92 --- /dev/null +++ b/extensions/@pgpm/services/deploy/schemas/services_public/tables/apps/table.sql @@ -0,0 +1,43 @@ +-- Deploy schemas/services_public/tables/apps/table to pg + +-- requires: schemas/services_public/schema +-- requires: schemas/services_public/tables/sites/table +-- requires: schemas/metaschema_public/tables/database/table + +BEGIN; + +CREATE TABLE services_public.apps ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + site_id uuid NOT NULL, + name text, + app_image image, + app_store_link url, + app_store_id text, + app_id_prefix text, + play_store_link url, + + -- + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + UNIQUE ( site_id ) +); + +COMMENT ON TABLE services_public.apps IS 'Mobile and native app configuration linked to a site, including store links and identifiers'; +COMMENT ON COLUMN services_public.apps.id IS 'Unique identifier for this app'; +COMMENT ON COLUMN services_public.apps.database_id IS 'Reference to the metaschema database this app belongs to'; +COMMENT ON COLUMN services_public.apps.site_id IS 'Site this app is associated with (one app per site)'; +COMMENT ON COLUMN services_public.apps.name IS 'Display name of the app'; +COMMENT ON COLUMN services_public.apps.app_image IS 'App icon or promotional image'; +COMMENT ON COLUMN services_public.apps.app_store_link IS 'URL to the Apple App Store listing'; +COMMENT ON COLUMN services_public.apps.app_store_id IS 'Apple App Store application identifier'; +COMMENT ON COLUMN services_public.apps.app_id_prefix IS 'Apple App ID prefix (Team ID) for universal links and associated domains'; +COMMENT ON COLUMN services_public.apps.play_store_link IS 'URL to the Google Play Store listing'; + +ALTER TABLE services_public.apps ADD CONSTRAINT apps_site_id_fkey FOREIGN KEY ( site_id ) REFERENCES services_public.sites ( id ); +CREATE INDEX apps_site_id_idx ON services_public.apps ( site_id ); + +CREATE INDEX apps_database_id_idx ON services_public.apps ( database_id ); + + +COMMIT; diff --git a/extensions/@pgpm/services/deploy/schemas/services_public/tables/cors_settings/table.sql b/extensions/@pgpm/services/deploy/schemas/services_public/tables/cors_settings/table.sql new file mode 100644 index 00000000..f5471890 --- /dev/null +++ b/extensions/@pgpm/services/deploy/schemas/services_public/tables/cors_settings/table.sql @@ -0,0 +1,36 @@ +-- Deploy schemas/services_public/tables/cors_settings/table to pg + +-- requires: schemas/services_public/schema +-- requires: schemas/services_public/tables/apis/table +-- requires: schemas/metaschema_public/tables/database/table + +BEGIN; + +-- Per-database (and optionally per-API) CORS origin configuration. +-- Typed replacement for api_modules rows with name = 'cors'. +-- Row with api_id NULL = database-wide default; row with api_id = per-API override. +CREATE TABLE services_public.cors_settings ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + api_id uuid, + + -- Allowed origins for CORS preflight and response headers + allowed_origins text[] NOT NULL DEFAULT '{}', + + -- + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT api_fkey FOREIGN KEY (api_id) REFERENCES services_public.apis (id) ON DELETE CASCADE, + CONSTRAINT cors_settings_unique UNIQUE (database_id, api_id) +); + +COMMENT ON TABLE services_public.cors_settings IS 'Per-database and per-API CORS origin configuration; typed replacement for api_modules cors JSONB entries'; +COMMENT ON COLUMN services_public.cors_settings.id IS 'Unique identifier for this CORS settings record'; +COMMENT ON COLUMN services_public.cors_settings.database_id IS 'Reference to the metaschema database'; +COMMENT ON COLUMN services_public.cors_settings.api_id IS 'Optional API for per-API override; NULL means database-wide default'; +COMMENT ON COLUMN services_public.cors_settings.allowed_origins IS 'Array of allowed CORS origins (e.g. https://example.com)'; + +CREATE INDEX cors_settings_database_id_idx ON services_public.cors_settings (database_id); +CREATE INDEX cors_settings_api_id_idx ON services_public.cors_settings (api_id); + +COMMIT; diff --git a/extensions/@pgpm/services/deploy/schemas/services_public/tables/database_settings/table.sql b/extensions/@pgpm/services/deploy/schemas/services_public/tables/database_settings/table.sql new file mode 100644 index 00000000..ec660def --- /dev/null +++ b/extensions/@pgpm/services/deploy/schemas/services_public/tables/database_settings/table.sql @@ -0,0 +1,53 @@ +-- Deploy schemas/services_public/tables/database_settings/table to pg + +-- requires: schemas/services_public/schema +-- requires: schemas/metaschema_public/tables/database/table + +BEGIN; + +CREATE TABLE services_public.database_settings ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL UNIQUE, + + -- GraphQL API features + enable_aggregates boolean NOT NULL DEFAULT false, + enable_postgis boolean NOT NULL DEFAULT true, + enable_search boolean NOT NULL DEFAULT true, + enable_direct_uploads boolean NOT NULL DEFAULT true, + enable_presigned_uploads boolean NOT NULL DEFAULT true, + enable_many_to_many boolean NOT NULL DEFAULT true, + enable_connection_filter boolean NOT NULL DEFAULT true, + enable_ltree boolean NOT NULL DEFAULT true, + enable_llm boolean NOT NULL DEFAULT false, + enable_realtime boolean NOT NULL DEFAULT false, + enable_bulk boolean NOT NULL DEFAULT false, + enable_i18n boolean NOT NULL DEFAULT false, + + -- Extensible JSON for future settings that don't warrant their own column + options jsonb NOT NULL DEFAULT '{}'::jsonb, + + -- + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE +); + +COMMENT ON TABLE services_public.database_settings IS 'Database-wide feature flags and settings; controls which platform features are available to all APIs in this database'; +COMMENT ON COLUMN services_public.database_settings.id IS 'Unique identifier for this settings record'; +COMMENT ON COLUMN services_public.database_settings.database_id IS 'Reference to the metaschema database these settings apply to'; +COMMENT ON COLUMN services_public.database_settings.enable_aggregates IS 'Enable aggregate queries (sum, avg, min, max, etc.) in the GraphQL API'; +COMMENT ON COLUMN services_public.database_settings.enable_postgis IS 'Enable PostGIS spatial types and operators in the GraphQL API'; +COMMENT ON COLUMN services_public.database_settings.enable_search IS 'Enable unified search (tsvector, BM25, pg_trgm, pgvector) in the GraphQL API'; +COMMENT ON COLUMN services_public.database_settings.enable_direct_uploads IS 'Enable direct (multipart) file upload mutations in the GraphQL API'; +COMMENT ON COLUMN services_public.database_settings.enable_presigned_uploads IS 'Enable presigned URL upload flow for S3/MinIO storage'; +COMMENT ON COLUMN services_public.database_settings.enable_many_to_many IS 'Enable many-to-many relationship queries in the GraphQL API'; +COMMENT ON COLUMN services_public.database_settings.enable_connection_filter IS 'Enable connection filter (where argument) in the GraphQL API'; +COMMENT ON COLUMN services_public.database_settings.enable_ltree IS 'Enable ltree hierarchical data type support in the GraphQL API'; +COMMENT ON COLUMN services_public.database_settings.enable_llm IS 'Enable LLM/AI integration features in the GraphQL API'; +COMMENT ON COLUMN services_public.database_settings.enable_realtime IS 'Enable realtime subscriptions (cursor-tracked change delivery) in the GraphQL API'; +COMMENT ON COLUMN services_public.database_settings.enable_bulk IS 'Enable bulk mutation operations (insert, upsert, update, delete) in the GraphQL API'; +COMMENT ON COLUMN services_public.database_settings.enable_i18n IS 'Enable internationalization plugin (localeStrings field, translation table discovery) in the GraphQL API'; +COMMENT ON COLUMN services_public.database_settings.options IS 'Extensible JSON for additional settings that do not have dedicated columns'; + +CREATE INDEX database_settings_database_id_idx ON services_public.database_settings (database_id); + +COMMIT; diff --git a/extensions/@pgpm/services/deploy/schemas/services_public/tables/domains/table.sql b/extensions/@pgpm/services/deploy/schemas/services_public/tables/domains/table.sql new file mode 100644 index 00000000..bf1c12e8 --- /dev/null +++ b/extensions/@pgpm/services/deploy/schemas/services_public/tables/domains/table.sql @@ -0,0 +1,46 @@ +-- Deploy schemas/services_public/tables/domains/table to pg + +-- requires: schemas/services_public/schema +-- requires: schemas/services_public/tables/apis/table +-- requires: schemas/services_public/tables/sites/table +-- requires: schemas/metaschema_public/tables/database/table + +BEGIN; + +CREATE TABLE services_public.domains ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + api_id uuid, + site_id uuid, + + subdomain hostname, + domain hostname, + + -- + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT api_fkey FOREIGN KEY (api_id) REFERENCES services_public.apis (id) ON DELETE CASCADE, + CONSTRAINT site_fkey FOREIGN KEY (site_id) REFERENCES services_public.sites (id) ON DELETE CASCADE, + CONSTRAINT one_route_chk CHECK ( + (api_id IS NULL AND site_id IS NULL) OR + (api_id IS NULL AND site_id IS NOT NULL) OR + (api_id IS NOT NULL AND site_id IS NULL) + ), + UNIQUE ( subdomain, domain ) +); + +COMMENT ON TABLE services_public.domains IS 'DNS domain and subdomain routing: maps hostnames to either an API endpoint or a site'; +COMMENT ON COLUMN services_public.domains.id IS 'Unique identifier for this domain record'; +COMMENT ON COLUMN services_public.domains.database_id IS 'Reference to the metaschema database this domain belongs to'; +COMMENT ON COLUMN services_public.domains.api_id IS 'API endpoint this domain routes to (mutually exclusive with site_id)'; +COMMENT ON COLUMN services_public.domains.site_id IS 'Site this domain routes to (mutually exclusive with api_id)'; +COMMENT ON COLUMN services_public.domains.subdomain IS 'Subdomain portion of the hostname'; +COMMENT ON COLUMN services_public.domains.domain IS 'Root domain of the hostname'; + +CREATE INDEX domains_database_id_idx ON services_public.domains ( database_id ); + +CREATE INDEX domains_api_id_idx ON services_public.domains ( api_id ); + +CREATE INDEX domains_site_id_idx ON services_public.domains ( site_id ); + +COMMIT; diff --git a/extensions/@pgpm/services/deploy/schemas/services_public/tables/pubkey_settings/table.sql b/extensions/@pgpm/services/deploy/schemas/services_public/tables/pubkey_settings/table.sql new file mode 100644 index 00000000..983da42f --- /dev/null +++ b/extensions/@pgpm/services/deploy/schemas/services_public/tables/pubkey_settings/table.sql @@ -0,0 +1,53 @@ +-- Deploy schemas/services_public/tables/pubkey_settings/table to pg + +-- requires: schemas/services_public/schema +-- requires: schemas/metaschema_public/tables/database/table +-- requires: schemas/metaschema_public/tables/schema/table +-- requires: schemas/metaschema_public/tables/function/table + +BEGIN; + +-- Per-database public-key / crypto auth runtime configuration. +-- Typed replacement for api_modules rows with name = 'pubkey_challenge'. +-- One row per database; the server reads this instead of the JSONB blob. +CREATE TABLE services_public.pubkey_settings ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL UNIQUE, + + -- Schema reference (FK to metaschema_public.schema) + schema_id uuid, + + -- Crypto auth configuration + crypto_network text NOT NULL DEFAULT 'cosmos', + user_field text NOT NULL DEFAULT 'user_id', + + -- Function references (FK to metaschema_public.function) + sign_up_with_key_function_id uuid, + sign_in_request_challenge_function_id uuid, + sign_in_record_failure_function_id uuid, + sign_in_with_challenge_function_id uuid, + + -- + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE SET NULL, + CONSTRAINT sign_up_with_key_function_fkey FOREIGN KEY (sign_up_with_key_function_id) REFERENCES metaschema_public.function (id) ON DELETE SET NULL, + CONSTRAINT sign_in_request_challenge_function_fkey FOREIGN KEY (sign_in_request_challenge_function_id) REFERENCES metaschema_public.function (id) ON DELETE SET NULL, + CONSTRAINT sign_in_record_failure_function_fkey FOREIGN KEY (sign_in_record_failure_function_id) REFERENCES metaschema_public.function (id) ON DELETE SET NULL, + CONSTRAINT sign_in_with_challenge_function_fkey FOREIGN KEY (sign_in_with_challenge_function_id) REFERENCES metaschema_public.function (id) ON DELETE SET NULL +); + +COMMENT ON TABLE services_public.pubkey_settings IS 'Per-database public-key crypto auth runtime configuration; typed replacement for api_modules pubkey_challenge JSONB entries'; +COMMENT ON COLUMN services_public.pubkey_settings.id IS 'Unique identifier for this pubkey settings record'; +COMMENT ON COLUMN services_public.pubkey_settings.database_id IS 'Reference to the metaschema database'; +COMMENT ON COLUMN services_public.pubkey_settings.schema_id IS 'Schema containing the crypto auth functions (FK to metaschema_public.schema)'; +COMMENT ON COLUMN services_public.pubkey_settings.crypto_network IS 'Crypto network for key derivation (e.g. cosmos, ethereum)'; +COMMENT ON COLUMN services_public.pubkey_settings.user_field IS 'Field name used to identify the user in crypto auth functions'; +COMMENT ON COLUMN services_public.pubkey_settings.sign_up_with_key_function_id IS 'Reference to the sign-up-with-key function (FK to metaschema_public.function)'; +COMMENT ON COLUMN services_public.pubkey_settings.sign_in_request_challenge_function_id IS 'Reference to the sign-in challenge request function (FK to metaschema_public.function)'; +COMMENT ON COLUMN services_public.pubkey_settings.sign_in_record_failure_function_id IS 'Reference to the sign-in failure recording function (FK to metaschema_public.function)'; +COMMENT ON COLUMN services_public.pubkey_settings.sign_in_with_challenge_function_id IS 'Reference to the sign-in-with-challenge function (FK to metaschema_public.function)'; + +CREATE INDEX pubkey_settings_database_id_idx ON services_public.pubkey_settings (database_id); + +COMMIT; diff --git a/extensions/@pgpm/services/deploy/schemas/services_public/tables/rls_settings/table.sql b/extensions/@pgpm/services/deploy/schemas/services_public/tables/rls_settings/table.sql new file mode 100644 index 00000000..b46cef17 --- /dev/null +++ b/extensions/@pgpm/services/deploy/schemas/services_public/tables/rls_settings/table.sql @@ -0,0 +1,56 @@ +-- Deploy schemas/services_public/tables/rls_settings/table to pg + +-- requires: schemas/services_public/schema +-- requires: schemas/metaschema_public/tables/database/table +-- requires: schemas/metaschema_public/tables/schema/table +-- requires: schemas/metaschema_public/tables/function/table + +BEGIN; + +-- Per-database RLS module runtime configuration. +-- Typed replacement for api_modules rows with name = 'rls_module'. +-- One row per database; the server reads this instead of the JSONB blob. +CREATE TABLE services_public.rls_settings ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL UNIQUE, + + -- Schema references (FK to metaschema_public.schema) + authenticate_schema_id uuid, + role_schema_id uuid, + + -- Function references (FK to metaschema_public.function) + authenticate_function_id uuid, + authenticate_strict_function_id uuid, + current_role_function_id uuid, + current_role_id_function_id uuid, + current_user_agent_function_id uuid, + current_ip_address_function_id uuid, + + -- + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT authenticate_schema_fkey FOREIGN KEY (authenticate_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE SET NULL, + CONSTRAINT role_schema_fkey FOREIGN KEY (role_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE SET NULL, + CONSTRAINT authenticate_function_fkey FOREIGN KEY (authenticate_function_id) REFERENCES metaschema_public.function (id) ON DELETE SET NULL, + CONSTRAINT authenticate_strict_function_fkey FOREIGN KEY (authenticate_strict_function_id) REFERENCES metaschema_public.function (id) ON DELETE SET NULL, + CONSTRAINT current_role_function_fkey FOREIGN KEY (current_role_function_id) REFERENCES metaschema_public.function (id) ON DELETE SET NULL, + CONSTRAINT current_role_id_function_fkey FOREIGN KEY (current_role_id_function_id) REFERENCES metaschema_public.function (id) ON DELETE SET NULL, + CONSTRAINT current_user_agent_function_fkey FOREIGN KEY (current_user_agent_function_id) REFERENCES metaschema_public.function (id) ON DELETE SET NULL, + CONSTRAINT current_ip_address_function_fkey FOREIGN KEY (current_ip_address_function_id) REFERENCES metaschema_public.function (id) ON DELETE SET NULL +); + +COMMENT ON TABLE services_public.rls_settings IS 'Per-database RLS module runtime configuration; typed replacement for api_modules rls_module JSONB entries'; +COMMENT ON COLUMN services_public.rls_settings.id IS 'Unique identifier for this RLS settings record'; +COMMENT ON COLUMN services_public.rls_settings.database_id IS 'Reference to the metaschema database'; +COMMENT ON COLUMN services_public.rls_settings.authenticate_schema_id IS 'Schema containing authenticate/authenticate_strict functions (FK to metaschema_public.schema)'; +COMMENT ON COLUMN services_public.rls_settings.role_schema_id IS 'Schema containing current_role and related functions (FK to metaschema_public.schema)'; +COMMENT ON COLUMN services_public.rls_settings.authenticate_function_id IS 'Reference to the authenticate function (FK to metaschema_public.function)'; +COMMENT ON COLUMN services_public.rls_settings.authenticate_strict_function_id IS 'Reference to the strict authenticate function (FK to metaschema_public.function)'; +COMMENT ON COLUMN services_public.rls_settings.current_role_function_id IS 'Reference to the current_role function (FK to metaschema_public.function)'; +COMMENT ON COLUMN services_public.rls_settings.current_role_id_function_id IS 'Reference to the current_role_id function (FK to metaschema_public.function)'; +COMMENT ON COLUMN services_public.rls_settings.current_user_agent_function_id IS 'Reference to the current_user_agent function (FK to metaschema_public.function)'; +COMMENT ON COLUMN services_public.rls_settings.current_ip_address_function_id IS 'Reference to the current_ip_address function (FK to metaschema_public.function)'; + +CREATE INDEX rls_settings_database_id_idx ON services_public.rls_settings (database_id); + +COMMIT; diff --git a/extensions/@pgpm/services/deploy/schemas/services_public/tables/site_metadata/table.sql b/extensions/@pgpm/services/deploy/schemas/services_public/tables/site_metadata/table.sql new file mode 100644 index 00000000..0cc0462b --- /dev/null +++ b/extensions/@pgpm/services/deploy/schemas/services_public/tables/site_metadata/table.sql @@ -0,0 +1,39 @@ +-- Deploy schemas/services_public/tables/site_metadata/table to pg + +-- requires: schemas/services_public/schema +-- requires: schemas/services_public/tables/sites/table +-- requires: schemas/metaschema_public/tables/database/table + +BEGIN; + +CREATE TABLE services_public.site_metadata ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + site_id uuid NOT NULL, + title text, + description text, + og_image image, + + -- + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + + CHECK ( character_length(title) <= 120 ), + CHECK ( character_length(description) <= 120 ) +); + + +COMMENT ON TABLE services_public.site_metadata IS 'SEO and social sharing metadata for a site: page title, description, and Open Graph image'; +COMMENT ON COLUMN services_public.site_metadata.id IS 'Unique identifier for this metadata record'; +COMMENT ON COLUMN services_public.site_metadata.database_id IS 'Reference to the metaschema database'; +COMMENT ON COLUMN services_public.site_metadata.site_id IS 'Site this metadata belongs to'; +COMMENT ON COLUMN services_public.site_metadata.title IS 'Page title for SEO (max 120 characters)'; +COMMENT ON COLUMN services_public.site_metadata.description IS 'Meta description for SEO and social sharing (max 120 characters)'; +COMMENT ON COLUMN services_public.site_metadata.og_image IS 'Open Graph image for social media previews'; + +ALTER TABLE services_public.site_metadata ADD CONSTRAINT site_metadata_site_id_fkey FOREIGN KEY ( site_id ) REFERENCES services_public.sites ( id ); +CREATE INDEX site_metadata_site_id_idx ON services_public.site_metadata ( site_id ); + +CREATE INDEX site_metadata_database_id_idx ON services_public.site_metadata ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/services/deploy/schemas/services_public/tables/site_modules/table.sql b/extensions/@pgpm/services/deploy/schemas/services_public/tables/site_modules/table.sql new file mode 100644 index 00000000..20bdf25f --- /dev/null +++ b/extensions/@pgpm/services/deploy/schemas/services_public/tables/site_modules/table.sql @@ -0,0 +1,33 @@ +-- Deploy schemas/services_public/tables/site_modules/table to pg + +-- requires: schemas/services_public/schema +-- requires: schemas/services_public/tables/sites/table +-- requires: schemas/metaschema_public/tables/database/table + +BEGIN; + +CREATE TABLE services_public.site_modules ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + site_id uuid NOT NULL, + name text NOT NULL, + data json NOT NULL, + + -- + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE +); + +COMMENT ON TABLE services_public.site_modules IS 'Site-level module configuration; stores module name and JSON settings used by the frontend or server for each site'; +COMMENT ON COLUMN services_public.site_modules.id IS 'Unique identifier for this site module record'; +COMMENT ON COLUMN services_public.site_modules.database_id IS 'Reference to the metaschema database'; +COMMENT ON COLUMN services_public.site_modules.site_id IS 'Site this module configuration belongs to'; +COMMENT ON COLUMN services_public.site_modules.name IS 'Module name (e.g. user_auth_module, analytics)'; +COMMENT ON COLUMN services_public.site_modules.data IS 'JSON configuration data for this module'; + +ALTER TABLE services_public.site_modules ADD CONSTRAINT site_modules_site_id_fkey FOREIGN KEY ( site_id ) REFERENCES services_public.sites ( id ); +CREATE INDEX site_modules_site_id_idx ON services_public.site_modules ( site_id ); + +CREATE INDEX site_modules_database_id_idx ON services_public.site_modules ( database_id ); + + +COMMIT; diff --git a/extensions/@pgpm/services/deploy/schemas/services_public/tables/site_themes/table.sql b/extensions/@pgpm/services/deploy/schemas/services_public/tables/site_themes/table.sql new file mode 100644 index 00000000..bd88291b --- /dev/null +++ b/extensions/@pgpm/services/deploy/schemas/services_public/tables/site_themes/table.sql @@ -0,0 +1,31 @@ +-- Deploy schemas/services_public/tables/site_themes/table to pg + +-- requires: schemas/services_public/schema +-- requires: schemas/services_public/tables/sites/table +-- requires: schemas/metaschema_public/tables/database/table + +BEGIN; + +CREATE TABLE services_public.site_themes ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + site_id uuid NOT NULL, + theme jsonb NOT NULL, + + -- + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE +); + +COMMENT ON TABLE services_public.site_themes IS 'Theme configuration for a site; stores design tokens, colors, and typography as JSONB'; +COMMENT ON COLUMN services_public.site_themes.id IS 'Unique identifier for this theme record'; +COMMENT ON COLUMN services_public.site_themes.database_id IS 'Reference to the metaschema database'; +COMMENT ON COLUMN services_public.site_themes.site_id IS 'Site this theme belongs to'; +COMMENT ON COLUMN services_public.site_themes.theme IS 'JSONB object containing theme tokens (colors, typography, spacing, etc.)'; + +ALTER TABLE services_public.site_themes ADD CONSTRAINT site_themes_site_id_fkey FOREIGN KEY ( site_id ) REFERENCES services_public.sites ( id ); +CREATE INDEX site_themes_site_id_idx ON services_public.site_themes ( site_id ); + +CREATE INDEX site_themes_database_id_idx ON services_public.site_themes ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/services/deploy/schemas/services_public/tables/sites/table.sql b/extensions/@pgpm/services/deploy/schemas/services_public/tables/sites/table.sql new file mode 100644 index 00000000..149a7178 --- /dev/null +++ b/extensions/@pgpm/services/deploy/schemas/services_public/tables/sites/table.sql @@ -0,0 +1,40 @@ +-- Deploy schemas/services_public/tables/sites/table to pg + +-- requires: schemas/services_public/schema +-- requires: schemas/metaschema_public/tables/database/table + +BEGIN; + +CREATE TABLE services_public.sites ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + title text, + description text, + og_image image, + favicon attachment, + apple_touch_icon image, + logo image, + + -- do we need this? + dbname text NOT NULL DEFAULT current_database(), + + -- + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT max_title CHECK ( character_length(title) <= 120 ), + CONSTRAINT max_descr CHECK ( character_length(description) <= 120 ) +); + +COMMENT ON TABLE services_public.sites IS 'Top-level site configuration: branding assets, title, and description for a deployed application'; +COMMENT ON COLUMN services_public.sites.id IS 'Unique identifier for this site'; +COMMENT ON COLUMN services_public.sites.database_id IS 'Reference to the metaschema database this site belongs to'; +COMMENT ON COLUMN services_public.sites.title IS 'Display title for the site (max 120 characters)'; +COMMENT ON COLUMN services_public.sites.description IS 'Short description of the site (max 120 characters)'; +COMMENT ON COLUMN services_public.sites.og_image IS 'Open Graph image used for social media link previews'; +COMMENT ON COLUMN services_public.sites.favicon IS 'Browser favicon attachment'; +COMMENT ON COLUMN services_public.sites.apple_touch_icon IS 'Apple touch icon for iOS home screen bookmarks'; +COMMENT ON COLUMN services_public.sites.logo IS 'Primary logo image for the site'; +COMMENT ON COLUMN services_public.sites.dbname IS 'PostgreSQL database name this site connects to'; + +CREATE INDEX sites_database_id_idx ON services_public.sites ( database_id ); + +COMMIT; diff --git a/extensions/@pgpm/services/deploy/schemas/services_public/tables/webauthn_settings/table.sql b/extensions/@pgpm/services/deploy/schemas/services_public/tables/webauthn_settings/table.sql new file mode 100644 index 00000000..326b42b3 --- /dev/null +++ b/extensions/@pgpm/services/deploy/schemas/services_public/tables/webauthn_settings/table.sql @@ -0,0 +1,84 @@ +-- Deploy schemas/services_public/tables/webauthn_settings/table to pg + +-- requires: schemas/services_public/schema +-- requires: schemas/metaschema_public/tables/database/table +-- requires: schemas/metaschema_public/tables/schema/table +-- requires: schemas/metaschema_public/tables/table/table +-- requires: schemas/metaschema_public/tables/field/table + +BEGIN; + +-- Per-database WebAuthn / passkey runtime configuration. +-- Typed replacement for api_modules rows with name = 'webauthn_challenge'. +-- One row per database; the server reads this instead of the JSONB blob. +CREATE TABLE services_public.webauthn_settings ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL UNIQUE, + + -- Schema references (FK to metaschema_public.schema) + schema_id uuid, + credentials_schema_id uuid, + sessions_schema_id uuid, + session_secrets_schema_id uuid, + + -- Table references (FK to metaschema_public.table) + credentials_table_id uuid, + sessions_table_id uuid, + session_credentials_table_id uuid, + session_secrets_table_id uuid, + + -- Field reference (FK to metaschema_public.field) + user_field_id uuid, + + -- Relying Party configuration + rp_id text NOT NULL DEFAULT '', + rp_name text NOT NULL DEFAULT '', + origin_allowlist text[] NOT NULL DEFAULT '{}', + + -- WebAuthn registration/authentication options + attestation_type text NOT NULL DEFAULT 'none' + CHECK (attestation_type IN ('none', 'indirect', 'direct', 'enterprise')), + require_user_verification boolean NOT NULL DEFAULT false, + resident_key text NOT NULL DEFAULT 'required' + CHECK (resident_key IN ('discouraged', 'preferred', 'required')), + + -- Challenge TTL in seconds (5 minutes = 300s) + challenge_expiry_seconds bigint NOT NULL DEFAULT 300, + + -- + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE SET NULL, + CONSTRAINT credentials_schema_fkey FOREIGN KEY (credentials_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE SET NULL, + CONSTRAINT sessions_schema_fkey FOREIGN KEY (sessions_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE SET NULL, + CONSTRAINT session_secrets_schema_fkey FOREIGN KEY (session_secrets_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE SET NULL, + CONSTRAINT credentials_table_fkey FOREIGN KEY (credentials_table_id) REFERENCES metaschema_public.table (id) ON DELETE SET NULL, + CONSTRAINT sessions_table_fkey FOREIGN KEY (sessions_table_id) REFERENCES metaschema_public.table (id) ON DELETE SET NULL, + CONSTRAINT session_credentials_table_fkey FOREIGN KEY (session_credentials_table_id) REFERENCES metaschema_public.table (id) ON DELETE SET NULL, + CONSTRAINT session_secrets_table_fkey FOREIGN KEY (session_secrets_table_id) REFERENCES metaschema_public.table (id) ON DELETE SET NULL, + CONSTRAINT user_field_fkey FOREIGN KEY (user_field_id) REFERENCES metaschema_public.field (id) ON DELETE SET NULL +); + +COMMENT ON TABLE services_public.webauthn_settings IS 'Per-database WebAuthn/passkey runtime configuration; typed replacement for api_modules webauthn_challenge JSONB entries'; +COMMENT ON COLUMN services_public.webauthn_settings.id IS 'Unique identifier for this WebAuthn settings record'; +COMMENT ON COLUMN services_public.webauthn_settings.database_id IS 'Reference to the metaschema database'; +COMMENT ON COLUMN services_public.webauthn_settings.schema_id IS 'Schema containing WebAuthn auth procedures (FK to metaschema_public.schema)'; +COMMENT ON COLUMN services_public.webauthn_settings.credentials_schema_id IS 'Schema of the webauthn_credentials table (FK to metaschema_public.schema)'; +COMMENT ON COLUMN services_public.webauthn_settings.sessions_schema_id IS 'Schema of the sessions table (FK to metaschema_public.schema)'; +COMMENT ON COLUMN services_public.webauthn_settings.session_secrets_schema_id IS 'Schema of the session_secrets table (FK to metaschema_public.schema)'; +COMMENT ON COLUMN services_public.webauthn_settings.credentials_table_id IS 'Reference to the webauthn_credentials table (FK to metaschema_public.table)'; +COMMENT ON COLUMN services_public.webauthn_settings.sessions_table_id IS 'Reference to the sessions table (FK to metaschema_public.table)'; +COMMENT ON COLUMN services_public.webauthn_settings.session_credentials_table_id IS 'Reference to the session_credentials table (FK to metaschema_public.table)'; +COMMENT ON COLUMN services_public.webauthn_settings.session_secrets_table_id IS 'Reference to the session_secrets table (FK to metaschema_public.table)'; +COMMENT ON COLUMN services_public.webauthn_settings.user_field_id IS 'Reference to the user field on webauthn_credentials (FK to metaschema_public.field)'; +COMMENT ON COLUMN services_public.webauthn_settings.rp_id IS 'WebAuthn Relying Party ID (typically the domain name)'; +COMMENT ON COLUMN services_public.webauthn_settings.rp_name IS 'WebAuthn Relying Party display name'; +COMMENT ON COLUMN services_public.webauthn_settings.origin_allowlist IS 'Allowed origins for WebAuthn registration and authentication'; +COMMENT ON COLUMN services_public.webauthn_settings.attestation_type IS 'Attestation conveyance preference (none, indirect, direct, enterprise)'; +COMMENT ON COLUMN services_public.webauthn_settings.require_user_verification IS 'Whether to require user verification (biometric/PIN) during auth'; +COMMENT ON COLUMN services_public.webauthn_settings.resident_key IS 'Resident key requirement (discouraged, preferred, required)'; +COMMENT ON COLUMN services_public.webauthn_settings.challenge_expiry_seconds IS 'Challenge TTL in seconds (default 300 = 5 minutes)'; + +CREATE INDEX webauthn_settings_database_id_idx ON services_public.webauthn_settings (database_id); + +COMMIT; diff --git a/extensions/@pgpm/services/jest.config.js b/extensions/@pgpm/services/jest.config.js new file mode 100644 index 00000000..e20e7efb --- /dev/null +++ b/extensions/@pgpm/services/jest.config.js @@ -0,0 +1,15 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + + // Match both __tests__ and colocated test files + testMatch: ['**/?(*.)+(test|spec).{ts,tsx,js,jsx}'], + + // Ignore build artifacts and type declarations + testPathIgnorePatterns: ['/dist/', '\\.d\\.ts$'], + modulePathIgnorePatterns: ['/dist/'], + watchPathIgnorePatterns: ['/dist/'], + + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], +}; diff --git a/extensions/@pgpm/services/package.json b/extensions/@pgpm/services/package.json new file mode 100644 index 00000000..80a7dee4 --- /dev/null +++ b/extensions/@pgpm/services/package.json @@ -0,0 +1,39 @@ +{ + "name": "@pgpm/services", + "version": "0.26.5", + "description": "Services schemas for module registration and service configuration", + "author": "Dan Lynch ", + "contributors": [ + "Constructive " + ], + "keywords": [ + "postgresql", + "pgpm", + "services", + "modules" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "bundle": "pgpm package", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@pgpm/metaschema-schema": "0.26.5", + "@pgpm/verify": "0.26.0" + }, + "devDependencies": { + "pgpm": "^4.23.2" + }, + "repository": { + "type": "git", + "url": "https://github.com/constructive-io/constructive-db" + }, + "homepage": "https://github.com/constructive-io/constructive-db", + "bugs": { + "url": "https://github.com/constructive-io/constructive-db/issues" + }, + "gitHead": "a496a00d89c37d874f4a7207265b9972b6f05c7d" +} diff --git a/extensions/@pgpm/services/pgpm.plan b/extensions/@pgpm/services/pgpm.plan new file mode 100644 index 00000000..8b75429b --- /dev/null +++ b/extensions/@pgpm/services/pgpm.plan @@ -0,0 +1,23 @@ +%syntax-version=1.0.0 +%project=services +%uri=services + +schemas/services_private/schema 2017-08-11T08:11:51Z skitch # add schemas/services_private/schema +schemas/services_public/schema 2017-08-11T08:11:51Z skitch # add schemas/services_public/schema +schemas/services_public/tables/apis/table [schemas/services_public/schema metaschema-schema:schemas/metaschema_public/tables/database/table] 2017-08-11T08:11:51Z skitch # add schemas/services_public/tables/apis/table +schemas/services_public/tables/api_modules/table [schemas/services_public/schema schemas/services_public/tables/apis/table metaschema-schema:schemas/metaschema_public/tables/database/table] 2017-08-11T08:11:51Z skitch # add schemas/services_public/tables/api_modules/table +schemas/services_public/tables/api_schemas/table [schemas/services_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/services_public/tables/api_schemas/table +schemas/services_public/tables/sites/table [schemas/services_public/schema metaschema-schema:schemas/metaschema_public/tables/database/table] 2017-08-11T08:11:51Z skitch # add schemas/services_public/tables/sites/table +schemas/services_public/tables/apps/table [schemas/services_public/schema schemas/services_public/tables/sites/table metaschema-schema:schemas/metaschema_public/tables/database/table] 2017-08-11T08:11:51Z skitch # add schemas/services_public/tables/apps/table +schemas/services_public/tables/domains/table [schemas/services_public/schema schemas/services_public/tables/apis/table schemas/services_public/tables/sites/table metaschema-schema:schemas/metaschema_public/tables/database/table] 2017-08-11T08:11:51Z skitch # add schemas/services_public/tables/domains/table +schemas/services_public/tables/site_metadata/table [schemas/services_public/schema schemas/services_public/tables/sites/table metaschema-schema:schemas/metaschema_public/tables/database/table] 2017-08-11T08:11:51Z skitch # add schemas/services_public/tables/site_metadata/table +schemas/services_public/tables/site_modules/table [schemas/services_public/schema schemas/services_public/tables/sites/table metaschema-schema:schemas/metaschema_public/tables/database/table] 2017-08-11T08:11:51Z skitch # add schemas/services_public/tables/site_modules/table +schemas/services_public/tables/site_themes/table [schemas/services_public/schema schemas/services_public/tables/sites/table metaschema-schema:schemas/metaschema_public/tables/database/table] 2017-08-11T08:11:51Z skitch # add schemas/services_public/tables/site_themes/table +schemas/services_private/triggers/enforce_api_table_name_uniqueness [schemas/services_private/schema schemas/services_public/tables/api_schemas/table metaschema-schema:schemas/metaschema_public/tables/table/table metaschema-schema:schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx] 2026-02-27T00:00:00Z devin # add API-level table name uniqueness trigger +schemas/services_private/triggers/enforce_api_schema_table_name_uniqueness [schemas/services_private/schema schemas/services_public/tables/api_schemas/table metaschema-schema:schemas/metaschema_public/tables/table/table metaschema-schema:schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx] 2026-02-27T00:00:00Z devin # add API-schema link table name uniqueness trigger +schemas/services_public/tables/database_settings/table [schemas/services_public/schema metaschema-schema:schemas/metaschema_public/tables/database/table] 2026-05-08T00:00:00Z devin # add database-wide feature flags and settings +schemas/services_public/tables/api_settings/table [schemas/services_public/schema schemas/services_public/tables/apis/table schemas/services_public/tables/database_settings/table metaschema-schema:schemas/metaschema_public/tables/database/table] 2026-05-08T00:00:00Z devin # add per-API feature flag overrides +schemas/services_public/tables/rls_settings/table [schemas/services_public/schema metaschema-schema:schemas/metaschema_public/tables/database/table metaschema-schema:schemas/metaschema_public/tables/schema/table metaschema-schema:schemas/metaschema_public/tables/function/table] 2026-05-09T00:00:00Z devin # add typed RLS module runtime settings with UUID FKs +schemas/services_public/tables/cors_settings/table [schemas/services_public/schema schemas/services_public/tables/apis/table metaschema-schema:schemas/metaschema_public/tables/database/table] 2026-05-09T00:00:00Z devin # add typed CORS origin settings +schemas/services_public/tables/pubkey_settings/table [schemas/services_public/schema metaschema-schema:schemas/metaschema_public/tables/database/table metaschema-schema:schemas/metaschema_public/tables/schema/table metaschema-schema:schemas/metaschema_public/tables/function/table] 2026-05-09T00:00:00Z devin # add typed pubkey crypto auth settings with UUID FKs +schemas/services_public/tables/webauthn_settings/table [schemas/services_public/schema metaschema-schema:schemas/metaschema_public/tables/database/table metaschema-schema:schemas/metaschema_public/tables/schema/table metaschema-schema:schemas/metaschema_public/tables/table/table metaschema-schema:schemas/metaschema_public/tables/field/table] 2026-05-09T00:00:00Z devin # add typed WebAuthn/passkey settings with UUID FKs diff --git a/extensions/@pgpm/services/revert/schemas/services_private/schema.sql b/extensions/@pgpm/services/revert/schemas/services_private/schema.sql new file mode 100644 index 00000000..710f99c9 --- /dev/null +++ b/extensions/@pgpm/services/revert/schemas/services_private/schema.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_private/schema from pg + +BEGIN; + +DROP SCHEMA services_private; + +COMMIT; diff --git a/extensions/@pgpm/services/revert/schemas/services_private/triggers/enforce_api_schema_table_name_uniqueness.sql b/extensions/@pgpm/services/revert/schemas/services_private/triggers/enforce_api_schema_table_name_uniqueness.sql new file mode 100644 index 00000000..c7e253f5 --- /dev/null +++ b/extensions/@pgpm/services/revert/schemas/services_private/triggers/enforce_api_schema_table_name_uniqueness.sql @@ -0,0 +1,8 @@ +-- Revert schemas/services_private/triggers/enforce_api_schema_table_name_uniqueness + +BEGIN; + +DROP TRIGGER IF EXISTS _000001_enforce_api_schema_table_name_uniqueness ON services_public.api_schemas; +DROP FUNCTION IF EXISTS services_private.tg_enforce_api_schema_table_name_uniqueness(); + +COMMIT; diff --git a/extensions/@pgpm/services/revert/schemas/services_private/triggers/enforce_api_table_name_uniqueness.sql b/extensions/@pgpm/services/revert/schemas/services_private/triggers/enforce_api_table_name_uniqueness.sql new file mode 100644 index 00000000..9d9cc8ea --- /dev/null +++ b/extensions/@pgpm/services/revert/schemas/services_private/triggers/enforce_api_table_name_uniqueness.sql @@ -0,0 +1,9 @@ +-- Revert schemas/services_private/triggers/enforce_api_table_name_uniqueness + +BEGIN; + +DROP TRIGGER IF EXISTS _000003_enforce_api_table_name_uniqueness_update ON metaschema_public.table; +DROP TRIGGER IF EXISTS _000003_enforce_api_table_name_uniqueness ON metaschema_public.table; +DROP FUNCTION IF EXISTS services_private.tg_enforce_api_table_name_uniqueness(); + +COMMIT; diff --git a/extensions/@pgpm/services/revert/schemas/services_public/schema.sql b/extensions/@pgpm/services/revert/schemas/services_public/schema.sql new file mode 100644 index 00000000..3fd696ac --- /dev/null +++ b/extensions/@pgpm/services/revert/schemas/services_public/schema.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/schema from pg + +BEGIN; + +DROP SCHEMA services_public; + +COMMIT; diff --git a/extensions/@pgpm/services/revert/schemas/services_public/tables/api_modules/table.sql b/extensions/@pgpm/services/revert/schemas/services_public/tables/api_modules/table.sql new file mode 100644 index 00000000..65543be1 --- /dev/null +++ b/extensions/@pgpm/services/revert/schemas/services_public/tables/api_modules/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/api_modules/table from pg + +BEGIN; + +DROP TABLE services_public.api_modules; + +COMMIT; diff --git a/extensions/@pgpm/services/revert/schemas/services_public/tables/api_schemas/table.sql b/extensions/@pgpm/services/revert/schemas/services_public/tables/api_schemas/table.sql new file mode 100644 index 00000000..8a310db7 --- /dev/null +++ b/extensions/@pgpm/services/revert/schemas/services_public/tables/api_schemas/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/api_schemas/table from pg + +BEGIN; + +DROP TABLE services_public.api_schemas; + +COMMIT; diff --git a/extensions/@pgpm/services/revert/schemas/services_public/tables/api_settings/table.sql b/extensions/@pgpm/services/revert/schemas/services_public/tables/api_settings/table.sql new file mode 100644 index 00000000..8e507fe9 --- /dev/null +++ b/extensions/@pgpm/services/revert/schemas/services_public/tables/api_settings/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/api_settings/table + +BEGIN; + +DROP TABLE IF EXISTS services_public.api_settings; + +COMMIT; diff --git a/extensions/@pgpm/services/revert/schemas/services_public/tables/apis/table.sql b/extensions/@pgpm/services/revert/schemas/services_public/tables/apis/table.sql new file mode 100644 index 00000000..2feff0a6 --- /dev/null +++ b/extensions/@pgpm/services/revert/schemas/services_public/tables/apis/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/apis/table from pg + +BEGIN; + +DROP TABLE services_public.apis; + +COMMIT; diff --git a/extensions/@pgpm/services/revert/schemas/services_public/tables/apps/table.sql b/extensions/@pgpm/services/revert/schemas/services_public/tables/apps/table.sql new file mode 100644 index 00000000..816bf6d3 --- /dev/null +++ b/extensions/@pgpm/services/revert/schemas/services_public/tables/apps/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/apps/table from pg + +BEGIN; + +DROP TABLE services_public.apps; + +COMMIT; diff --git a/extensions/@pgpm/services/revert/schemas/services_public/tables/cors_settings/table.sql b/extensions/@pgpm/services/revert/schemas/services_public/tables/cors_settings/table.sql new file mode 100644 index 00000000..0ff2d58d --- /dev/null +++ b/extensions/@pgpm/services/revert/schemas/services_public/tables/cors_settings/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/cors_settings/table + +BEGIN; + +DROP TABLE IF EXISTS services_public.cors_settings; + +COMMIT; diff --git a/extensions/@pgpm/services/revert/schemas/services_public/tables/database_settings/table.sql b/extensions/@pgpm/services/revert/schemas/services_public/tables/database_settings/table.sql new file mode 100644 index 00000000..dbc13091 --- /dev/null +++ b/extensions/@pgpm/services/revert/schemas/services_public/tables/database_settings/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/database_settings/table + +BEGIN; + +DROP TABLE IF EXISTS services_public.database_settings; + +COMMIT; diff --git a/extensions/@pgpm/services/revert/schemas/services_public/tables/domains/table.sql b/extensions/@pgpm/services/revert/schemas/services_public/tables/domains/table.sql new file mode 100644 index 00000000..44b47a3e --- /dev/null +++ b/extensions/@pgpm/services/revert/schemas/services_public/tables/domains/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/domains/table from pg + +BEGIN; + +DROP TABLE services_public.domains; + +COMMIT; diff --git a/extensions/@pgpm/services/revert/schemas/services_public/tables/pubkey_settings/table.sql b/extensions/@pgpm/services/revert/schemas/services_public/tables/pubkey_settings/table.sql new file mode 100644 index 00000000..c1a12a23 --- /dev/null +++ b/extensions/@pgpm/services/revert/schemas/services_public/tables/pubkey_settings/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/pubkey_settings/table + +BEGIN; + +DROP TABLE IF EXISTS services_public.pubkey_settings; + +COMMIT; diff --git a/extensions/@pgpm/services/revert/schemas/services_public/tables/rls_settings/table.sql b/extensions/@pgpm/services/revert/schemas/services_public/tables/rls_settings/table.sql new file mode 100644 index 00000000..ca78acb0 --- /dev/null +++ b/extensions/@pgpm/services/revert/schemas/services_public/tables/rls_settings/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/rls_settings/table + +BEGIN; + +DROP TABLE IF EXISTS services_public.rls_settings; + +COMMIT; diff --git a/extensions/@pgpm/services/revert/schemas/services_public/tables/site_metadata/table.sql b/extensions/@pgpm/services/revert/schemas/services_public/tables/site_metadata/table.sql new file mode 100644 index 00000000..cef080d5 --- /dev/null +++ b/extensions/@pgpm/services/revert/schemas/services_public/tables/site_metadata/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/site_metadata/table from pg + +BEGIN; + +DROP TABLE services_public.site_metadata; + +COMMIT; diff --git a/extensions/@pgpm/services/revert/schemas/services_public/tables/site_modules/table.sql b/extensions/@pgpm/services/revert/schemas/services_public/tables/site_modules/table.sql new file mode 100644 index 00000000..a63f2042 --- /dev/null +++ b/extensions/@pgpm/services/revert/schemas/services_public/tables/site_modules/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/site_modules/table from pg + +BEGIN; + +DROP TABLE services_public.site_modules; + +COMMIT; diff --git a/extensions/@pgpm/services/revert/schemas/services_public/tables/site_themes/table.sql b/extensions/@pgpm/services/revert/schemas/services_public/tables/site_themes/table.sql new file mode 100644 index 00000000..21f2965c --- /dev/null +++ b/extensions/@pgpm/services/revert/schemas/services_public/tables/site_themes/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/site_themes/table from pg + +BEGIN; + +DROP TABLE services_public.site_themes; + +COMMIT; diff --git a/extensions/@pgpm/services/revert/schemas/services_public/tables/sites/table.sql b/extensions/@pgpm/services/revert/schemas/services_public/tables/sites/table.sql new file mode 100644 index 00000000..913178bb --- /dev/null +++ b/extensions/@pgpm/services/revert/schemas/services_public/tables/sites/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/sites/table from pg + +BEGIN; + +DROP TABLE services_public.sites; + +COMMIT; diff --git a/extensions/@pgpm/services/revert/schemas/services_public/tables/webauthn_settings/table.sql b/extensions/@pgpm/services/revert/schemas/services_public/tables/webauthn_settings/table.sql new file mode 100644 index 00000000..16e6667b --- /dev/null +++ b/extensions/@pgpm/services/revert/schemas/services_public/tables/webauthn_settings/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/services_public/tables/webauthn_settings/table + +BEGIN; + +DROP TABLE IF EXISTS services_public.webauthn_settings; + +COMMIT; diff --git a/extensions/@pgpm/services/services.control b/extensions/@pgpm/services/services.control new file mode 100644 index 00000000..8f6e433d --- /dev/null +++ b/extensions/@pgpm/services/services.control @@ -0,0 +1,7 @@ +# services extension +comment = 'services extension - schemas for module registration and service configuration' +default_version = '0.26.3' +module_pathname = '$libdir/services' +requires = 'plpgsql,metaschema-schema,pgpm-verify' +relocatable = false +superuser = false diff --git a/extensions/@pgpm/services/sql/services--0.26.3.sql b/extensions/@pgpm/services/sql/services--0.26.3.sql new file mode 100644 index 00000000..b1d54b02 --- /dev/null +++ b/extensions/@pgpm/services/sql/services--0.26.3.sql @@ -0,0 +1,838 @@ +\echo Use "CREATE EXTENSION services" to load this file. \quit +CREATE SCHEMA services_private; + +GRANT USAGE ON SCHEMA services_private TO authenticated; + +GRANT USAGE ON SCHEMA services_private TO administrator; + +ALTER DEFAULT PRIVILEGES IN SCHEMA services_private + GRANT ALL ON TABLES TO administrator; + +ALTER DEFAULT PRIVILEGES IN SCHEMA services_private + GRANT ALL ON SEQUENCES TO administrator; + +ALTER DEFAULT PRIVILEGES IN SCHEMA services_private + GRANT ALL ON FUNCTIONS TO administrator; + +CREATE SCHEMA services_public; + +GRANT USAGE ON SCHEMA services_public TO authenticated; + +GRANT USAGE ON SCHEMA services_public TO administrator; + +ALTER DEFAULT PRIVILEGES IN SCHEMA services_public + GRANT ALL ON TABLES TO administrator; + +ALTER DEFAULT PRIVILEGES IN SCHEMA services_public + GRANT ALL ON SEQUENCES TO administrator; + +ALTER DEFAULT PRIVILEGES IN SCHEMA services_public + GRANT ALL ON FUNCTIONS TO administrator; + +CREATE TABLE services_public.apis ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + name text NOT NULL, + dbname text NOT NULL DEFAULT current_database(), + role_name text NOT NULL DEFAULT 'authenticated', + anon_role text NOT NULL DEFAULT 'anonymous', + is_public boolean NOT NULL DEFAULT true, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + UNIQUE (database_id, name) +); + +COMMENT ON TABLE services_public.apis IS 'API endpoint configurations: each record defines a PostGraphile/PostgREST API with its database role and public access settings'; + +COMMENT ON COLUMN services_public.apis.id IS 'Unique identifier for this API'; + +COMMENT ON COLUMN services_public.apis.database_id IS 'Reference to the metaschema database this API serves'; + +COMMENT ON COLUMN services_public.apis.name IS 'Unique name for this API within its database'; + +COMMENT ON COLUMN services_public.apis.dbname IS 'PostgreSQL database name to connect to'; + +COMMENT ON COLUMN services_public.apis.role_name IS 'PostgreSQL role used for authenticated requests'; + +COMMENT ON COLUMN services_public.apis.anon_role IS 'PostgreSQL role used for anonymous/unauthenticated requests'; + +COMMENT ON COLUMN services_public.apis.is_public IS 'Whether this API is publicly accessible without authentication'; + +CREATE INDEX apis_database_id_idx ON services_public.apis (database_id); + +CREATE TABLE services_public.api_modules ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + api_id uuid NOT NULL, + name text NOT NULL, + data pg_catalog.json NOT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE +); + +COMMENT ON TABLE services_public.api_modules IS 'Server-side module configuration for an API endpoint; stores module name and JSON settings used by the application server'; + +COMMENT ON COLUMN services_public.api_modules.id IS 'Unique identifier for this API module record'; + +COMMENT ON COLUMN services_public.api_modules.database_id IS 'Reference to the metaschema database'; + +COMMENT ON COLUMN services_public.api_modules.api_id IS 'API this module configuration belongs to'; + +COMMENT ON COLUMN services_public.api_modules.name IS 'Module name (e.g. auth, uploads, webhooks)'; + +COMMENT ON COLUMN services_public.api_modules.data IS 'JSON configuration data for this module'; + +ALTER TABLE services_public.api_modules + ADD CONSTRAINT api_modules_api_id_fkey + FOREIGN KEY(api_id) + REFERENCES services_public.apis (id); + +CREATE INDEX api_modules_api_id_idx ON services_public.api_modules (api_id); + +CREATE INDEX api_modules_database_id_idx ON services_public.api_modules (database_id); + +CREATE TABLE services_public.api_schemas ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL, + api_id uuid NOT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT api_fkey + FOREIGN KEY(api_id) + REFERENCES services_public.apis (id) + ON DELETE CASCADE, + UNIQUE (api_id, schema_id) +); + +COMMENT ON TABLE services_public.api_schemas IS 'Join table linking APIs to the database schemas they expose; controls which schemas are accessible through each API'; + +COMMENT ON COLUMN services_public.api_schemas.id IS 'Unique identifier for this API-schema mapping'; + +COMMENT ON COLUMN services_public.api_schemas.database_id IS 'Reference to the metaschema database'; + +COMMENT ON COLUMN services_public.api_schemas.schema_id IS 'Metaschema schema being exposed through the API'; + +COMMENT ON COLUMN services_public.api_schemas.api_id IS 'API that exposes this schema'; + +CREATE INDEX api_schemas_database_id_idx ON services_public.api_schemas (database_id); + +CREATE INDEX api_schemas_schema_id_idx ON services_public.api_schemas (schema_id); + +CREATE INDEX api_schemas_api_id_idx ON services_public.api_schemas (api_id); + +CREATE TABLE services_public.sites ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + title text, + description text, + og_image image, + favicon attachment, + apple_touch_icon image, + logo image, + dbname text NOT NULL DEFAULT current_database(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT max_title + CHECK (character_length(title) <= 120), + CONSTRAINT max_descr + CHECK (character_length(description) <= 120) +); + +COMMENT ON TABLE services_public.sites IS 'Top-level site configuration: branding assets, title, and description for a deployed application'; + +COMMENT ON COLUMN services_public.sites.id IS 'Unique identifier for this site'; + +COMMENT ON COLUMN services_public.sites.database_id IS 'Reference to the metaschema database this site belongs to'; + +COMMENT ON COLUMN services_public.sites.title IS 'Display title for the site (max 120 characters)'; + +COMMENT ON COLUMN services_public.sites.description IS 'Short description of the site (max 120 characters)'; + +COMMENT ON COLUMN services_public.sites.og_image IS 'Open Graph image used for social media link previews'; + +COMMENT ON COLUMN services_public.sites.favicon IS 'Browser favicon attachment'; + +COMMENT ON COLUMN services_public.sites.apple_touch_icon IS 'Apple touch icon for iOS home screen bookmarks'; + +COMMENT ON COLUMN services_public.sites.logo IS 'Primary logo image for the site'; + +COMMENT ON COLUMN services_public.sites.dbname IS 'PostgreSQL database name this site connects to'; + +CREATE INDEX sites_database_id_idx ON services_public.sites (database_id); + +CREATE TABLE services_public.apps ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + site_id uuid NOT NULL, + name text, + app_image image, + app_store_link url, + app_store_id text, + app_id_prefix text, + play_store_link url, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + UNIQUE (site_id) +); + +COMMENT ON TABLE services_public.apps IS 'Mobile and native app configuration linked to a site, including store links and identifiers'; + +COMMENT ON COLUMN services_public.apps.id IS 'Unique identifier for this app'; + +COMMENT ON COLUMN services_public.apps.database_id IS 'Reference to the metaschema database this app belongs to'; + +COMMENT ON COLUMN services_public.apps.site_id IS 'Site this app is associated with (one app per site)'; + +COMMENT ON COLUMN services_public.apps.name IS 'Display name of the app'; + +COMMENT ON COLUMN services_public.apps.app_image IS 'App icon or promotional image'; + +COMMENT ON COLUMN services_public.apps.app_store_link IS 'URL to the Apple App Store listing'; + +COMMENT ON COLUMN services_public.apps.app_store_id IS 'Apple App Store application identifier'; + +COMMENT ON COLUMN services_public.apps.app_id_prefix IS 'Apple App ID prefix (Team ID) for universal links and associated domains'; + +COMMENT ON COLUMN services_public.apps.play_store_link IS 'URL to the Google Play Store listing'; + +ALTER TABLE services_public.apps + ADD CONSTRAINT apps_site_id_fkey + FOREIGN KEY(site_id) + REFERENCES services_public.sites (id); + +CREATE INDEX apps_site_id_idx ON services_public.apps (site_id); + +CREATE INDEX apps_database_id_idx ON services_public.apps (database_id); + +CREATE TABLE services_public.domains ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + api_id uuid, + site_id uuid, + subdomain hostname, + domain hostname, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT api_fkey + FOREIGN KEY(api_id) + REFERENCES services_public.apis (id) + ON DELETE CASCADE, + CONSTRAINT site_fkey + FOREIGN KEY(site_id) + REFERENCES services_public.sites (id) + ON DELETE CASCADE, + CONSTRAINT one_route_chk + CHECK ( + (api_id IS NULL + AND site_id IS NULL) + OR (api_id IS NULL + AND site_id IS NOT NULL) + OR (api_id IS NOT NULL + AND site_id IS NULL) + ), + UNIQUE (subdomain, domain) +); + +COMMENT ON TABLE services_public.domains IS 'DNS domain and subdomain routing: maps hostnames to either an API endpoint or a site'; + +COMMENT ON COLUMN services_public.domains.id IS 'Unique identifier for this domain record'; + +COMMENT ON COLUMN services_public.domains.database_id IS 'Reference to the metaschema database this domain belongs to'; + +COMMENT ON COLUMN services_public.domains.api_id IS 'API endpoint this domain routes to (mutually exclusive with site_id)'; + +COMMENT ON COLUMN services_public.domains.site_id IS 'Site this domain routes to (mutually exclusive with api_id)'; + +COMMENT ON COLUMN services_public.domains.subdomain IS 'Subdomain portion of the hostname'; + +COMMENT ON COLUMN services_public.domains.domain IS 'Root domain of the hostname'; + +CREATE INDEX domains_database_id_idx ON services_public.domains (database_id); + +CREATE INDEX domains_api_id_idx ON services_public.domains (api_id); + +CREATE INDEX domains_site_id_idx ON services_public.domains (site_id); + +CREATE TABLE services_public.site_metadata ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + site_id uuid NOT NULL, + title text, + description text, + og_image image, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CHECK (character_length(title) <= 120), + CHECK (character_length(description) <= 120) +); + +COMMENT ON TABLE services_public.site_metadata IS 'SEO and social sharing metadata for a site: page title, description, and Open Graph image'; + +COMMENT ON COLUMN services_public.site_metadata.id IS 'Unique identifier for this metadata record'; + +COMMENT ON COLUMN services_public.site_metadata.database_id IS 'Reference to the metaschema database'; + +COMMENT ON COLUMN services_public.site_metadata.site_id IS 'Site this metadata belongs to'; + +COMMENT ON COLUMN services_public.site_metadata.title IS 'Page title for SEO (max 120 characters)'; + +COMMENT ON COLUMN services_public.site_metadata.description IS 'Meta description for SEO and social sharing (max 120 characters)'; + +COMMENT ON COLUMN services_public.site_metadata.og_image IS 'Open Graph image for social media previews'; + +ALTER TABLE services_public.site_metadata + ADD CONSTRAINT site_metadata_site_id_fkey + FOREIGN KEY(site_id) + REFERENCES services_public.sites (id); + +CREATE INDEX site_metadata_site_id_idx ON services_public.site_metadata (site_id); + +CREATE INDEX site_metadata_database_id_idx ON services_public.site_metadata (database_id); + +CREATE TABLE services_public.site_modules ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + site_id uuid NOT NULL, + name text NOT NULL, + data pg_catalog.json NOT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE +); + +COMMENT ON TABLE services_public.site_modules IS 'Site-level module configuration; stores module name and JSON settings used by the frontend or server for each site'; + +COMMENT ON COLUMN services_public.site_modules.id IS 'Unique identifier for this site module record'; + +COMMENT ON COLUMN services_public.site_modules.database_id IS 'Reference to the metaschema database'; + +COMMENT ON COLUMN services_public.site_modules.site_id IS 'Site this module configuration belongs to'; + +COMMENT ON COLUMN services_public.site_modules.name IS 'Module name (e.g. user_auth_module, analytics)'; + +COMMENT ON COLUMN services_public.site_modules.data IS 'JSON configuration data for this module'; + +ALTER TABLE services_public.site_modules + ADD CONSTRAINT site_modules_site_id_fkey + FOREIGN KEY(site_id) + REFERENCES services_public.sites (id); + +CREATE INDEX site_modules_site_id_idx ON services_public.site_modules (site_id); + +CREATE INDEX site_modules_database_id_idx ON services_public.site_modules (database_id); + +CREATE TABLE services_public.site_themes ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + site_id uuid NOT NULL, + theme jsonb NOT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE +); + +COMMENT ON TABLE services_public.site_themes IS 'Theme configuration for a site; stores design tokens, colors, and typography as JSONB'; + +COMMENT ON COLUMN services_public.site_themes.id IS 'Unique identifier for this theme record'; + +COMMENT ON COLUMN services_public.site_themes.database_id IS 'Reference to the metaschema database'; + +COMMENT ON COLUMN services_public.site_themes.site_id IS 'Site this theme belongs to'; + +COMMENT ON COLUMN services_public.site_themes.theme IS 'JSONB object containing theme tokens (colors, typography, spacing, etc.)'; + +ALTER TABLE services_public.site_themes + ADD CONSTRAINT site_themes_site_id_fkey + FOREIGN KEY(site_id) + REFERENCES services_public.sites (id); + +CREATE INDEX site_themes_site_id_idx ON services_public.site_themes (site_id); + +CREATE INDEX site_themes_database_id_idx ON services_public.site_themes (database_id); + +CREATE FUNCTION services_private.tg_enforce_api_table_name_uniqueness() RETURNS trigger AS $EOFCODE$ +DECLARE + new_name_hash bytea; + conflicting_api_name text; + conflicting_table_name text; +BEGIN + -- Compute the plural-hash of the new table name + new_name_hash := metaschema_private.table_name_hash(NEW.name); + + -- Check if any API that includes this table's schema also includes + -- another schema containing a table with the same name hash + SELECT a.name, t.name + INTO conflicting_api_name, conflicting_table_name + FROM services_public.api_schemas AS my_api + JOIN services_public.api_schemas AS other_api + ON other_api.api_id = my_api.api_id + AND other_api.schema_id IS DISTINCT FROM NEW.schema_id + JOIN metaschema_public.table AS t + ON t.schema_id = other_api.schema_id + AND metaschema_private.table_name_hash(t.name) = new_name_hash + JOIN services_public.apis AS a + ON a.id = my_api.api_id + WHERE my_api.schema_id = NEW.schema_id + LIMIT 1; + + IF conflicting_api_name IS NOT NULL THEN + RAISE EXCEPTION 'Table name "%" conflicts with existing table "%" in API "%". Table names must be unique (by plural form) across all schemas within the same API.', + NEW.name, conflicting_table_name, conflicting_api_name; + END IF; + + RETURN NEW; +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE; + +CREATE TRIGGER _000003_enforce_api_table_name_uniqueness + BEFORE INSERT + ON metaschema_public."table" + FOR EACH ROW + EXECUTE PROCEDURE services_private.tg_enforce_api_table_name_uniqueness(); + +CREATE TRIGGER _000003_enforce_api_table_name_uniqueness_update + BEFORE UPDATE + ON metaschema_public."table" + FOR EACH ROW + WHEN (new.name IS DISTINCT FROM old.name + OR new.schema_id IS DISTINCT FROM old.schema_id) + EXECUTE PROCEDURE services_private.tg_enforce_api_table_name_uniqueness(); + +CREATE FUNCTION services_private.tg_enforce_api_schema_table_name_uniqueness() RETURNS trigger AS $EOFCODE$ +DECLARE + conflicting_new_table text; + conflicting_existing_table text; +BEGIN + -- Find any table name collision between the newly linked schema + -- and any schema already linked to the same API + SELECT new_t.name, existing_t.name + INTO conflicting_new_table, conflicting_existing_table + FROM metaschema_public.table AS new_t + JOIN services_public.api_schemas AS existing_link + ON existing_link.api_id = NEW.api_id + AND existing_link.schema_id IS DISTINCT FROM NEW.schema_id + JOIN metaschema_public.table AS existing_t + ON existing_t.schema_id = existing_link.schema_id + AND metaschema_private.table_name_hash(existing_t.name) = metaschema_private.table_name_hash(new_t.name) + WHERE new_t.schema_id = NEW.schema_id + LIMIT 1; + + IF conflicting_new_table IS NOT NULL THEN + RAISE EXCEPTION 'Cannot link schema to API: table "%" conflicts with existing table "%" already exposed in this API. Table names must be unique (by plural form) across all schemas within the same API.', + conflicting_new_table, conflicting_existing_table; + END IF; + + RETURN NEW; +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE; + +CREATE TRIGGER _000001_enforce_api_schema_table_name_uniqueness + BEFORE INSERT + ON services_public.api_schemas + FOR EACH ROW + EXECUTE PROCEDURE services_private.tg_enforce_api_schema_table_name_uniqueness(); + +CREATE TABLE services_public.database_settings ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL UNIQUE, + enable_aggregates boolean NOT NULL DEFAULT false, + enable_postgis boolean NOT NULL DEFAULT true, + enable_search boolean NOT NULL DEFAULT true, + enable_direct_uploads boolean NOT NULL DEFAULT true, + enable_presigned_uploads boolean NOT NULL DEFAULT true, + enable_many_to_many boolean NOT NULL DEFAULT true, + enable_connection_filter boolean NOT NULL DEFAULT true, + enable_ltree boolean NOT NULL DEFAULT true, + enable_llm boolean NOT NULL DEFAULT false, + enable_realtime boolean NOT NULL DEFAULT false, + enable_bulk boolean NOT NULL DEFAULT false, + enable_i18n boolean NOT NULL DEFAULT false, + options jsonb NOT NULL DEFAULT '{}'::jsonb, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE +); + +COMMENT ON TABLE services_public.database_settings IS 'Database-wide feature flags and settings; controls which platform features are available to all APIs in this database'; + +COMMENT ON COLUMN services_public.database_settings.id IS 'Unique identifier for this settings record'; + +COMMENT ON COLUMN services_public.database_settings.database_id IS 'Reference to the metaschema database these settings apply to'; + +COMMENT ON COLUMN services_public.database_settings.enable_aggregates IS 'Enable aggregate queries (sum, avg, min, max, etc.) in the GraphQL API'; + +COMMENT ON COLUMN services_public.database_settings.enable_postgis IS 'Enable PostGIS spatial types and operators in the GraphQL API'; + +COMMENT ON COLUMN services_public.database_settings.enable_search IS 'Enable unified search (tsvector, BM25, pg_trgm, pgvector) in the GraphQL API'; + +COMMENT ON COLUMN services_public.database_settings.enable_direct_uploads IS 'Enable direct (multipart) file upload mutations in the GraphQL API'; + +COMMENT ON COLUMN services_public.database_settings.enable_presigned_uploads IS 'Enable presigned URL upload flow for S3/MinIO storage'; + +COMMENT ON COLUMN services_public.database_settings.enable_many_to_many IS 'Enable many-to-many relationship queries in the GraphQL API'; + +COMMENT ON COLUMN services_public.database_settings.enable_connection_filter IS 'Enable connection filter (where argument) in the GraphQL API'; + +COMMENT ON COLUMN services_public.database_settings.enable_ltree IS 'Enable ltree hierarchical data type support in the GraphQL API'; + +COMMENT ON COLUMN services_public.database_settings.enable_llm IS 'Enable LLM/AI integration features in the GraphQL API'; + +COMMENT ON COLUMN services_public.database_settings.enable_realtime IS 'Enable realtime subscriptions (cursor-tracked change delivery) in the GraphQL API'; + +COMMENT ON COLUMN services_public.database_settings.enable_bulk IS 'Enable bulk mutation operations (insert, upsert, update, delete) in the GraphQL API'; + +COMMENT ON COLUMN services_public.database_settings.enable_i18n IS 'Enable internationalization plugin (localeStrings field, translation table discovery) in the GraphQL API'; + +COMMENT ON COLUMN services_public.database_settings.options IS 'Extensible JSON for additional settings that do not have dedicated columns'; + +CREATE INDEX database_settings_database_id_idx ON services_public.database_settings (database_id); + +CREATE TABLE services_public.api_settings ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + api_id uuid NOT NULL UNIQUE, + enable_aggregates boolean, + enable_postgis boolean, + enable_search boolean, + enable_direct_uploads boolean, + enable_presigned_uploads boolean, + enable_many_to_many boolean, + enable_connection_filter boolean, + enable_ltree boolean, + enable_llm boolean, + enable_realtime boolean, + enable_bulk boolean, + enable_i18n boolean, + options jsonb NOT NULL DEFAULT '{}'::jsonb, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT api_fkey + FOREIGN KEY(api_id) + REFERENCES services_public.apis (id) + ON DELETE CASCADE +); + +COMMENT ON TABLE services_public.api_settings IS 'Per-API feature flag overrides; NULL columns inherit from database_settings, explicit true/false overrides the database default'; + +COMMENT ON COLUMN services_public.api_settings.id IS 'Unique identifier for this API settings record'; + +COMMENT ON COLUMN services_public.api_settings.database_id IS 'Reference to the metaschema database'; + +COMMENT ON COLUMN services_public.api_settings.api_id IS 'API these settings override for'; + +COMMENT ON COLUMN services_public.api_settings.enable_aggregates IS 'Override: enable aggregate queries (NULL = inherit from database_settings)'; + +COMMENT ON COLUMN services_public.api_settings.enable_postgis IS 'Override: enable PostGIS spatial types (NULL = inherit from database_settings)'; + +COMMENT ON COLUMN services_public.api_settings.enable_search IS 'Override: enable unified search (NULL = inherit from database_settings)'; + +COMMENT ON COLUMN services_public.api_settings.enable_direct_uploads IS 'Override: enable direct (multipart) file uploads (NULL = inherit from database_settings)'; + +COMMENT ON COLUMN services_public.api_settings.enable_presigned_uploads IS 'Override: enable presigned URL upload flow (NULL = inherit from database_settings)'; + +COMMENT ON COLUMN services_public.api_settings.enable_many_to_many IS 'Override: enable many-to-many relationships (NULL = inherit from database_settings)'; + +COMMENT ON COLUMN services_public.api_settings.enable_connection_filter IS 'Override: enable connection filter (NULL = inherit from database_settings)'; + +COMMENT ON COLUMN services_public.api_settings.enable_ltree IS 'Override: enable ltree hierarchical data type (NULL = inherit from database_settings)'; + +COMMENT ON COLUMN services_public.api_settings.enable_llm IS 'Override: enable LLM/AI integration features (NULL = inherit from database_settings)'; + +COMMENT ON COLUMN services_public.api_settings.enable_realtime IS 'Override: enable realtime subscriptions (NULL = inherit from database_settings)'; + +COMMENT ON COLUMN services_public.api_settings.enable_bulk IS 'Override: enable bulk mutations (NULL = inherit from database_settings)'; + +COMMENT ON COLUMN services_public.api_settings.enable_i18n IS 'Override: enable internationalization plugin (NULL = inherit from database_settings)'; + +COMMENT ON COLUMN services_public.api_settings.options IS 'Extensible JSON for additional per-API settings that do not have dedicated columns'; + +CREATE INDEX api_settings_database_id_idx ON services_public.api_settings (database_id); + +CREATE INDEX api_settings_api_id_idx ON services_public.api_settings (api_id); + +CREATE TABLE services_public.rls_settings ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL UNIQUE, + authenticate_schema_id uuid, + role_schema_id uuid, + authenticate_function_id uuid, + authenticate_strict_function_id uuid, + current_role_function_id uuid, + current_role_id_function_id uuid, + current_user_agent_function_id uuid, + current_ip_address_function_id uuid, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT authenticate_schema_fkey + FOREIGN KEY(authenticate_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE SET NULL, + CONSTRAINT role_schema_fkey + FOREIGN KEY(role_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE SET NULL, + CONSTRAINT authenticate_function_fkey + FOREIGN KEY(authenticate_function_id) + REFERENCES metaschema_public.function (id) + ON DELETE SET NULL, + CONSTRAINT authenticate_strict_function_fkey + FOREIGN KEY(authenticate_strict_function_id) + REFERENCES metaschema_public.function (id) + ON DELETE SET NULL, + CONSTRAINT current_role_function_fkey + FOREIGN KEY(current_role_function_id) + REFERENCES metaschema_public.function (id) + ON DELETE SET NULL, + CONSTRAINT current_role_id_function_fkey + FOREIGN KEY(current_role_id_function_id) + REFERENCES metaschema_public.function (id) + ON DELETE SET NULL, + CONSTRAINT current_user_agent_function_fkey + FOREIGN KEY(current_user_agent_function_id) + REFERENCES metaschema_public.function (id) + ON DELETE SET NULL, + CONSTRAINT current_ip_address_function_fkey + FOREIGN KEY(current_ip_address_function_id) + REFERENCES metaschema_public.function (id) + ON DELETE SET NULL +); + +COMMENT ON TABLE services_public.rls_settings IS 'Per-database RLS module runtime configuration; typed replacement for api_modules rls_module JSONB entries'; + +COMMENT ON COLUMN services_public.rls_settings.id IS 'Unique identifier for this RLS settings record'; + +COMMENT ON COLUMN services_public.rls_settings.database_id IS 'Reference to the metaschema database'; + +COMMENT ON COLUMN services_public.rls_settings.authenticate_schema_id IS 'Schema containing authenticate/authenticate_strict functions (FK to metaschema_public.schema)'; + +COMMENT ON COLUMN services_public.rls_settings.role_schema_id IS 'Schema containing current_role and related functions (FK to metaschema_public.schema)'; + +COMMENT ON COLUMN services_public.rls_settings.authenticate_function_id IS 'Reference to the authenticate function (FK to metaschema_public.function)'; + +COMMENT ON COLUMN services_public.rls_settings.authenticate_strict_function_id IS 'Reference to the strict authenticate function (FK to metaschema_public.function)'; + +COMMENT ON COLUMN services_public.rls_settings.current_role_function_id IS 'Reference to the current_role function (FK to metaschema_public.function)'; + +COMMENT ON COLUMN services_public.rls_settings.current_role_id_function_id IS 'Reference to the current_role_id function (FK to metaschema_public.function)'; + +COMMENT ON COLUMN services_public.rls_settings.current_user_agent_function_id IS 'Reference to the current_user_agent function (FK to metaschema_public.function)'; + +COMMENT ON COLUMN services_public.rls_settings.current_ip_address_function_id IS 'Reference to the current_ip_address function (FK to metaschema_public.function)'; + +CREATE INDEX rls_settings_database_id_idx ON services_public.rls_settings (database_id); + +CREATE TABLE services_public.cors_settings ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + api_id uuid, + allowed_origins text[] NOT NULL DEFAULT '{}', + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT api_fkey + FOREIGN KEY(api_id) + REFERENCES services_public.apis (id) + ON DELETE CASCADE, + CONSTRAINT cors_settings_unique + UNIQUE (database_id, api_id) +); + +COMMENT ON TABLE services_public.cors_settings IS 'Per-database and per-API CORS origin configuration; typed replacement for api_modules cors JSONB entries'; + +COMMENT ON COLUMN services_public.cors_settings.id IS 'Unique identifier for this CORS settings record'; + +COMMENT ON COLUMN services_public.cors_settings.database_id IS 'Reference to the metaschema database'; + +COMMENT ON COLUMN services_public.cors_settings.api_id IS 'Optional API for per-API override; NULL means database-wide default'; + +COMMENT ON COLUMN services_public.cors_settings.allowed_origins IS 'Array of allowed CORS origins (e.g. https://example.com)'; + +CREATE INDEX cors_settings_database_id_idx ON services_public.cors_settings (database_id); + +CREATE INDEX cors_settings_api_id_idx ON services_public.cors_settings (api_id); + +CREATE TABLE services_public.pubkey_settings ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL UNIQUE, + schema_id uuid, + crypto_network text NOT NULL DEFAULT 'cosmos', + user_field text NOT NULL DEFAULT 'user_id', + sign_up_with_key_function_id uuid, + sign_in_request_challenge_function_id uuid, + sign_in_record_failure_function_id uuid, + sign_in_with_challenge_function_id uuid, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE SET NULL, + CONSTRAINT sign_up_with_key_function_fkey + FOREIGN KEY(sign_up_with_key_function_id) + REFERENCES metaschema_public.function (id) + ON DELETE SET NULL, + CONSTRAINT sign_in_request_challenge_function_fkey + FOREIGN KEY(sign_in_request_challenge_function_id) + REFERENCES metaschema_public.function (id) + ON DELETE SET NULL, + CONSTRAINT sign_in_record_failure_function_fkey + FOREIGN KEY(sign_in_record_failure_function_id) + REFERENCES metaschema_public.function (id) + ON DELETE SET NULL, + CONSTRAINT sign_in_with_challenge_function_fkey + FOREIGN KEY(sign_in_with_challenge_function_id) + REFERENCES metaschema_public.function (id) + ON DELETE SET NULL +); + +COMMENT ON TABLE services_public.pubkey_settings IS 'Per-database public-key crypto auth runtime configuration; typed replacement for api_modules pubkey_challenge JSONB entries'; + +COMMENT ON COLUMN services_public.pubkey_settings.id IS 'Unique identifier for this pubkey settings record'; + +COMMENT ON COLUMN services_public.pubkey_settings.database_id IS 'Reference to the metaschema database'; + +COMMENT ON COLUMN services_public.pubkey_settings.schema_id IS 'Schema containing the crypto auth functions (FK to metaschema_public.schema)'; + +COMMENT ON COLUMN services_public.pubkey_settings.crypto_network IS 'Crypto network for key derivation (e.g. cosmos, ethereum)'; + +COMMENT ON COLUMN services_public.pubkey_settings.user_field IS 'Field name used to identify the user in crypto auth functions'; + +COMMENT ON COLUMN services_public.pubkey_settings.sign_up_with_key_function_id IS 'Reference to the sign-up-with-key function (FK to metaschema_public.function)'; + +COMMENT ON COLUMN services_public.pubkey_settings.sign_in_request_challenge_function_id IS 'Reference to the sign-in challenge request function (FK to metaschema_public.function)'; + +COMMENT ON COLUMN services_public.pubkey_settings.sign_in_record_failure_function_id IS 'Reference to the sign-in failure recording function (FK to metaschema_public.function)'; + +COMMENT ON COLUMN services_public.pubkey_settings.sign_in_with_challenge_function_id IS 'Reference to the sign-in-with-challenge function (FK to metaschema_public.function)'; + +CREATE INDEX pubkey_settings_database_id_idx ON services_public.pubkey_settings (database_id); + +CREATE TABLE services_public.webauthn_settings ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL UNIQUE, + schema_id uuid, + credentials_schema_id uuid, + sessions_schema_id uuid, + session_secrets_schema_id uuid, + credentials_table_id uuid, + sessions_table_id uuid, + session_credentials_table_id uuid, + session_secrets_table_id uuid, + user_field_id uuid, + rp_id text NOT NULL DEFAULT '', + rp_name text NOT NULL DEFAULT '', + origin_allowlist text[] NOT NULL DEFAULT '{}', + attestation_type text NOT NULL DEFAULT 'none' CHECK (attestation_type IN ('none', 'indirect', 'direct', 'enterprise')), + require_user_verification boolean NOT NULL DEFAULT false, + resident_key text NOT NULL DEFAULT 'required' CHECK (resident_key IN ('discouraged', 'preferred', 'required')), + challenge_expiry_seconds bigint NOT NULL DEFAULT 300, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE SET NULL, + CONSTRAINT credentials_schema_fkey + FOREIGN KEY(credentials_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE SET NULL, + CONSTRAINT sessions_schema_fkey + FOREIGN KEY(sessions_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE SET NULL, + CONSTRAINT session_secrets_schema_fkey + FOREIGN KEY(session_secrets_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE SET NULL, + CONSTRAINT credentials_table_fkey + FOREIGN KEY(credentials_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE SET NULL, + CONSTRAINT sessions_table_fkey + FOREIGN KEY(sessions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE SET NULL, + CONSTRAINT session_credentials_table_fkey + FOREIGN KEY(session_credentials_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE SET NULL, + CONSTRAINT session_secrets_table_fkey + FOREIGN KEY(session_secrets_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE SET NULL, + CONSTRAINT user_field_fkey + FOREIGN KEY(user_field_id) + REFERENCES metaschema_public.field (id) + ON DELETE SET NULL +); + +COMMENT ON TABLE services_public.webauthn_settings IS 'Per-database WebAuthn/passkey runtime configuration; typed replacement for api_modules webauthn_challenge JSONB entries'; + +COMMENT ON COLUMN services_public.webauthn_settings.id IS 'Unique identifier for this WebAuthn settings record'; + +COMMENT ON COLUMN services_public.webauthn_settings.database_id IS 'Reference to the metaschema database'; + +COMMENT ON COLUMN services_public.webauthn_settings.schema_id IS 'Schema containing WebAuthn auth procedures (FK to metaschema_public.schema)'; + +COMMENT ON COLUMN services_public.webauthn_settings.credentials_schema_id IS 'Schema of the webauthn_credentials table (FK to metaschema_public.schema)'; + +COMMENT ON COLUMN services_public.webauthn_settings.sessions_schema_id IS 'Schema of the sessions table (FK to metaschema_public.schema)'; + +COMMENT ON COLUMN services_public.webauthn_settings.session_secrets_schema_id IS 'Schema of the session_secrets table (FK to metaschema_public.schema)'; + +COMMENT ON COLUMN services_public.webauthn_settings.credentials_table_id IS 'Reference to the webauthn_credentials table (FK to metaschema_public.table)'; + +COMMENT ON COLUMN services_public.webauthn_settings.sessions_table_id IS 'Reference to the sessions table (FK to metaschema_public.table)'; + +COMMENT ON COLUMN services_public.webauthn_settings.session_credentials_table_id IS 'Reference to the session_credentials table (FK to metaschema_public.table)'; + +COMMENT ON COLUMN services_public.webauthn_settings.session_secrets_table_id IS 'Reference to the session_secrets table (FK to metaschema_public.table)'; + +COMMENT ON COLUMN services_public.webauthn_settings.user_field_id IS 'Reference to the user field on webauthn_credentials (FK to metaschema_public.field)'; + +COMMENT ON COLUMN services_public.webauthn_settings.rp_id IS 'WebAuthn Relying Party ID (typically the domain name)'; + +COMMENT ON COLUMN services_public.webauthn_settings.rp_name IS 'WebAuthn Relying Party display name'; + +COMMENT ON COLUMN services_public.webauthn_settings.origin_allowlist IS 'Allowed origins for WebAuthn registration and authentication'; + +COMMENT ON COLUMN services_public.webauthn_settings.attestation_type IS 'Attestation conveyance preference (none, indirect, direct, enterprise)'; + +COMMENT ON COLUMN services_public.webauthn_settings.require_user_verification IS 'Whether to require user verification (biometric/PIN) during auth'; + +COMMENT ON COLUMN services_public.webauthn_settings.resident_key IS 'Resident key requirement (discouraged, preferred, required)'; + +COMMENT ON COLUMN services_public.webauthn_settings.challenge_expiry_seconds IS 'Challenge TTL in seconds (default 300 = 5 minutes)'; + +CREATE INDEX webauthn_settings_database_id_idx ON services_public.webauthn_settings (database_id); \ No newline at end of file diff --git a/extensions/@pgpm/services/verify/schemas/services_private/triggers/enforce_api_schema_table_name_uniqueness.sql b/extensions/@pgpm/services/verify/schemas/services_private/triggers/enforce_api_schema_table_name_uniqueness.sql new file mode 100644 index 00000000..20fd8aee --- /dev/null +++ b/extensions/@pgpm/services/verify/schemas/services_private/triggers/enforce_api_schema_table_name_uniqueness.sql @@ -0,0 +1,10 @@ +-- Verify schemas/services_private/triggers/enforce_api_schema_table_name_uniqueness + +BEGIN; + +SELECT has_function_privilege( + 'services_private.tg_enforce_api_schema_table_name_uniqueness()', + 'execute' +); + +ROLLBACK; diff --git a/extensions/@pgpm/services/verify/schemas/services_private/triggers/enforce_api_table_name_uniqueness.sql b/extensions/@pgpm/services/verify/schemas/services_private/triggers/enforce_api_table_name_uniqueness.sql new file mode 100644 index 00000000..c9b35545 --- /dev/null +++ b/extensions/@pgpm/services/verify/schemas/services_private/triggers/enforce_api_table_name_uniqueness.sql @@ -0,0 +1,10 @@ +-- Verify schemas/services_private/triggers/enforce_api_table_name_uniqueness + +BEGIN; + +SELECT has_function_privilege( + 'services_private.tg_enforce_api_table_name_uniqueness()', + 'execute' +); + +ROLLBACK; diff --git a/extensions/@pgpm/services/verify/schemas/services_public/tables/api_settings/table.sql b/extensions/@pgpm/services/verify/schemas/services_public/tables/api_settings/table.sql new file mode 100644 index 00000000..1c026102 --- /dev/null +++ b/extensions/@pgpm/services/verify/schemas/services_public/tables/api_settings/table.sql @@ -0,0 +1,25 @@ +-- Verify schemas/services_public/tables/api_settings/table + +BEGIN; + +SELECT + id, + database_id, + api_id, + enable_aggregates, + enable_postgis, + enable_search, + enable_direct_uploads, + enable_presigned_uploads, + enable_many_to_many, + enable_connection_filter, + enable_ltree, + enable_llm, + enable_realtime, + enable_bulk, + enable_i18n, + options +FROM services_public.api_settings +WHERE false; + +ROLLBACK; diff --git a/extensions/@pgpm/services/verify/schemas/services_public/tables/cors_settings/table.sql b/extensions/@pgpm/services/verify/schemas/services_public/tables/cors_settings/table.sql new file mode 100644 index 00000000..8f86a332 --- /dev/null +++ b/extensions/@pgpm/services/verify/schemas/services_public/tables/cors_settings/table.sql @@ -0,0 +1,13 @@ +-- Verify schemas/services_public/tables/cors_settings/table + +BEGIN; + +SELECT + id, + database_id, + api_id, + allowed_origins +FROM services_public.cors_settings +WHERE false; + +ROLLBACK; diff --git a/extensions/@pgpm/services/verify/schemas/services_public/tables/database_settings/table.sql b/extensions/@pgpm/services/verify/schemas/services_public/tables/database_settings/table.sql new file mode 100644 index 00000000..50ff73c2 --- /dev/null +++ b/extensions/@pgpm/services/verify/schemas/services_public/tables/database_settings/table.sql @@ -0,0 +1,24 @@ +-- Verify schemas/services_public/tables/database_settings/table + +BEGIN; + +SELECT + id, + database_id, + enable_aggregates, + enable_postgis, + enable_search, + enable_direct_uploads, + enable_presigned_uploads, + enable_many_to_many, + enable_connection_filter, + enable_ltree, + enable_llm, + enable_realtime, + enable_bulk, + enable_i18n, + options +FROM services_public.database_settings +WHERE false; + +ROLLBACK; diff --git a/extensions/@pgpm/services/verify/schemas/services_public/tables/pubkey_settings/table.sql b/extensions/@pgpm/services/verify/schemas/services_public/tables/pubkey_settings/table.sql new file mode 100644 index 00000000..ce244d38 --- /dev/null +++ b/extensions/@pgpm/services/verify/schemas/services_public/tables/pubkey_settings/table.sql @@ -0,0 +1,18 @@ +-- Verify schemas/services_public/tables/pubkey_settings/table + +BEGIN; + +SELECT + id, + database_id, + schema_id, + crypto_network, + user_field, + sign_up_with_key_function_id, + sign_in_request_challenge_function_id, + sign_in_record_failure_function_id, + sign_in_with_challenge_function_id +FROM services_public.pubkey_settings +WHERE false; + +ROLLBACK; diff --git a/extensions/@pgpm/services/verify/schemas/services_public/tables/rls_settings/table.sql b/extensions/@pgpm/services/verify/schemas/services_public/tables/rls_settings/table.sql new file mode 100644 index 00000000..2bd25d79 --- /dev/null +++ b/extensions/@pgpm/services/verify/schemas/services_public/tables/rls_settings/table.sql @@ -0,0 +1,19 @@ +-- Verify schemas/services_public/tables/rls_settings/table + +BEGIN; + +SELECT + id, + database_id, + authenticate_schema_id, + role_schema_id, + authenticate_function_id, + authenticate_strict_function_id, + current_role_function_id, + current_role_id_function_id, + current_user_agent_function_id, + current_ip_address_function_id +FROM services_public.rls_settings +WHERE false; + +ROLLBACK; diff --git a/extensions/@pgpm/services/verify/schemas/services_public/tables/webauthn_settings/table.sql b/extensions/@pgpm/services/verify/schemas/services_public/tables/webauthn_settings/table.sql new file mode 100644 index 00000000..21580e86 --- /dev/null +++ b/extensions/@pgpm/services/verify/schemas/services_public/tables/webauthn_settings/table.sql @@ -0,0 +1,27 @@ +-- Verify schemas/services_public/tables/webauthn_settings/table + +BEGIN; + +SELECT + id, + database_id, + schema_id, + credentials_schema_id, + sessions_schema_id, + session_secrets_schema_id, + credentials_table_id, + sessions_table_id, + session_credentials_table_id, + session_secrets_table_id, + user_field_id, + rp_id, + rp_name, + origin_allowlist, + attestation_type, + require_user_verification, + resident_key, + challenge_expiry_seconds +FROM services_public.webauthn_settings +WHERE false; + +ROLLBACK; diff --git a/extensions/@pgpm/stamps/LICENSE b/extensions/@pgpm/stamps/LICENSE new file mode 100644 index 00000000..7b18c918 --- /dev/null +++ b/extensions/@pgpm/stamps/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2025 Dan Lynch +Copyright (c) 2025 Constructive + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/@pgpm/stamps/Makefile b/extensions/@pgpm/stamps/Makefile new file mode 100644 index 00000000..bea069d4 --- /dev/null +++ b/extensions/@pgpm/stamps/Makefile @@ -0,0 +1,6 @@ +EXTENSION = pgpm-stamps +DATA = sql/pgpm-stamps--0.15.5.sql + +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) diff --git a/extensions/@pgpm/stamps/README.md b/extensions/@pgpm/stamps/README.md new file mode 100644 index 00000000..1b8b626c --- /dev/null +++ b/extensions/@pgpm/stamps/README.md @@ -0,0 +1,218 @@ +# @pgpm/stamps + +

+ +

+ +

+ + + + + +

+ +Timestamp utilities and audit trail functions for PostgreSQL. + +## Overview + +`@pgpm/stamps` provides PostgreSQL trigger functions for automatically managing timestamp and user tracking columns in your tables. This package simplifies audit trail implementation by automatically setting `created_at`, `updated_at`, `created_by`, and `updated_by` fields. + +## Features + +- **timestamps()**: Trigger function that automatically manages `created_at` and `updated_at` columns +- **peoplestamps()**: Trigger function that automatically manages `created_by` and `updated_by` columns using JWT claims +- Automatic preservation of creation timestamps and users on updates +- Integration with `@pgpm/jwt-claims` for user context + +## Installation + +If you have `pgpm` installed: + +```bash +pgpm install @pgpm/stamps +pgpm deploy +``` + +This is a quick way to get started. The sections below provide more detailed installation options. + +### Prerequisites + +```bash +# Install pgpm CLI +npm install -g pgpm + +# Start local Postgres (via Docker) and export env vars +pgpm docker start +eval "$(pgpm env)" +``` + +> **Tip:** Already running Postgres? Skip the Docker step and just export your `PG*` environment variables. + +### **Add to an Existing Package** + +```bash +# 1. Install the package +pgpm install @pgpm/stamps + +# 2. Deploy locally +pgpm deploy +``` + +### **Add to a New Project** + +```bash +# 1. Create a workspace +pgpm init workspace + +# 2. Create your first module +cd my-workspace +pgpm init + +# 3. Install a package +cd packages/my-module +pgpm install @pgpm/stamps + +# 4. Deploy everything +pgpm deploy --createdb --database mydb1 +``` + +## Usage + +### Setting Up Timestamp Tracking + +```sql +-- Create a table with timestamp columns +CREATE TABLE public.posts ( + id serial PRIMARY KEY, + title text, + content text, + created_at timestamptz, + updated_at timestamptz +); + +-- Add trigger to automatically manage timestamps +CREATE TRIGGER set_timestamps +BEFORE INSERT OR UPDATE ON public.posts +FOR EACH ROW +EXECUTE FUNCTION stamps.timestamps(); +``` + +### Setting Up User Tracking + +```sql +-- Create a table with user tracking columns +CREATE TABLE public.posts ( + id serial PRIMARY KEY, + title text, + content text, + created_at timestamptz, + updated_at timestamptz, + created_by uuid, + updated_by uuid +); + +-- Add triggers for automatic timestamp and user tracking +CREATE TRIGGER set_timestamps +BEFORE INSERT OR UPDATE ON public.posts +FOR EACH ROW +EXECUTE FUNCTION stamps.timestamps(); + +CREATE TRIGGER set_peoplestamps +BEFORE INSERT OR UPDATE ON public.posts +FOR EACH ROW +EXECUTE FUNCTION stamps.peoplestamps(); +``` + +### How It Works + +When you insert a new row: +- `created_at` and `updated_at` are set to the current timestamp +- `created_by` and `updated_by` are set to the current user ID from JWT claims + +When you update an existing row: +- `created_at` and `created_by` are preserved (set to their original values) +- `updated_at` is set to the current timestamp +- `updated_by` is set to the current user ID from JWT claims + +### Example Usage + +```sql +-- Set the user context (typically done by your application) +SET jwt.claims.user_id = '00000000-0000-0000-0000-000000000001'; + +-- Insert a new post +INSERT INTO public.posts (title, content) +VALUES ('My First Post', 'Hello World!'); +-- created_at, updated_at, created_by, and updated_by are automatically set + +-- Update the post +UPDATE public.posts +SET content = 'Updated content' +WHERE id = 1; +-- updated_at and updated_by are automatically updated +-- created_at and created_by remain unchanged +``` + +## Trigger Functions + +### stamps.timestamps() + +Automatically manages timestamp columns on INSERT and UPDATE operations. + +**Behavior:** +- **INSERT**: Sets both `created_at` and `updated_at` to `NOW()` +- **UPDATE**: Preserves `created_at`, updates `updated_at` to `NOW()` + +**Required Columns:** +- `created_at timestamptz` +- `updated_at timestamptz` + +### stamps.peoplestamps() + +Automatically manages user tracking columns on INSERT and UPDATE operations using JWT claims. + +**Behavior:** +- **INSERT**: Sets both `created_by` and `updated_by` to `jwt_public.current_user_id()` +- **UPDATE**: Preserves `created_by`, updates `updated_by` to `jwt_public.current_user_id()` + +**Required Columns:** +- `created_by uuid` +- `updated_by uuid` + +**Dependencies:** +- Requires `@pgpm/jwt-claims` for `jwt_public.current_user_id()` function +- User context must be set via `jwt.claims.user_id` session variable + +## Dependencies + +- `@pgpm/jwt-claims`: JWT claim handling for user context +- `@pgpm/verify`: Verification utilities for database objects + +## Testing + +```bash +pnpm test +``` + +The test suite validates: +- Automatic timestamp setting on insert and update +- Automatic user tracking on insert and update +- Preservation of creation timestamps and users on updates +- Integration with JWT claims for user context + +## Related Tooling + +* [pgpm](https://github.com/constructive-io/constructive/tree/main/packages/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages. +* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/packages/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation. +* [supabase-test](https://github.com/constructive-io/constructive/tree/main/packages/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready. +* [graphile-test](https://github.com/constructive-io/constructive/tree/main/packages/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts. +* [pgsql-parser](https://github.com/constructive-io/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax. +* [libpg-query-node](https://github.com/constructive-io/libpg-query-node): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees. +* [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. + +## Disclaimer + +AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND. + +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. diff --git a/extensions/@pgpm/stamps/deploy/schemas/stamps/procedures/utils.sql b/extensions/@pgpm/stamps/deploy/schemas/stamps/procedures/utils.sql new file mode 100644 index 00000000..14158262 --- /dev/null +++ b/extensions/@pgpm/stamps/deploy/schemas/stamps/procedures/utils.sql @@ -0,0 +1,35 @@ +-- Deploy schemas/stamps/procedures/utils to pg + +-- requires: schemas/stamps/schema + +BEGIN; + +CREATE FUNCTION stamps.peoplestamps() +RETURNS TRIGGER AS $$ +BEGIN + IF TG_OP = 'INSERT' THEN + NEW.created_by = jwt_public.current_user_id(); + NEW.updated_by = jwt_public.current_user_id(); + ELSIF TG_OP = 'UPDATE' THEN + NEW.created_by = OLD.created_by; + NEW.updated_by = jwt_public.current_user_id(); + END IF; + RETURN NEW; +END; +$$ LANGUAGE 'plpgsql'; + +CREATE FUNCTION stamps.timestamps() +RETURNS TRIGGER AS $$ +BEGIN + IF TG_OP = 'INSERT' THEN + NEW.created_at = NOW(); + NEW.updated_at = NOW(); + ELSIF TG_OP = 'UPDATE' THEN + NEW.created_at = OLD.created_at; + NEW.updated_at = NOW(); + END IF; + RETURN NEW; +END; +$$ LANGUAGE 'plpgsql'; + +COMMIT; diff --git a/extensions/@pgpm/stamps/deploy/schemas/stamps/schema.sql b/extensions/@pgpm/stamps/deploy/schemas/stamps/schema.sql new file mode 100644 index 00000000..ce403b11 --- /dev/null +++ b/extensions/@pgpm/stamps/deploy/schemas/stamps/schema.sql @@ -0,0 +1,15 @@ +-- Deploy schemas/stamps/schema to pg + + +BEGIN; + +CREATE SCHEMA stamps; + +GRANT USAGE ON SCHEMA stamps +TO authenticated, anonymous; + +ALTER DEFAULT PRIVILEGES IN SCHEMA stamps +GRANT EXECUTE ON FUNCTIONS +TO authenticated; + +COMMIT; diff --git a/extensions/@pgpm/stamps/package.json b/extensions/@pgpm/stamps/package.json new file mode 100644 index 00000000..f71f3b94 --- /dev/null +++ b/extensions/@pgpm/stamps/package.json @@ -0,0 +1,39 @@ +{ + "name": "@pgpm/stamps", + "version": "0.26.0", + "description": "Timestamp utilities and audit trail functions for PostgreSQL", + "author": "Dan Lynch ", + "contributors": [ + "Constructive " + ], + "keywords": [ + "postgresql", + "pgpm", + "timestamp", + "audit" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "bundle": "pgpm package", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@pgpm/jwt-claims": "0.26.0", + "@pgpm/verify": "0.26.0" + }, + "devDependencies": { + "pgpm": "^4.23.2" + }, + "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" + }, + "gitHead": "3badf1e5e2fc71deae9e194b779599d17ab28a0d" +} diff --git a/extensions/@pgpm/stamps/pgpm-stamps.control b/extensions/@pgpm/stamps/pgpm-stamps.control new file mode 100644 index 00000000..19449b9c --- /dev/null +++ b/extensions/@pgpm/stamps/pgpm-stamps.control @@ -0,0 +1,8 @@ +# pgpm-stamps extension +comment = 'pgpm-stamps extension' +default_version = '0.15.5' +module_pathname = '$libdir/pgpm-stamps' +requires = 'plpgsql,pgpm-jwt-claims,pgpm-verify' +relocatable = false +superuser = false + \ No newline at end of file diff --git a/extensions/@pgpm/stamps/pgpm.plan b/extensions/@pgpm/stamps/pgpm.plan new file mode 100644 index 00000000..486b32f9 --- /dev/null +++ b/extensions/@pgpm/stamps/pgpm.plan @@ -0,0 +1,6 @@ +%syntax-version=1.0.0 +%project=pgpm-stamps +%uri=pgpm-stamps + +schemas/stamps/schema 2020-12-18T04:55:26Z Dan Lynch # add schemas/stamps/schema +schemas/stamps/procedures/utils [schemas/stamps/schema] 2020-12-18T04:57:09Z Dan Lynch # add schemas/stamps/procedures/utils diff --git a/extensions/@pgpm/stamps/revert/schemas/stamps/procedures/utils.sql b/extensions/@pgpm/stamps/revert/schemas/stamps/procedures/utils.sql new file mode 100644 index 00000000..ddbf19cf --- /dev/null +++ b/extensions/@pgpm/stamps/revert/schemas/stamps/procedures/utils.sql @@ -0,0 +1,8 @@ +-- Revert schemas/stamps/procedures/utils from pg + +BEGIN; + +DROP FUNCTION stamps.peoplestamps(); +DROP FUNCTION stamps.timestamps(); + +COMMIT; diff --git a/extensions/@pgpm/stamps/revert/schemas/stamps/schema.sql b/extensions/@pgpm/stamps/revert/schemas/stamps/schema.sql new file mode 100644 index 00000000..fbcf1f6d --- /dev/null +++ b/extensions/@pgpm/stamps/revert/schemas/stamps/schema.sql @@ -0,0 +1,7 @@ +-- Revert schemas/stamps/schema from pg + +BEGIN; + +DROP SCHEMA stamps; + +COMMIT; diff --git a/extensions/@pgpm/stamps/sql/pgpm-stamps--0.15.3.sql b/extensions/@pgpm/stamps/sql/pgpm-stamps--0.15.3.sql new file mode 100644 index 00000000..49fe2299 --- /dev/null +++ b/extensions/@pgpm/stamps/sql/pgpm-stamps--0.15.3.sql @@ -0,0 +1,33 @@ +\echo Use "CREATE EXTENSION pgpm-stamps" to load this file. \quit +CREATE SCHEMA stamps; + +GRANT USAGE ON SCHEMA stamps TO authenticated, anonymous; + +ALTER DEFAULT PRIVILEGES IN SCHEMA stamps + GRANT EXECUTE ON FUNCTIONS TO authenticated; + +CREATE FUNCTION stamps.peoplestamps() RETURNS trigger AS $EOFCODE$ +BEGIN + IF TG_OP = 'INSERT' THEN + NEW.created_by = jwt_public.current_user_id(); + NEW.updated_by = jwt_public.current_user_id(); + ELSIF TG_OP = 'UPDATE' THEN + NEW.created_by = OLD.created_by; + NEW.updated_by = jwt_public.current_user_id(); + END IF; + RETURN NEW; +END; +$EOFCODE$ LANGUAGE plpgsql; + +CREATE FUNCTION stamps.timestamps() RETURNS trigger AS $EOFCODE$ +BEGIN + IF TG_OP = 'INSERT' THEN + NEW.created_at = NOW(); + NEW.updated_at = NOW(); + ELSIF TG_OP = 'UPDATE' THEN + NEW.created_at = OLD.created_at; + NEW.updated_at = NOW(); + END IF; + RETURN NEW; +END; +$EOFCODE$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/extensions/@pgpm/stamps/sql/pgpm-stamps--0.15.5.sql b/extensions/@pgpm/stamps/sql/pgpm-stamps--0.15.5.sql new file mode 100644 index 00000000..49fe2299 --- /dev/null +++ b/extensions/@pgpm/stamps/sql/pgpm-stamps--0.15.5.sql @@ -0,0 +1,33 @@ +\echo Use "CREATE EXTENSION pgpm-stamps" to load this file. \quit +CREATE SCHEMA stamps; + +GRANT USAGE ON SCHEMA stamps TO authenticated, anonymous; + +ALTER DEFAULT PRIVILEGES IN SCHEMA stamps + GRANT EXECUTE ON FUNCTIONS TO authenticated; + +CREATE FUNCTION stamps.peoplestamps() RETURNS trigger AS $EOFCODE$ +BEGIN + IF TG_OP = 'INSERT' THEN + NEW.created_by = jwt_public.current_user_id(); + NEW.updated_by = jwt_public.current_user_id(); + ELSIF TG_OP = 'UPDATE' THEN + NEW.created_by = OLD.created_by; + NEW.updated_by = jwt_public.current_user_id(); + END IF; + RETURN NEW; +END; +$EOFCODE$ LANGUAGE plpgsql; + +CREATE FUNCTION stamps.timestamps() RETURNS trigger AS $EOFCODE$ +BEGIN + IF TG_OP = 'INSERT' THEN + NEW.created_at = NOW(); + NEW.updated_at = NOW(); + ELSIF TG_OP = 'UPDATE' THEN + NEW.created_at = OLD.created_at; + NEW.updated_at = NOW(); + END IF; + RETURN NEW; +END; +$EOFCODE$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/extensions/@pgpm/stamps/verify/schemas/stamps/procedures/utils.sql b/extensions/@pgpm/stamps/verify/schemas/stamps/procedures/utils.sql new file mode 100644 index 00000000..c418090f --- /dev/null +++ b/extensions/@pgpm/stamps/verify/schemas/stamps/procedures/utils.sql @@ -0,0 +1,8 @@ +-- Verify schemas/stamps/procedures/utils on pg + +BEGIN; + +SELECT verify_function ('stamps.peoplestamps'); +SELECT verify_function ('stamps.timestamps'); + +ROLLBACK; diff --git a/extensions/@pgpm/stamps/verify/schemas/stamps/schema.sql b/extensions/@pgpm/stamps/verify/schemas/stamps/schema.sql new file mode 100644 index 00000000..c5e61273 --- /dev/null +++ b/extensions/@pgpm/stamps/verify/schemas/stamps/schema.sql @@ -0,0 +1,7 @@ +-- Verify schemas/stamps/schema on pg + +BEGIN; + +SELECT verify_schema ('stamps'); + +ROLLBACK; diff --git a/extensions/@pgpm/types/LICENSE b/extensions/@pgpm/types/LICENSE new file mode 100644 index 00000000..7b18c918 --- /dev/null +++ b/extensions/@pgpm/types/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2025 Dan Lynch +Copyright (c) 2025 Constructive + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/@pgpm/types/Makefile b/extensions/@pgpm/types/Makefile new file mode 100644 index 00000000..d5953566 --- /dev/null +++ b/extensions/@pgpm/types/Makefile @@ -0,0 +1,6 @@ +EXTENSION = pgpm-types +DATA = sql/pgpm-types--0.15.5.sql + +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) diff --git a/extensions/@pgpm/types/README.md b/extensions/@pgpm/types/README.md new file mode 100644 index 00000000..b48dbed1 --- /dev/null +++ b/extensions/@pgpm/types/README.md @@ -0,0 +1,232 @@ +# @pgpm/types + +

+ +

+ +

+ + + + + +

+ +Core PostgreSQL data types with SQL scripts. + +## Overview + +`@pgpm/types` provides a collection of validated PostgreSQL domain types for common data formats. These domains enforce data integrity at the database level through regex-based validation, ensuring that only properly formatted data is stored. + +## Features + +- **email**: Case-insensitive email address validation +- **url**: HTTP/HTTPS URL validation +- **origin**: Origin URL validation (scheme + host) +- **hostname**: Domain name validation +- **image**: JSON-based image metadata with URL and MIME type +- **attachment**: File attachment metadata as a URL string or JSON with URL and MIME type +- **upload**: File upload metadata +- **single_select**: Single selection field +- **multiple_select**: Multiple selection field + +## Installation + +If you have `pgpm` installed: + +```bash +pgpm install @pgpm/types +pgpm deploy +``` + +This is a quick way to get started. The sections below provide more detailed installation options. + +### Prerequisites + +```bash +# Install pgpm CLI +npm install -g pgpm + +# Start local Postgres (via Docker) and export env vars +pgpm docker start +eval "$(pgpm env)" +``` + +> **Tip:** Already running Postgres? Skip the Docker step and just export your `PG*` environment variables. + +### **Add to an Existing Package** + +```bash +# 1. Install the package +pgpm install @pgpm/types + +# 2. Deploy locally +pgpm deploy +``` + +### **Add to a New Project** + +```bash +# 1. Create a workspace +pgpm init workspace + +# 2. Create your first module +cd my-workspace +pgpm init + +# 3. Install a package +cd packages/my-module +pgpm install @pgpm/types + +# 4. Deploy everything +pgpm deploy --createdb --database mydb1 +``` + +## Usage + +### Creating Tables with Validated Types + +```sql +CREATE TABLE customers ( + id serial PRIMARY KEY, + email email, + website url, + domain hostname, + profile_image image, + document attachment +); +``` + +### Email Domain + +The `email` domain validates email addresses using a comprehensive regex pattern and stores them as case-insensitive text (`citext`). + +```sql +-- Valid emails +INSERT INTO customers (email) VALUES + ('user@example.com'), + ('john.doe@company.co.uk'), + ('support+tag@service.io'); + +-- Invalid email (will fail) +INSERT INTO customers (email) VALUES ('not-an-email'); +-- ERROR: value for domain email violates check constraint +``` + +**Validation Pattern**: RFC-compliant email format with support for special characters and subdomains. + +### URL Domain + +The `url` domain validates HTTP and HTTPS URLs. + +```sql +-- Valid URLs +INSERT INTO customers (website) VALUES + ('http://example.com'), + ('https://www.example.com/path?query=value'), + ('http://foo.bar/path_(with)_parens'); + +-- Invalid URLs (will fail) +INSERT INTO customers (website) VALUES + ('ftp://example.com'), -- Only http/https allowed + ('example.com'), -- Missing protocol + ('http://'); -- Incomplete URL +``` + +**Validation Pattern**: Requires `http://` or `https://` protocol and valid URL structure. + +### Hostname Domain + +The `hostname` domain validates domain names without protocol or path. + +```sql +-- Valid hostnames +INSERT INTO customers (domain) VALUES + ('example.com'), + ('subdomain.example.com'), + ('my-site.co.uk'); + +-- Invalid hostnames (will fail) +INSERT INTO customers (domain) VALUES + ('http://example.com'), -- No protocol allowed + ('example.com/path'), -- No path allowed + ('invalid..domain.com'); -- Invalid format +``` + +**Validation Pattern**: Standard domain name format with support for subdomains and hyphens. + +### Image and Attachment Domains + +The `image` domain stores JSON objects with URL and MIME type information. The `attachment` domain accepts either that JSON shape or a plain URL string. +The `upload` domain uses the same JSON object shape as `image`, ensuring both the file URL and MIME type are present. + +```sql +-- Valid image +INSERT INTO customers (profile_image) VALUES + ('{"url": "https://cdn.example.com/photo.jpg", "mime": "image/jpeg"}'::json); + +-- Valid attachment +INSERT INTO customers (document) VALUES + ('{"url": "https://storage.example.com/file.pdf", "mime": "application/pdf"}'::json); + +-- Valid attachment as plain URL +INSERT INTO customers (document) VALUES ('https://storage.example.com/favicon.ico'); +``` + +**Structure**: Image values and JSON-form attachments expect `url` and `mime` properties; attachments also allow a bare URL string. + +## Domain Types Reference + +| Domain | Base Type | Description | Example | +|--------|-----------|-------------|---------| +| `email` | `citext` | Case-insensitive email address | `user@example.com` | +| `url` | `text` | HTTP/HTTPS URL | `https://example.com/path` | +| `origin` | `text` | Origin (scheme + host) | `https://example.com` | +| `hostname` | `text` | Domain name without protocol | `example.com` | +| `image` | `json` | Image metadata with URL and MIME | `{"url": "...", "mime": "image/jpeg"}` | +| `attachment` | `json` | File attachment URL or metadata | `{"url": "...", "mime": "application/pdf"}` or `https://example.com/favicon.ico` | +| `upload` | `json` | File upload metadata (URL + MIME) | `{"url": "...", "mime": "application/pdf"}` | +| `single_select` | `text` | Single selection value | Text value | +| `multiple_select` | `text[]` | Multiple selection values | Array of text values | + +## Validation Benefits + +Using domain types provides several advantages over plain text columns: + +1. **Data Integrity**: Invalid data is rejected at insert/update time +2. **Self-Documenting**: Column types clearly indicate expected format +3. **Consistent Validation**: Same rules applied across all tables +4. **Database-Level Enforcement**: No reliance on application-level validation alone + +## Dependencies + +- `@pgpm/verify`: Verification utilities for database objects +- PostgreSQL `citext` extension (for email domain) + +## Testing + +```bash +pnpm test +``` + +The test suite validates: +- Email format validation (valid and invalid cases) +- URL format validation with extensive test cases +- Hostname format validation +- Image, upload, and attachment JSON structure validation + +## Related Tooling + +* [pgpm](https://github.com/constructive-io/constructive/tree/main/packages/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages. +* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/packages/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation. +* [supabase-test](https://github.com/constructive-io/constructive/tree/main/packages/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready. +* [graphile-test](https://github.com/constructive-io/constructive/tree/main/packages/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts. +* [pgsql-parser](https://github.com/constructive-io/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax. +* [libpg-query-node](https://github.com/constructive-io/libpg-query-node): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees. +* [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. + +## Disclaimer + +AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND. + +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. diff --git a/extensions/@pgpm/types/deploy/schemas/public/domains/attachment.sql b/extensions/@pgpm/types/deploy/schemas/public/domains/attachment.sql new file mode 100644 index 00000000..cf24ab96 --- /dev/null +++ b/extensions/@pgpm/types/deploy/schemas/public/domains/attachment.sql @@ -0,0 +1,7 @@ +-- Deploy schemas/public/domains/attachment to pg +-- requires: schemas/public/schema + +BEGIN; +CREATE DOMAIN attachment AS text CHECK (value ~ '^https?://[^\s]+$'); +COMMENT ON DOMAIN attachment IS E'@name constructiveInternalTypeAttachment'; +COMMIT; diff --git a/extensions/@pgpm/types/deploy/schemas/public/domains/email.sql b/extensions/@pgpm/types/deploy/schemas/public/domains/email.sql new file mode 100644 index 00000000..e221e1e7 --- /dev/null +++ b/extensions/@pgpm/types/deploy/schemas/public/domains/email.sql @@ -0,0 +1,8 @@ +-- Deploy schemas/public/domains/email to pg +-- requires: schemas/public/schema + +BEGIN; +CREATE DOMAIN email AS citext CHECK (value ~ '@'); +COMMENT ON DOMAIN email IS E'@name constructiveInternalTypeEmail'; +COMMIT; + diff --git a/extensions/@pgpm/types/deploy/schemas/public/domains/hostname.sql b/extensions/@pgpm/types/deploy/schemas/public/domains/hostname.sql new file mode 100644 index 00000000..326fe9ff --- /dev/null +++ b/extensions/@pgpm/types/deploy/schemas/public/domains/hostname.sql @@ -0,0 +1,8 @@ +-- Deploy schemas/public/domains/hostname to pg +-- requires: schemas/public/schema + +BEGIN; +CREATE DOMAIN hostname AS text CHECK (value ~ '^[^\s]+$'); +COMMENT ON DOMAIN hostname IS E'@name constructiveInternalTypeHostname'; +COMMIT; + diff --git a/extensions/@pgpm/types/deploy/schemas/public/domains/image.sql b/extensions/@pgpm/types/deploy/schemas/public/domains/image.sql new file mode 100644 index 00000000..84c7afb3 --- /dev/null +++ b/extensions/@pgpm/types/deploy/schemas/public/domains/image.sql @@ -0,0 +1,18 @@ +-- Deploy schemas/public/domains/image to pg +-- requires: schemas/public/schema + +BEGIN; +CREATE DOMAIN image AS jsonb CHECK ( + jsonb_typeof(value) = 'object' + AND (value ? 'url' OR value ? 'id' OR value ? 'key') + AND (NOT value ? 'url' OR (value->>'url') ~ '^https?://[^\s]+$') + AND (NOT value ? 'id' OR jsonb_typeof(value->'id') = 'string') + AND (NOT value ? 'key' OR jsonb_typeof(value->'key') = 'string') + AND (NOT value ? 'bucket' OR jsonb_typeof(value->'bucket') = 'string') + AND (NOT value ? 'provider' OR jsonb_typeof(value->'provider') = 'string') + AND (NOT value ? 'mime' OR jsonb_typeof(value->'mime') = 'string') + AND (NOT value ? 'versions' OR jsonb_typeof(value->'versions') = 'array') +); +COMMENT ON DOMAIN image IS E'@name constructiveInternalTypeImage'; +COMMIT; + diff --git a/extensions/@pgpm/types/deploy/schemas/public/domains/origin.sql b/extensions/@pgpm/types/deploy/schemas/public/domains/origin.sql new file mode 100644 index 00000000..1da23aaf --- /dev/null +++ b/extensions/@pgpm/types/deploy/schemas/public/domains/origin.sql @@ -0,0 +1,8 @@ +-- Deploy schemas/public/domains/origin to pg +-- requires: schemas/public/schema + +BEGIN; +CREATE DOMAIN origin AS text CHECK (value ~ '^https?://[^/\s]+$'); +COMMENT ON DOMAIN origin IS E'@name constructiveInternalTypeOrigin'; +COMMIT; + diff --git a/extensions/@pgpm/types/deploy/schemas/public/domains/upload.sql b/extensions/@pgpm/types/deploy/schemas/public/domains/upload.sql new file mode 100644 index 00000000..147be781 --- /dev/null +++ b/extensions/@pgpm/types/deploy/schemas/public/domains/upload.sql @@ -0,0 +1,16 @@ +-- Deploy schemas/public/domains/upload to pg +-- requires: schemas/public/schema + +BEGIN; +CREATE DOMAIN upload AS jsonb CHECK ( + jsonb_typeof(value) = 'object' + AND (value ? 'url' OR value ? 'id' OR value ? 'key') + AND (NOT value ? 'url' OR (value->>'url') ~ '^https?://[^\s]+$') + AND (NOT value ? 'id' OR jsonb_typeof(value->'id') = 'string') + AND (NOT value ? 'key' OR jsonb_typeof(value->'key') = 'string') + AND (NOT value ? 'bucket' OR jsonb_typeof(value->'bucket') = 'string') + AND (NOT value ? 'provider' OR jsonb_typeof(value->'provider') = 'string') + AND (NOT value ? 'mime' OR jsonb_typeof(value->'mime') = 'string') +); +COMMENT ON DOMAIN upload IS E'@name constructiveInternalTypeUpload'; +COMMIT; diff --git a/extensions/@pgpm/types/deploy/schemas/public/domains/url.sql b/extensions/@pgpm/types/deploy/schemas/public/domains/url.sql new file mode 100644 index 00000000..5e5a9436 --- /dev/null +++ b/extensions/@pgpm/types/deploy/schemas/public/domains/url.sql @@ -0,0 +1,8 @@ +-- Deploy schemas/public/domains/url to pg +-- requires: schemas/public/schema + +BEGIN; +CREATE DOMAIN url AS text CHECK (value ~ '^https?://[^\s]+$'); +COMMENT ON DOMAIN url IS E'@name constructiveInternalTypeUrl'; +COMMIT; + diff --git a/extensions/@pgpm/types/deploy/schemas/public/schema.sql b/extensions/@pgpm/types/deploy/schemas/public/schema.sql new file mode 100644 index 00000000..f3de2d36 --- /dev/null +++ b/extensions/@pgpm/types/deploy/schemas/public/schema.sql @@ -0,0 +1,2 @@ +-- Deploy schemas/public/schema to pg + diff --git a/extensions/@pgpm/types/package.json b/extensions/@pgpm/types/package.json new file mode 100644 index 00000000..341c2a8d --- /dev/null +++ b/extensions/@pgpm/types/package.json @@ -0,0 +1,38 @@ +{ + "name": "@pgpm/types", + "version": "0.26.0", + "description": "Core PostgreSQL data types with deploy/verify/revert SQL scripts", + "author": "Dan Lynch ", + "contributors": [ + "Constructive " + ], + "keywords": [ + "postgresql", + "pgpm", + "data-types", + "sql" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "bundle": "pgpm package", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@pgpm/verify": "0.26.0" + }, + "devDependencies": { + "pgpm": "^4.23.2" + }, + "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" + }, + "gitHead": "3badf1e5e2fc71deae9e194b779599d17ab28a0d" +} diff --git a/extensions/@pgpm/types/pgpm-types.control b/extensions/@pgpm/types/pgpm-types.control new file mode 100644 index 00000000..7c098cbe --- /dev/null +++ b/extensions/@pgpm/types/pgpm-types.control @@ -0,0 +1,8 @@ +# pgpm-types extension +comment = 'pgpm-types extension' +default_version = '0.15.5' +module_pathname = '$libdir/pgpm-types' +requires = 'plpgsql,citext,pgpm-verify' +relocatable = false +superuser = false + \ No newline at end of file diff --git a/extensions/@pgpm/types/pgpm.plan b/extensions/@pgpm/types/pgpm.plan new file mode 100644 index 00000000..c9653e3f --- /dev/null +++ b/extensions/@pgpm/types/pgpm.plan @@ -0,0 +1,12 @@ +%syntax-version=1.0.0 +%project=pgpm-types +%uri=pgpm-types + +schemas/public/schema 2017-08-11T08:11:51Z skitch # add schemas/public/schema +schemas/public/domains/attachment [schemas/public/schema] 2017-08-11T08:11:51Z skitch # add schemas/public/domains/attachment +schemas/public/domains/email [schemas/public/schema] 2017-08-11T08:11:51Z skitch # add schemas/public/domains/email +schemas/public/domains/hostname [schemas/public/schema] 2017-08-11T08:11:51Z skitch # add schemas/public/domains/hostname +schemas/public/domains/image [schemas/public/schema] 2017-08-11T08:11:51Z skitch # add schemas/public/domains/image +schemas/public/domains/origin [schemas/public/schema] 2017-08-11T08:11:51Z skitch # add schemas/public/domains/origin +schemas/public/domains/upload [schemas/public/schema] 2017-08-11T08:11:51Z skitch # add schemas/public/domains/upload +schemas/public/domains/url [schemas/public/schema] 2017-08-11T08:11:51Z skitch # add schemas/public/domains/url diff --git a/extensions/@pgpm/types/revert/schemas/public/domains/attachment.sql b/extensions/@pgpm/types/revert/schemas/public/domains/attachment.sql new file mode 100644 index 00000000..1122815a --- /dev/null +++ b/extensions/@pgpm/types/revert/schemas/public/domains/attachment.sql @@ -0,0 +1,7 @@ +-- Revert schemas/public/domains/attachment from pg + +BEGIN; + +DROP TYPE public.attachment; + +COMMIT; diff --git a/extensions/@pgpm/types/revert/schemas/public/domains/email.sql b/extensions/@pgpm/types/revert/schemas/public/domains/email.sql new file mode 100644 index 00000000..f4b86cc7 --- /dev/null +++ b/extensions/@pgpm/types/revert/schemas/public/domains/email.sql @@ -0,0 +1,7 @@ +-- Revert schemas/public/domains/email from pg + +BEGIN; + +DROP TYPE public.email; + +COMMIT; diff --git a/extensions/@pgpm/types/revert/schemas/public/domains/hostname.sql b/extensions/@pgpm/types/revert/schemas/public/domains/hostname.sql new file mode 100644 index 00000000..43298365 --- /dev/null +++ b/extensions/@pgpm/types/revert/schemas/public/domains/hostname.sql @@ -0,0 +1,7 @@ +-- Revert schemas/public/domains/hostname from pg + +BEGIN; + +DROP DOMAIN hostname; + +COMMIT; diff --git a/extensions/@pgpm/types/revert/schemas/public/domains/image.sql b/extensions/@pgpm/types/revert/schemas/public/domains/image.sql new file mode 100644 index 00000000..732325f3 --- /dev/null +++ b/extensions/@pgpm/types/revert/schemas/public/domains/image.sql @@ -0,0 +1,7 @@ +-- Revert schemas/public/domains/image from pg + +BEGIN; + +DROP TYPE public.image; + +COMMIT; diff --git a/extensions/@pgpm/types/revert/schemas/public/domains/origin.sql b/extensions/@pgpm/types/revert/schemas/public/domains/origin.sql new file mode 100644 index 00000000..6cdc9574 --- /dev/null +++ b/extensions/@pgpm/types/revert/schemas/public/domains/origin.sql @@ -0,0 +1,8 @@ +-- Revert schemas/public/domains/origin from pg + +BEGIN; + +DROP TYPE public.origin; + +COMMIT; + diff --git a/extensions/@pgpm/types/revert/schemas/public/domains/upload.sql b/extensions/@pgpm/types/revert/schemas/public/domains/upload.sql new file mode 100644 index 00000000..cd80a4c2 --- /dev/null +++ b/extensions/@pgpm/types/revert/schemas/public/domains/upload.sql @@ -0,0 +1,7 @@ +-- Revert schemas/public/domains/upload from pg + +BEGIN; + +DROP TYPE public.upload; + +COMMIT; diff --git a/extensions/@pgpm/types/revert/schemas/public/domains/url.sql b/extensions/@pgpm/types/revert/schemas/public/domains/url.sql new file mode 100644 index 00000000..f60351bc --- /dev/null +++ b/extensions/@pgpm/types/revert/schemas/public/domains/url.sql @@ -0,0 +1,7 @@ +-- Revert schemas/public/domains/url from pg + +BEGIN; + +DROP TYPE public.url; + +COMMIT; diff --git a/extensions/@pgpm/types/revert/schemas/public/schema.sql b/extensions/@pgpm/types/revert/schemas/public/schema.sql new file mode 100644 index 00000000..4225c16a --- /dev/null +++ b/extensions/@pgpm/types/revert/schemas/public/schema.sql @@ -0,0 +1 @@ +-- Revert schemas/public/schema from pg diff --git a/extensions/@pgpm/types/sql/pgpm-types--0.15.5.sql b/extensions/@pgpm/types/sql/pgpm-types--0.15.5.sql new file mode 100644 index 00000000..76d524a4 --- /dev/null +++ b/extensions/@pgpm/types/sql/pgpm-types--0.15.5.sql @@ -0,0 +1,71 @@ +\echo Use "CREATE EXTENSION pgpm-types" to load this file. \quit +CREATE DOMAIN attachment AS text + CHECK (value ~ E'^https?://[^\\s]+$'); + +COMMENT ON DOMAIN attachment IS '@name constructiveInternalTypeAttachment'; + +CREATE DOMAIN email AS citext + CHECK (value ~ '@'); + +COMMENT ON DOMAIN email IS '@name constructiveInternalTypeEmail'; + +CREATE DOMAIN hostname AS text + CHECK (value ~ E'^[^\\s]+$'); + +COMMENT ON DOMAIN hostname IS '@name constructiveInternalTypeHostname'; + +CREATE DOMAIN image AS jsonb + CHECK ( + jsonb_typeof(value) = 'object' + AND (value ? 'url' + OR value ? 'id' + OR value ? 'key') + AND (NOT (value ? 'url') + OR (value ->> 'url') ~ E'^https?://[^\\s]+$') + AND (NOT (value ? 'id') + OR jsonb_typeof(value -> 'id') = 'string') + AND (NOT (value ? 'key') + OR jsonb_typeof(value -> 'key') = 'string') + AND (NOT (value ? 'bucket') + OR jsonb_typeof(value -> 'bucket') = 'string') + AND (NOT (value ? 'provider') + OR jsonb_typeof(value -> 'provider') = 'string') + AND (NOT (value ? 'mime') + OR jsonb_typeof(value -> 'mime') = 'string') + AND (NOT (value ? 'versions') + OR jsonb_typeof(value -> 'versions') = 'array') +); + +COMMENT ON DOMAIN image IS '@name constructiveInternalTypeImage'; + +CREATE DOMAIN origin AS text + CHECK (value ~ E'^https?://[^/\\s]+$'); + +COMMENT ON DOMAIN origin IS '@name constructiveInternalTypeOrigin'; + +CREATE DOMAIN upload AS jsonb + CHECK ( + jsonb_typeof(value) = 'object' + AND (value ? 'url' + OR value ? 'id' + OR value ? 'key') + AND (NOT (value ? 'url') + OR (value ->> 'url') ~ E'^https?://[^\\s]+$') + AND (NOT (value ? 'id') + OR jsonb_typeof(value -> 'id') = 'string') + AND (NOT (value ? 'key') + OR jsonb_typeof(value -> 'key') = 'string') + AND (NOT (value ? 'bucket') + OR jsonb_typeof(value -> 'bucket') = 'string') + AND (NOT (value ? 'provider') + OR jsonb_typeof(value -> 'provider') = 'string') + AND (NOT (value ? 'mime') + OR jsonb_typeof(value -> 'mime') = 'string') +); + +COMMENT ON DOMAIN upload IS '@name constructiveInternalTypeUpload'; + +CREATE DOMAIN url AS text + CHECK (value ~ E'^https?://[^\\s]+$'); + +COMMENT ON DOMAIN url IS '@name constructiveInternalTypeUrl'; \ No newline at end of file diff --git a/extensions/@pgpm/types/verify/schemas/public/domains/attachment.sql b/extensions/@pgpm/types/verify/schemas/public/domains/attachment.sql new file mode 100644 index 00000000..34c05d9f --- /dev/null +++ b/extensions/@pgpm/types/verify/schemas/public/domains/attachment.sql @@ -0,0 +1,7 @@ +-- Verify schemas/public/domains/attachment on pg + +BEGIN; + +SELECT verify_domain ('public.attachment'); + +ROLLBACK; diff --git a/extensions/@pgpm/types/verify/schemas/public/domains/email.sql b/extensions/@pgpm/types/verify/schemas/public/domains/email.sql new file mode 100644 index 00000000..62d87b47 --- /dev/null +++ b/extensions/@pgpm/types/verify/schemas/public/domains/email.sql @@ -0,0 +1,7 @@ +-- Verify schemas/public/domains/email on pg + +BEGIN; + +SELECT verify_domain ('public.email'); + +ROLLBACK; diff --git a/extensions/@pgpm/types/verify/schemas/public/domains/hostname.sql b/extensions/@pgpm/types/verify/schemas/public/domains/hostname.sql new file mode 100644 index 00000000..e6da0854 --- /dev/null +++ b/extensions/@pgpm/types/verify/schemas/public/domains/hostname.sql @@ -0,0 +1,7 @@ +-- Verify schemas/public/domains/hostname on pg + +BEGIN; + +SELECT verify_domain ('public.hostname'); + +ROLLBACK; diff --git a/extensions/@pgpm/types/verify/schemas/public/domains/image.sql b/extensions/@pgpm/types/verify/schemas/public/domains/image.sql new file mode 100644 index 00000000..85d7e898 --- /dev/null +++ b/extensions/@pgpm/types/verify/schemas/public/domains/image.sql @@ -0,0 +1,7 @@ +-- Verify schemas/public/domains/image on pg + +BEGIN; + +SELECT verify_domain ('public.image'); + +ROLLBACK; diff --git a/extensions/@pgpm/types/verify/schemas/public/domains/origin.sql b/extensions/@pgpm/types/verify/schemas/public/domains/origin.sql new file mode 100644 index 00000000..2d69930f --- /dev/null +++ b/extensions/@pgpm/types/verify/schemas/public/domains/origin.sql @@ -0,0 +1,8 @@ +-- Verify schemas/public/domains/origin on pg + +BEGIN; + +SELECT verify_type ('public.origin'); + +ROLLBACK; + diff --git a/extensions/@pgpm/types/verify/schemas/public/domains/upload.sql b/extensions/@pgpm/types/verify/schemas/public/domains/upload.sql new file mode 100644 index 00000000..68f70d83 --- /dev/null +++ b/extensions/@pgpm/types/verify/schemas/public/domains/upload.sql @@ -0,0 +1,7 @@ +-- Verify schemas/public/domains/upload on pg + +BEGIN; + +SELECT verify_domain ('public.upload'); + +ROLLBACK; diff --git a/extensions/@pgpm/types/verify/schemas/public/domains/url.sql b/extensions/@pgpm/types/verify/schemas/public/domains/url.sql new file mode 100644 index 00000000..a2251df0 --- /dev/null +++ b/extensions/@pgpm/types/verify/schemas/public/domains/url.sql @@ -0,0 +1,7 @@ +-- Verify schemas/public/domains/url on pg + +BEGIN; + +SELECT verify_domain ('public.url'); + +ROLLBACK; diff --git a/extensions/@pgpm/types/verify/schemas/public/schema.sql b/extensions/@pgpm/types/verify/schemas/public/schema.sql new file mode 100644 index 00000000..176ef808 --- /dev/null +++ b/extensions/@pgpm/types/verify/schemas/public/schema.sql @@ -0,0 +1 @@ +-- Verify schemas/public/schema on pg diff --git a/extensions/@pgpm/verify/LICENSE b/extensions/@pgpm/verify/LICENSE new file mode 100644 index 00000000..7b18c918 --- /dev/null +++ b/extensions/@pgpm/verify/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2025 Dan Lynch +Copyright (c) 2025 Constructive + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/@pgpm/verify/Makefile b/extensions/@pgpm/verify/Makefile new file mode 100644 index 00000000..0c85491a --- /dev/null +++ b/extensions/@pgpm/verify/Makefile @@ -0,0 +1,6 @@ +EXTENSION = pgpm-verify +DATA = sql/pgpm-verify--0.15.5.sql + +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) diff --git a/extensions/@pgpm/verify/README.md b/extensions/@pgpm/verify/README.md new file mode 100644 index 00000000..58ce1edc --- /dev/null +++ b/extensions/@pgpm/verify/README.md @@ -0,0 +1,453 @@ +# @pgpm/verify + +

+ +

+ +

+ + + + + +

+ +Verification utilities for PostgreSQL modules + +## Overview + +`@pgpm/verify` is the foundational verification package used by all PGPM modules. It provides SQL functions to verify the existence and correctness of database objects during deployment, testing, and migrations. This package is essential for the deploy/verify/revert pattern, ensuring that database changes are applied correctly and can be validated programmatically. + +## Features + +- **Comprehensive Verification**: Verify tables, functions, schemas, indexes, triggers, views, domains, and roles +- **Universal Dependency**: Required by all 22 pgpm packages +- **Deploy/Verify/Revert Pattern**: Core component of safe database migrations +- **Testing Support**: Essential for integration and unit tests +- **Error Detection**: Catch deployment issues early with clear error messages +- **Pure plpgsql**: No external dependencies required + +## Installation + +If you have `pgpm` installed: + +```bash +pgpm install @pgpm/verify +pgpm deploy +``` + +This is a quick way to get started. The sections below provide more detailed installation options. + +### Prerequisites + +```bash +# Install pgpm CLI +npm install -g pgpm + +# Start local Postgres (via Docker) and export env vars +pgpm docker start +eval "$(pgpm env)" +``` + +> **Tip:** Already running Postgres? Skip the Docker step and just export your `PG*` environment variables. + +### **Add to an Existing Package** + +```bash +# 1. Install the package +pgpm install @pgpm/verify + +# 2. Deploy locally +pgpm deploy +``` + +### **Add to a New Project** + +```bash +# 1. Create a workspace +pgpm init workspace + +# 2. Create your first module +cd my-workspace +pgpm init + +# 3. Install a package +cd packages/my-module +pgpm install @pgpm/verify + +# 4. Deploy everything +pgpm deploy --createdb --database mydb1 +``` + +## Core Functions + +### verify.verify_table(schema_name, table_name) + +Verify that a table exists in the specified schema. + +**Signature:** +```sql +verify.verify_table(schema_name text, table_name text) RETURNS void +``` + +**Usage:** +```sql +-- Verify users table exists +SELECT verify.verify_table('public', 'users'); + +-- Verify in verify script +-- verify/schemas/public/tables/users/table.sql +SELECT verify.verify_table('public', 'users'); +``` + +### verify.verify_function(function_name) + +Verify that a function exists. + +**Signature:** +```sql +verify.verify_function(function_name text) RETURNS void +``` + +**Usage:** +```sql +-- Verify function exists +SELECT verify.verify_function('public.calculate_total'); + +-- Verify with schema prefix +SELECT verify.verify_function('utils.throw'); +``` + +### verify.verify_schema(schema_name) + +Verify that a schema exists. + +**Signature:** +```sql +verify.verify_schema(schema_name text) RETURNS void +``` + +**Usage:** +```sql +-- Verify schema exists +SELECT verify.verify_schema('public'); +SELECT verify.verify_schema('app_jobs'); +SELECT verify.verify_schema('status_public'); +``` + +### verify.verify_index(schema_name, index_name) + +Verify that an index exists in the specified schema. + +**Signature:** +```sql +verify.verify_index(schema_name text, index_name text) RETURNS void +``` + +**Usage:** +```sql +-- Verify index exists +SELECT verify.verify_index('public', 'users_email_idx'); +SELECT verify.verify_index('app_jobs', 'jobs_priority_run_at_id_idx'); +``` + +### verify.verify_trigger(trigger_name) + +Verify that a trigger exists. + +**Signature:** +```sql +verify.verify_trigger(trigger_name text) RETURNS void +``` + +**Usage:** +```sql +-- Verify trigger exists +SELECT verify.verify_trigger('update_updated_at_trigger'); +SELECT verify.verify_trigger('notify_worker'); +``` + +### verify.verify_view(schema_name, view_name) + +Verify that a view exists in the specified schema. + +**Signature:** +```sql +verify.verify_view(schema_name text, view_name text) RETURNS void +``` + +**Usage:** +```sql +-- Verify view exists +SELECT verify.verify_view('public', 'user_profiles_view'); +SELECT verify.verify_view('status_public', 'achievements_summary'); +``` + +### verify.verify_domain(schema_name, domain_name) + +Verify that a domain type exists in the specified schema. + +**Signature:** +```sql +verify.verify_domain(schema_name text, domain_name text) RETURNS void +``` + +**Usage:** +```sql +-- Verify domain exists +SELECT verify.verify_domain('public', 'email'); +SELECT verify.verify_domain('public', 'hostname'); +SELECT verify.verify_domain('public', 'url'); +``` + +### verify.verify_role(role_name) + +Verify that a PostgreSQL role exists. + +**Signature:** +```sql +verify.verify_role(role_name text) RETURNS void +``` + +**Usage:** +```sql +-- Verify role exists +SELECT verify.verify_role('authenticated'); +SELECT verify.verify_role('anonymous'); +SELECT verify.verify_role('administrator'); +``` + +## Usage in Deploy/Verify/Revert Pattern + +### Verify Scripts + +Every deploy script should have a corresponding verify script: + +``` +packages/example/ +├── deploy/ +│ └── schemas/public/tables/users/table.sql +├── verify/ +│ └── schemas/public/tables/users/table.sql +└── revert/ + └── schemas/public/tables/users/table.sql +``` + +**deploy/schemas/public/tables/users/table.sql:** +```sql +BEGIN; + +CREATE TABLE public.users ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + email text NOT NULL UNIQUE, + name text NOT NULL, + created_at timestamptz DEFAULT now() +); + +COMMIT; +``` + +**verify/schemas/public/tables/users/table.sql:** +```sql +SELECT verify.verify_table('public', 'users'); +``` + +**revert/schemas/public/tables/users/table.sql:** +```sql +BEGIN; +DROP TABLE IF EXISTS public.users; +COMMIT; +``` + +### Complex Verification + +Verify multiple related objects: + +**verify/schemas/app_jobs/tables/jobs/table.sql:** +```sql +-- Verify table exists +SELECT verify.verify_table('app_jobs', 'jobs'); + +-- Verify indexes exist +SELECT verify.verify_index('app_jobs', 'jobs_priority_run_at_id_idx'); +SELECT verify.verify_index('app_jobs', 'jobs_locked_by_idx'); + +-- Verify triggers exist +SELECT verify.verify_trigger('update_timestamps'); +SELECT verify.verify_trigger('notify_worker'); +``` + +## Usage in Testing + +### Integration Tests + +Use verify functions in test setup and assertions: + +```javascript +describe('User Table', () => { + it('should create users table', async () => { + await pg.query(` + CREATE TABLE public.users ( + id uuid PRIMARY KEY, + email text NOT NULL + ) + `); + + // Verify table was created + await pg.query(`SELECT verify.verify_table('public', 'users')`); + }); + + it('should create email index', async () => { + await pg.query(` + CREATE INDEX users_email_idx ON public.users(email) + `); + + // Verify index was created + await pg.query(`SELECT verify.verify_index('public', 'users_email_idx')`); + }); +}); +``` + +### Verification in Migrations + +Ensure migrations are applied correctly: + +```sql +-- Migration script +DO $$ +BEGIN + -- Create schema + CREATE SCHEMA IF NOT EXISTS app_jobs; + + -- Verify schema was created + PERFORM verify.verify_schema('app_jobs'); + + -- Create table + CREATE TABLE app_jobs.jobs ( + id serial PRIMARY KEY, + task_identifier text NOT NULL + ); + + -- Verify table was created + PERFORM verify.verify_table('app_jobs', 'jobs'); + + RAISE NOTICE 'Migration completed successfully'; +END $$; +``` + +## Integration Examples + +### With All pgpm modules + +Every pgpm module depends on `@pgpm/verify`: + +**package.json:** +```json +{ + "dependencies": { + "@pgpm/verify": "workspace:*" + } +} +``` + +**Verification in extensions:** +```sql +-- @pgpm/types verifies domains +SELECT verify.verify_domain('public', 'email'); +SELECT verify.verify_domain('public', 'hostname'); + +-- @pgpm/jobs verifies tables and functions +SELECT verify.verify_table('app_jobs', 'jobs'); +SELECT verify.verify_function('app_jobs.add_job'); + +-- @pgpm/achievements verifies schemas and triggers +SELECT verify.verify_schema('status_public'); +SELECT verify.verify_trigger('achievement_trigger'); +``` + +### With CI/CD Pipeline + +Verify deployments in CI: + +```bash +#!/bin/bash +# scripts/verify-deployment.sh + +# Deploy changes +pgpm deploy test_db --yes --recursive --createdb + +# Run verification +pgpm verify test_db --yes --recursive + +# If verification fails, revert +if [ $? -ne 0 ]; then + echo "Verification failed, reverting..." + pgpm revert test_db --yes --recursive + exit 1 +fi + +echo "Deployment verified successfully" +``` + +## Error Handling + +Verification functions throw clear errors when objects don't exist: + +```sql +-- Table doesn't exist +SELECT verify.verify_table('public', 'nonexistent_table'); +-- ERROR: Table public.nonexistent_table does not exist + +-- Function doesn't exist +SELECT verify.verify_function('public.nonexistent_function'); +-- ERROR: Function public.nonexistent_function does not exist + +-- Schema doesn't exist +SELECT verify.verify_schema('nonexistent_schema'); +-- ERROR: Schema nonexistent_schema does not exist +``` + +## Best Practices + +1. **Always Create Verify Scripts**: Every deploy script should have a matching verify script +2. **Verify Immediately**: Run verification right after deployment +3. **Verify Dependencies**: Check that required objects exist before creating dependent objects +4. **Use in Tests**: Incorporate verification in integration tests +5. **CI Integration**: Make verification part of your CI/CD pipeline +6. **Clear Naming**: Use descriptive names that match your deploy scripts + +## Use Cases + +- **Safe Migrations**: Ensure database changes are applied correctly +- **Deployment Validation**: Verify production deployments +- **Testing**: Validate test database setup +- **CI/CD**: Automated verification in continuous integration +- **Rollback Safety**: Confirm revert scripts work correctly +- **Documentation**: Self-documenting database structure +- **Debugging**: Quickly identify missing database objects + +## Testing + +```bash +pnpm test +``` + +## Dependencies + +None - this is the foundational package that all other packages depend on. + +## Related Tooling + +* [pgpm](https://github.com/constructive-io/constructive/tree/main/packages/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages. +* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/packages/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation. +* [supabase-test](https://github.com/constructive-io/constructive/tree/main/packages/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready. +* [graphile-test](https://github.com/constructive-io/constructive/tree/main/packages/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts. +* [pgsql-parser](https://github.com/constructive-io/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax. +* [libpg-query-node](https://github.com/constructive-io/libpg-query-node): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees. +* [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. + +## Disclaimer + +AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND. + +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. \ No newline at end of file diff --git a/extensions/@pgpm/verify/deploy/procedures/get_entity_from_str.sql b/extensions/@pgpm/verify/deploy/procedures/get_entity_from_str.sql new file mode 100644 index 00000000..1c29a05a --- /dev/null +++ b/extensions/@pgpm/verify/deploy/procedures/get_entity_from_str.sql @@ -0,0 +1,21 @@ +-- Deploy procedures/get_entity_from_str to pg + +BEGIN; +CREATE FUNCTION get_entity_from_str (qualified_name text) + RETURNS text + AS $$ +DECLARE + parts text[]; +BEGIN + SELECT + parse_ident(qualified_name) INTO parts; + IF cardinality(parts) > 1 THEN + RETURN parts[2]; + ELSE + RETURN parts[1]; + END IF; +END; +$$ +LANGUAGE plpgsql +STRICT; +COMMIT; diff --git a/extensions/@pgpm/verify/deploy/procedures/get_schema_from_str.sql b/extensions/@pgpm/verify/deploy/procedures/get_schema_from_str.sql new file mode 100644 index 00000000..5a833a19 --- /dev/null +++ b/extensions/@pgpm/verify/deploy/procedures/get_schema_from_str.sql @@ -0,0 +1,21 @@ +-- Deploy procedures/get_schema_from_str to pg +BEGIN; +CREATE FUNCTION get_schema_from_str (qualified_name text) + RETURNS text + AS $$ +DECLARE + parts text[]; +BEGIN + SELECT + parse_ident(qualified_name) INTO parts; + IF cardinality(parts) > 1 THEN + RETURN parts[1]; + ELSE + RETURN 'public'; + END IF; +END; +$$ +LANGUAGE plpgsql +STRICT; +COMMIT; + diff --git a/extensions/@pgpm/verify/deploy/procedures/list_indexes.sql b/extensions/@pgpm/verify/deploy/procedures/list_indexes.sql new file mode 100644 index 00000000..9e36f6ff --- /dev/null +++ b/extensions/@pgpm/verify/deploy/procedures/list_indexes.sql @@ -0,0 +1,29 @@ +-- Deploy procedures/list_indexes to pg +-- requires: procedures/get_entity_from_str +-- requires: procedures/get_schema_from_str + +BEGIN; + +CREATE FUNCTION list_indexes (_table text, _index text) + RETURNS TABLE (schema_name text, table_name text, index_name text) +AS $$ +SELECT + n.nspname::text AS schema_name, + t.relname::text AS table_name, + i.relname::text AS index_name +FROM + pg_class t, + pg_class i, + pg_index ix, + pg_catalog.pg_namespace n +WHERE + t.oid = ix.indrelid + AND i.oid = ix.indexrelid + AND n.oid = i.relnamespace + AND n.nspname = get_schema_from_str(_table) + AND i.relname = _index + AND t.relname = get_entity_from_str(_table); +$$ +LANGUAGE 'sql' IMMUTABLE; + +COMMIT; diff --git a/extensions/@pgpm/verify/deploy/procedures/list_memberships.sql b/extensions/@pgpm/verify/deploy/procedures/list_memberships.sql new file mode 100644 index 00000000..ad3d5668 --- /dev/null +++ b/extensions/@pgpm/verify/deploy/procedures/list_memberships.sql @@ -0,0 +1,31 @@ +-- Deploy procedures/list_memberships to pg + +BEGIN; + +CREATE FUNCTION list_memberships (_user text) + RETURNS TABLE (rolname text) +AS $$ WITH RECURSIVE cte AS ( + SELECT + oid + FROM + pg_roles + WHERE + rolname = _user + UNION ALL + SELECT + m.roleid + FROM + cte + JOIN pg_auth_members m ON m.member = cte.oid +) +SELECT + pg_roles.rolname::text AS rolname +FROM + cte c, + pg_roles +WHERE + pg_roles.oid = c.oid; +$$ +LANGUAGE 'sql' IMMUTABLE; + +COMMIT; diff --git a/extensions/@pgpm/verify/deploy/procedures/verify_constraint.sql b/extensions/@pgpm/verify/deploy/procedures/verify_constraint.sql new file mode 100644 index 00000000..048231db --- /dev/null +++ b/extensions/@pgpm/verify/deploy/procedures/verify_constraint.sql @@ -0,0 +1,27 @@ +-- Deploy procedures/verify_constraint to pg +BEGIN; +-- https://stackoverflow.com/questions/20087259/how-to-find-whether-unique-key-constraint-exists-for-given-columns +CREATE FUNCTION verify_constraint (_table text, _name text) + RETURNS boolean + AS $$ +BEGIN + IF EXISTS ( + SELECT + c.conname, + pg_get_constraintdef(c.oid) + FROM + pg_constraint c + WHERE + conname = _name + AND c.conrelid = _table::regclass) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent constraint --> %', _name + USING HINT = 'Please check'; +END IF; +END; +$$ +LANGUAGE 'plpgsql' +IMMUTABLE; +COMMIT; + diff --git a/extensions/@pgpm/verify/deploy/procedures/verify_domain.sql b/extensions/@pgpm/verify/deploy/procedures/verify_domain.sql new file mode 100644 index 00000000..df3c4c35 --- /dev/null +++ b/extensions/@pgpm/verify/deploy/procedures/verify_domain.sql @@ -0,0 +1,32 @@ +-- Deploy procedures/verify_domain to pg + +-- requires: procedures/get_entity_from_str +-- requires: procedures/get_schema_from_str + +BEGIN; +CREATE FUNCTION verify_domain (_type text) + RETURNS boolean + AS $$ +BEGIN + IF EXISTS ( + SELECT + pg_type.typname, + n.nspname + FROM + pg_type, + pg_catalog.pg_namespace n + WHERE + typtype = 'd' + AND typname = get_entity_from_str (_type) + AND nspname = get_schema_from_str (_type)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent type --> %', _type + USING HINT = 'Please check'; +END IF; +END; +$$ +LANGUAGE 'plpgsql' +IMMUTABLE; +COMMIT; + diff --git a/extensions/@pgpm/verify/deploy/procedures/verify_extension.sql b/extensions/@pgpm/verify/deploy/procedures/verify_extension.sql new file mode 100644 index 00000000..08caacfb --- /dev/null +++ b/extensions/@pgpm/verify/deploy/procedures/verify_extension.sql @@ -0,0 +1,24 @@ +-- Deploy procedures/verify_extension to pg +BEGIN; +CREATE FUNCTION verify_extension (_extname text) + RETURNS boolean + AS $$ +BEGIN + IF EXISTS ( + SELECT + 1 + FROM + pg_available_extensions + WHERE + name = _extname) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent extension --> %', _extname + USING HINT = 'Please check'; +END IF; +END; +$$ +LANGUAGE 'plpgsql' +IMMUTABLE; +COMMIT; + diff --git a/extensions/@pgpm/verify/deploy/procedures/verify_function.sql b/extensions/@pgpm/verify/deploy/procedures/verify_function.sql new file mode 100644 index 00000000..b74eaf42 --- /dev/null +++ b/extensions/@pgpm/verify/deploy/procedures/verify_function.sql @@ -0,0 +1,50 @@ +-- Deploy procedures/verify_function to pg + +-- requires: procedures/get_entity_from_str +-- requires: procedures/get_schema_from_str + +BEGIN; +CREATE FUNCTION verify_function (_name text, _user text DEFAULT NULL) + RETURNS boolean + AS $$ +DECLARE + check_user text; + func_oid oid; +BEGIN + IF (_user IS NOT NULL) THEN + check_user = _user; + ELSE + check_user = CURRENT_USER; + END IF; + IF position('(' IN _name) > 0 THEN + func_oid = to_regprocedure(_name); + IF func_oid IS NULL THEN + RAISE EXCEPTION 'Nonexistent function --> %', _name + USING HINT = 'Please check'; + END IF; + IF has_function_privilege(check_user, func_oid, 'execute') THEN + RETURN TRUE; + ELSE + RAISE EXCEPTION 'Nonexistent function --> %', _name + USING HINT = 'Please check'; + END IF; + END IF; + IF EXISTS ( + SELECT + has_function_privilege(check_user, p.oid, 'execute') + FROM + pg_catalog.pg_proc p + JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace + WHERE + n.nspname = get_schema_from_str (_name) + AND p.proname = get_entity_from_str (_name)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent function --> %', _name + USING HINT = 'Please check'; +END IF; +END; +$$ +LANGUAGE 'plpgsql' +IMMUTABLE; +COMMIT; diff --git a/extensions/@pgpm/verify/deploy/procedures/verify_index.sql b/extensions/@pgpm/verify/deploy/procedures/verify_index.sql new file mode 100644 index 00000000..f073ea99 --- /dev/null +++ b/extensions/@pgpm/verify/deploy/procedures/verify_index.sql @@ -0,0 +1,23 @@ +-- Deploy procedures/verify_index to pg + +-- requires: procedures/list_indexes + +BEGIN; +CREATE FUNCTION verify_index (_table text, _index text) + RETURNS boolean + AS $$ +BEGIN + IF EXISTS ( + SELECT + list_indexes (_table, _index)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent index --> %', _index + USING HINT = 'Please check'; +END IF; +END; +$$ +LANGUAGE 'plpgsql' +IMMUTABLE; +COMMIT; + diff --git a/extensions/@pgpm/verify/deploy/procedures/verify_membership.sql b/extensions/@pgpm/verify/deploy/procedures/verify_membership.sql new file mode 100644 index 00000000..b8edc2f4 --- /dev/null +++ b/extensions/@pgpm/verify/deploy/procedures/verify_membership.sql @@ -0,0 +1,27 @@ +-- Deploy procedures/verify_membership to pg + +-- requires: procedures/list_memberships + +BEGIN; +CREATE FUNCTION verify_membership (_user text, _role text) + RETURNS boolean + AS $$ +BEGIN + IF EXISTS ( + SELECT + 1 + FROM + list_memberships (_user) + WHERE + rolname = _role) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent member --> %', _user + USING HINT = 'Please check'; +END IF; +END; +$$ +LANGUAGE 'plpgsql' +IMMUTABLE; +COMMIT; + diff --git a/extensions/@pgpm/verify/deploy/procedures/verify_policy.sql b/extensions/@pgpm/verify/deploy/procedures/verify_policy.sql new file mode 100644 index 00000000..4b37f536 --- /dev/null +++ b/extensions/@pgpm/verify/deploy/procedures/verify_policy.sql @@ -0,0 +1,35 @@ +-- Deploy procedures/verify_policy to pg + +-- requires: procedures/get_entity_from_str +-- requires: procedures/get_schema_from_str + + +BEGIN; + +CREATE FUNCTION verify_policy (_policy text, _table text) + RETURNS boolean + AS $$ +BEGIN + IF EXISTS ( + SELECT + 1 + FROM + pg_class p + JOIN pg_catalog.pg_namespace n ON n.oid = p.relnamespace + JOIN pg_policy pol ON pol.polrelid = p.relfilenode + WHERE + pol.polname = _policy + AND relrowsecurity = 'true' + AND relname = get_entity_from_str (_table) + AND nspname = get_schema_from_str (_table)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent policy or missing relrowsecurity --> %', _policy + USING HINT = 'Please check'; +END IF; +END; +$$ +LANGUAGE 'plpgsql' +IMMUTABLE; +COMMIT; + diff --git a/extensions/@pgpm/verify/deploy/procedures/verify_role.sql b/extensions/@pgpm/verify/deploy/procedures/verify_role.sql new file mode 100644 index 00000000..5c87261c --- /dev/null +++ b/extensions/@pgpm/verify/deploy/procedures/verify_role.sql @@ -0,0 +1,24 @@ +-- Deploy procedures/verify_role to pg +BEGIN; +CREATE FUNCTION verify_role (_user text) + RETURNS boolean + AS $$ +BEGIN + IF EXISTS ( + SELECT + 1 + FROM + pg_roles + WHERE + rolname = _user) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent user --> %', _user + USING HINT = 'Please check'; +END IF; +END; +$$ +LANGUAGE 'plpgsql' +IMMUTABLE; +COMMIT; + diff --git a/extensions/@pgpm/verify/deploy/procedures/verify_schema.sql b/extensions/@pgpm/verify/deploy/procedures/verify_schema.sql new file mode 100644 index 00000000..3eba268b --- /dev/null +++ b/extensions/@pgpm/verify/deploy/procedures/verify_schema.sql @@ -0,0 +1,24 @@ +-- Deploy procedures/verify_schema to pg +BEGIN; +CREATE FUNCTION verify_schema (_schema text) + RETURNS boolean + AS $$ +BEGIN + IF EXISTS ( + SELECT + * + FROM + pg_catalog.pg_namespace + WHERE + nspname = _schema) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent schema --> %', _schema + USING HINT = 'Please check'; +END IF; +END; +$$ +LANGUAGE 'plpgsql' +IMMUTABLE; +COMMIT; + diff --git a/extensions/@pgpm/verify/deploy/procedures/verify_security.sql b/extensions/@pgpm/verify/deploy/procedures/verify_security.sql new file mode 100644 index 00000000..21c7aada --- /dev/null +++ b/extensions/@pgpm/verify/deploy/procedures/verify_security.sql @@ -0,0 +1,33 @@ +-- Deploy procedures/verify_security to pg + +-- requires: procedures/get_entity_from_str +-- requires: procedures/get_schema_from_str + +BEGIN; +CREATE FUNCTION verify_security (_table text) + RETURNS boolean + AS $$ +BEGIN + IF EXISTS ( + SELECT + n.oid, + relname, + n.nspname + FROM + pg_class p + JOIN pg_catalog.pg_namespace n ON n.oid = p.relnamespace + WHERE + relrowsecurity = 'true' + AND relname = get_entity_from_str (_table) + AND nspname = get_schema_from_str (_table)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent security --> %', _name + USING HINT = 'Please check'; +END IF; +END; +$$ +LANGUAGE 'plpgsql' +IMMUTABLE; +COMMIT; + diff --git a/extensions/@pgpm/verify/deploy/procedures/verify_table.sql b/extensions/@pgpm/verify/deploy/procedures/verify_table.sql new file mode 100644 index 00000000..b43ee67a --- /dev/null +++ b/extensions/@pgpm/verify/deploy/procedures/verify_table.sql @@ -0,0 +1,30 @@ +-- Deploy procedures/verify_table to pg + +-- requires: procedures/get_entity_from_str +-- requires: procedures/get_schema_from_str + + +BEGIN; +CREATE FUNCTION verify_table (_table text) + RETURNS boolean + AS $$ +BEGIN + IF EXISTS ( + SELECT + * + FROM + information_schema.tables + WHERE + table_schema = get_schema_from_str (_table) + AND table_name = get_entity_from_str (_table)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent table --> %', _table + USING HINT = 'Please check'; +END IF; +END; +$$ +LANGUAGE 'plpgsql' +IMMUTABLE; +COMMIT; + diff --git a/extensions/@pgpm/verify/deploy/procedures/verify_table_grant.sql b/extensions/@pgpm/verify/deploy/procedures/verify_table_grant.sql new file mode 100644 index 00000000..26a239d9 --- /dev/null +++ b/extensions/@pgpm/verify/deploy/procedures/verify_table_grant.sql @@ -0,0 +1,32 @@ +-- Deploy procedures/verify_table_grant to pg + +-- requires: procedures/get_entity_from_str +-- requires: procedures/get_schema_from_str + +BEGIN; +CREATE FUNCTION verify_table_grant (_table text, _privilege text, _role text) + RETURNS boolean + AS $$ +BEGIN + IF EXISTS ( + SELECT + grantee, + privilege_type + FROM + information_schema.role_table_grants + WHERE + table_schema = get_schema_from_str (_table) + AND table_name = get_entity_from_str (_table) + AND privilege_type = _privilege + AND grantee = _role) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent table grant --> %', _privilege + USING HINT = 'Please check'; +END IF; +END; +$$ +LANGUAGE 'plpgsql' +IMMUTABLE; +COMMIT; + diff --git a/extensions/@pgpm/verify/deploy/procedures/verify_trigger.sql b/extensions/@pgpm/verify/deploy/procedures/verify_trigger.sql new file mode 100644 index 00000000..e74fa831 --- /dev/null +++ b/extensions/@pgpm/verify/deploy/procedures/verify_trigger.sql @@ -0,0 +1,32 @@ +-- Deploy procedures/verify_trigger to pg + +-- requires: procedures/get_entity_from_str +-- requires: procedures/get_schema_from_str + + +BEGIN; +CREATE FUNCTION verify_trigger (_trigger text) + RETURNS boolean + AS $$ +BEGIN + IF EXISTS ( + SELECT + pg_trigger.tgname, + n.nspname + FROM + pg_trigger, + pg_catalog.pg_namespace n + WHERE + tgname = get_entity_from_str (_trigger) + AND nspname = get_schema_from_str (_trigger)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent trigger --> %', _trigger + USING HINT = 'Please check'; +END IF; +END; +$$ +LANGUAGE 'plpgsql' +IMMUTABLE; +COMMIT; + diff --git a/extensions/@pgpm/verify/deploy/procedures/verify_type.sql b/extensions/@pgpm/verify/deploy/procedures/verify_type.sql new file mode 100644 index 00000000..7dd1c1dc --- /dev/null +++ b/extensions/@pgpm/verify/deploy/procedures/verify_type.sql @@ -0,0 +1,32 @@ +-- Deploy procedures/verify_type to pg + +-- requires: procedures/get_entity_from_str +-- requires: procedures/get_schema_from_str + + +BEGIN; +CREATE FUNCTION verify_type (_type text) + RETURNS boolean + AS $$ +BEGIN + IF EXISTS ( + SELECT + pg_type.typname, + n.nspname + FROM + pg_type, + pg_catalog.pg_namespace n + WHERE + typname = get_entity_from_str (_type) + AND nspname = get_schema_from_str (_type)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent type --> %', _type + USING HINT = 'Please check'; +END IF; +END; +$$ +LANGUAGE 'plpgsql' +IMMUTABLE; +COMMIT; + diff --git a/extensions/@pgpm/verify/deploy/procedures/verify_view.sql b/extensions/@pgpm/verify/deploy/procedures/verify_view.sql new file mode 100644 index 00000000..09fff01c --- /dev/null +++ b/extensions/@pgpm/verify/deploy/procedures/verify_view.sql @@ -0,0 +1,30 @@ +-- Deploy procedures/verify_view to pg + +-- requires: procedures/get_entity_from_str +-- requires: procedures/get_schema_from_str + + +BEGIN; +CREATE FUNCTION verify_view (_view text) + RETURNS boolean + AS $$ +BEGIN + IF EXISTS ( + SELECT + * + FROM + information_schema.views + WHERE + table_schema = get_schema_from_str (_view) + AND table_name = get_entity_from_str (_view)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent view --> %', _view + USING HINT = 'Please check'; +END IF; +END; +$$ +LANGUAGE 'plpgsql' +IMMUTABLE; +COMMIT; + diff --git a/extensions/@pgpm/verify/package.json b/extensions/@pgpm/verify/package.json new file mode 100644 index 00000000..89d5fdae --- /dev/null +++ b/extensions/@pgpm/verify/package.json @@ -0,0 +1,35 @@ +{ + "name": "@pgpm/verify", + "version": "0.26.0", + "description": "Verification utilities for PGPM deploy/verify/revert workflow", + "author": "Dan Lynch ", + "contributors": [ + "Constructive " + ], + "keywords": [ + "postgresql", + "pgpm", + "verification", + "testing" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "bundle": "pgpm package", + "test": "jest", + "test:watch": "jest --watch" + }, + "devDependencies": { + "pgpm": "^4.23.2" + }, + "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" + }, + "gitHead": "3badf1e5e2fc71deae9e194b779599d17ab28a0d" +} diff --git a/extensions/@pgpm/verify/pgpm-verify.control b/extensions/@pgpm/verify/pgpm-verify.control new file mode 100644 index 00000000..7b3c7b8f --- /dev/null +++ b/extensions/@pgpm/verify/pgpm-verify.control @@ -0,0 +1,8 @@ +# pgpm-verify extension +comment = 'pgpm-verify extension' +default_version = '0.15.5' +module_pathname = '$libdir/pgpm-verify' +requires = 'plpgsql' +relocatable = false +superuser = false + \ No newline at end of file diff --git a/extensions/@pgpm/verify/pgpm.plan b/extensions/@pgpm/verify/pgpm.plan new file mode 100644 index 00000000..0c9ea10c --- /dev/null +++ b/extensions/@pgpm/verify/pgpm.plan @@ -0,0 +1,24 @@ +%syntax-version=1.0.0 +%project=pgpm-verify +%uri=pgpm-verify + +procedures/get_entity_from_str 2017-08-11T08:11:51Z skitch # add procedures/get_entity_from_str +procedures/get_schema_from_str 2017-08-11T08:11:51Z skitch # add procedures/get_schema_from_str +procedures/list_indexes [procedures/get_entity_from_str procedures/get_schema_from_str] 2017-08-11T08:11:51Z skitch # add procedures/list_indexes +procedures/list_memberships 2017-08-11T08:11:51Z skitch # add procedures/list_memberships +procedures/verify_constraint 2017-08-11T08:11:51Z skitch # add procedures/verify_constraint +procedures/verify_domain [procedures/get_entity_from_str procedures/get_schema_from_str] 2017-08-11T08:11:51Z skitch # add procedures/verify_domain +procedures/verify_extension 2017-08-11T08:11:51Z skitch # add procedures/verify_extension +procedures/verify_function [procedures/get_entity_from_str procedures/get_schema_from_str] 2017-08-11T08:11:51Z skitch # add procedures/verify_function +procedures/verify_index [procedures/list_indexes] 2017-08-11T08:11:51Z skitch # add procedures/verify_index +procedures/verify_membership [procedures/list_memberships] 2017-08-11T08:11:51Z skitch # add procedures/verify_membership +procedures/verify_policy [procedures/get_entity_from_str procedures/get_schema_from_str] 2017-08-11T08:11:51Z skitch # add procedures/verify_policy +procedures/verify_role 2017-08-11T08:11:51Z skitch # add procedures/verify_role +procedures/verify_schema 2017-08-11T08:11:51Z skitch # add procedures/verify_schema +procedures/verify_security [procedures/get_entity_from_str procedures/get_schema_from_str] 2017-08-11T08:11:51Z skitch # add procedures/verify_security +procedures/verify_table_grant [procedures/get_entity_from_str procedures/get_schema_from_str] 2017-08-11T08:11:51Z skitch # add procedures/verify_table_grant +procedures/verify_table [procedures/get_entity_from_str procedures/get_schema_from_str] 2017-08-11T08:11:51Z skitch # add procedures/verify_table +procedures/verify_trigger [procedures/get_entity_from_str procedures/get_schema_from_str] 2017-08-11T08:11:51Z skitch # add procedures/verify_trigger +procedures/verify_type [procedures/get_entity_from_str procedures/get_schema_from_str] 2017-08-11T08:11:51Z skitch # add procedures/verify_type +procedures/verify_view [procedures/get_entity_from_str procedures/get_schema_from_str] 2017-08-11T08:11:51Z skitch # add procedures/verify_view +@0.1.0 2025-08-26T23:56:48Z pgpm # verify diff --git a/extensions/@pgpm/verify/revert/procedures/get_entity_from_str.sql b/extensions/@pgpm/verify/revert/procedures/get_entity_from_str.sql new file mode 100644 index 00000000..d8692b00 --- /dev/null +++ b/extensions/@pgpm/verify/revert/procedures/get_entity_from_str.sql @@ -0,0 +1,7 @@ +-- Revert pgpm-verify:procedures/get_entity_from_str from pg + +BEGIN; + +DROP FUNCTION get_entity_from_str(text); + +COMMIT; diff --git a/extensions/@pgpm/verify/revert/procedures/get_schema_from_str.sql b/extensions/@pgpm/verify/revert/procedures/get_schema_from_str.sql new file mode 100644 index 00000000..4a507ca3 --- /dev/null +++ b/extensions/@pgpm/verify/revert/procedures/get_schema_from_str.sql @@ -0,0 +1,7 @@ +-- Revert pgpm-verify:procedures/get_schema_from_str from pg + +BEGIN; + +DROP FUNCTION get_schema_from_str(text); + +COMMIT; diff --git a/extensions/@pgpm/verify/revert/procedures/list_indexes.sql b/extensions/@pgpm/verify/revert/procedures/list_indexes.sql new file mode 100644 index 00000000..705b8033 --- /dev/null +++ b/extensions/@pgpm/verify/revert/procedures/list_indexes.sql @@ -0,0 +1,7 @@ +-- Revert pgpm-verify:procedures/list_indexes from pg + +BEGIN; + +DROP FUNCTION list_indexes(text, text); + +COMMIT; diff --git a/extensions/@pgpm/verify/revert/procedures/list_memberships.sql b/extensions/@pgpm/verify/revert/procedures/list_memberships.sql new file mode 100644 index 00000000..e2f80557 --- /dev/null +++ b/extensions/@pgpm/verify/revert/procedures/list_memberships.sql @@ -0,0 +1,7 @@ +-- Revert pgpm-verify:procedures/list_memberships from pg + +BEGIN; + +DROP FUNCTION list_memberships(text); + +COMMIT; diff --git a/extensions/@pgpm/verify/revert/procedures/verify_constraint.sql b/extensions/@pgpm/verify/revert/procedures/verify_constraint.sql new file mode 100644 index 00000000..0db59385 --- /dev/null +++ b/extensions/@pgpm/verify/revert/procedures/verify_constraint.sql @@ -0,0 +1,7 @@ +-- Revert procedures/verify_constraint from pg + +BEGIN; + +DROP FUNCTION verify_constraint; + +COMMIT; diff --git a/extensions/@pgpm/verify/revert/procedures/verify_domain.sql b/extensions/@pgpm/verify/revert/procedures/verify_domain.sql new file mode 100644 index 00000000..d7a25634 --- /dev/null +++ b/extensions/@pgpm/verify/revert/procedures/verify_domain.sql @@ -0,0 +1,7 @@ +-- Revert procedures/verify_domain from pg + +BEGIN; + +DROP FUNCTION public.verify_domain; + +COMMIT; diff --git a/extensions/@pgpm/verify/revert/procedures/verify_extension.sql b/extensions/@pgpm/verify/revert/procedures/verify_extension.sql new file mode 100644 index 00000000..95fb3df2 --- /dev/null +++ b/extensions/@pgpm/verify/revert/procedures/verify_extension.sql @@ -0,0 +1,7 @@ +-- Revert procedures/verify_extension from pg + +BEGIN; + +DROP FUNCTION public.verify_extension; + +COMMIT; diff --git a/extensions/@pgpm/verify/revert/procedures/verify_function.sql b/extensions/@pgpm/verify/revert/procedures/verify_function.sql new file mode 100644 index 00000000..457a7da0 --- /dev/null +++ b/extensions/@pgpm/verify/revert/procedures/verify_function.sql @@ -0,0 +1,7 @@ +-- Revert procedures/verify_function from pg + +BEGIN; + +DROP FUNCTION verify_function; + +COMMIT; diff --git a/extensions/@pgpm/verify/revert/procedures/verify_index.sql b/extensions/@pgpm/verify/revert/procedures/verify_index.sql new file mode 100644 index 00000000..6ddda346 --- /dev/null +++ b/extensions/@pgpm/verify/revert/procedures/verify_index.sql @@ -0,0 +1,7 @@ +-- Revert procedures/verify_index from pg + +BEGIN; + +DROP FUNCTION verify_index; + +COMMIT; diff --git a/extensions/@pgpm/verify/revert/procedures/verify_membership.sql b/extensions/@pgpm/verify/revert/procedures/verify_membership.sql new file mode 100644 index 00000000..62e69dd0 --- /dev/null +++ b/extensions/@pgpm/verify/revert/procedures/verify_membership.sql @@ -0,0 +1,7 @@ +-- Revert procedures/verify_membership from pg + +BEGIN; + +DROP FUNCTION verify_membership; + +COMMIT; diff --git a/extensions/@pgpm/verify/revert/procedures/verify_policy.sql b/extensions/@pgpm/verify/revert/procedures/verify_policy.sql new file mode 100644 index 00000000..e16b7b5d --- /dev/null +++ b/extensions/@pgpm/verify/revert/procedures/verify_policy.sql @@ -0,0 +1,7 @@ +-- Revert procedures/verify_policy from pg + +BEGIN; + +DROP FUNCTION verify_policy; + +COMMIT; diff --git a/extensions/@pgpm/verify/revert/procedures/verify_role.sql b/extensions/@pgpm/verify/revert/procedures/verify_role.sql new file mode 100644 index 00000000..5c649e8a --- /dev/null +++ b/extensions/@pgpm/verify/revert/procedures/verify_role.sql @@ -0,0 +1,7 @@ +-- Revert procedures/verify_role from pg + +BEGIN; + +DROP FUNCTION verify_role; + +COMMIT; diff --git a/extensions/@pgpm/verify/revert/procedures/verify_schema.sql b/extensions/@pgpm/verify/revert/procedures/verify_schema.sql new file mode 100644 index 00000000..c058f28e --- /dev/null +++ b/extensions/@pgpm/verify/revert/procedures/verify_schema.sql @@ -0,0 +1,7 @@ +-- Revert procedures/verify_schema from pg + +BEGIN; + +DROP FUNCTION verify_schema; + +COMMIT; diff --git a/extensions/@pgpm/verify/revert/procedures/verify_security.sql b/extensions/@pgpm/verify/revert/procedures/verify_security.sql new file mode 100644 index 00000000..3b58b2fc --- /dev/null +++ b/extensions/@pgpm/verify/revert/procedures/verify_security.sql @@ -0,0 +1,7 @@ +-- Revert procedures/verify_security from pg + +BEGIN; + +DROP FUNCTION verify_security; + +COMMIT; diff --git a/extensions/@pgpm/verify/revert/procedures/verify_table.sql b/extensions/@pgpm/verify/revert/procedures/verify_table.sql new file mode 100644 index 00000000..d2235bdf --- /dev/null +++ b/extensions/@pgpm/verify/revert/procedures/verify_table.sql @@ -0,0 +1,7 @@ +-- Revert procedures/verify_table from pg + +BEGIN; + +DROP FUNCTION verify_table; + +COMMIT; diff --git a/extensions/@pgpm/verify/revert/procedures/verify_table_grant.sql b/extensions/@pgpm/verify/revert/procedures/verify_table_grant.sql new file mode 100644 index 00000000..780e8497 --- /dev/null +++ b/extensions/@pgpm/verify/revert/procedures/verify_table_grant.sql @@ -0,0 +1,7 @@ +-- Revert procedures/verify_table_grant from pg + +BEGIN; + +DROP FUNCTION verify_table_grant; + +COMMIT; diff --git a/extensions/@pgpm/verify/revert/procedures/verify_trigger.sql b/extensions/@pgpm/verify/revert/procedures/verify_trigger.sql new file mode 100644 index 00000000..c51d8a29 --- /dev/null +++ b/extensions/@pgpm/verify/revert/procedures/verify_trigger.sql @@ -0,0 +1,7 @@ +-- Revert procedures/verify_trigger from pg + +BEGIN; + +DROP FUNCTION verify_trigger; + +COMMIT; diff --git a/extensions/@pgpm/verify/revert/procedures/verify_type.sql b/extensions/@pgpm/verify/revert/procedures/verify_type.sql new file mode 100644 index 00000000..da4e1e65 --- /dev/null +++ b/extensions/@pgpm/verify/revert/procedures/verify_type.sql @@ -0,0 +1,7 @@ +-- Revert procedures/verify_type from pg + +BEGIN; + +DROP FUNCTION verify_type; + +COMMIT; diff --git a/extensions/@pgpm/verify/revert/procedures/verify_view.sql b/extensions/@pgpm/verify/revert/procedures/verify_view.sql new file mode 100644 index 00000000..632e34a5 --- /dev/null +++ b/extensions/@pgpm/verify/revert/procedures/verify_view.sql @@ -0,0 +1,7 @@ +-- Revert procedures/verify_view from pg + +BEGIN; + +DROP FUNCTION public.verify_view; + +COMMIT; diff --git a/extensions/@pgpm/verify/sql/pgpm-verify--0.15.3.sql b/extensions/@pgpm/verify/sql/pgpm-verify--0.15.3.sql new file mode 100644 index 00000000..515c7179 --- /dev/null +++ b/extensions/@pgpm/verify/sql/pgpm-verify--0.15.3.sql @@ -0,0 +1,372 @@ +\echo Use "CREATE EXTENSION pgpm-verify" to load this file. \quit +CREATE FUNCTION get_entity_from_str(qualified_name text) RETURNS text AS $EOFCODE$ +DECLARE + parts text[]; +BEGIN + SELECT + parse_ident(qualified_name) INTO parts; + IF cardinality(parts) > 1 THEN + RETURN parts[2]; + ELSE + RETURN parts[1]; + END IF; +END; +$EOFCODE$ LANGUAGE plpgsql STRICT; + +CREATE FUNCTION get_schema_from_str(qualified_name text) RETURNS text AS $EOFCODE$ +DECLARE + parts text[]; +BEGIN + SELECT + parse_ident(qualified_name) INTO parts; + IF cardinality(parts) > 1 THEN + RETURN parts[1]; + ELSE + RETURN 'public'; + END IF; +END; +$EOFCODE$ LANGUAGE plpgsql STRICT; + +CREATE FUNCTION list_indexes(_table text, _index text) RETURNS TABLE ( schema_name text, table_name text, index_name text ) AS $EOFCODE$ +SELECT + n.nspname::text AS schema_name, + t.relname::text AS table_name, + i.relname::text AS index_name +FROM + pg_class t, + pg_class i, + pg_index ix, + pg_catalog.pg_namespace n +WHERE + t.oid = ix.indrelid + AND i.oid = ix.indexrelid + AND n.oid = i.relnamespace + AND n.nspname = get_schema_from_str(_table) + AND i.relname = _index + AND t.relname = get_entity_from_str(_table); +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION list_memberships(_user text) RETURNS TABLE ( rolname text ) AS $EOFCODE$ WITH RECURSIVE cte AS ( + SELECT + oid + FROM + pg_roles + WHERE + rolname = _user + UNION ALL + SELECT + m.roleid + FROM + cte + JOIN pg_auth_members m ON m.member = cte.oid +) +SELECT + pg_roles.rolname::text AS rolname +FROM + cte c, + pg_roles +WHERE + pg_roles.oid = c.oid; +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION verify_constraint(_table text, _name text) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + c.conname, + pg_get_constraintdef(c.oid) + FROM + pg_constraint c + WHERE + conname = _name + AND c.conrelid = _table::regclass) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent constraint --> %', _name + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_domain(_type text) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + pg_type.typname, + n.nspname + FROM + pg_type, + pg_catalog.pg_namespace n + WHERE + typtype = 'd' + AND typname = get_entity_from_str (_type) + AND nspname = get_schema_from_str (_type)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent type --> %', _type + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_extension(_extname text) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + 1 + FROM + pg_available_extensions + WHERE + name = _extname) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent extension --> %', _extname + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_function(_name text, _user text DEFAULT NULL) RETURNS boolean AS $EOFCODE$ +DECLARE + check_user text; + func_oid oid; +BEGIN + IF (_user IS NOT NULL) THEN + check_user = _user; + ELSE + check_user = CURRENT_USER; + END IF; + IF position('(' IN _name) > 0 THEN + func_oid = to_regprocedure(_name); + IF func_oid IS NULL THEN + RAISE EXCEPTION 'Nonexistent function --> %', _name + USING HINT = 'Please check'; + END IF; + IF has_function_privilege(check_user, func_oid, 'execute') THEN + RETURN TRUE; + ELSE + RAISE EXCEPTION 'Nonexistent function --> %', _name + USING HINT = 'Please check'; + END IF; + END IF; + IF EXISTS ( + SELECT + has_function_privilege(check_user, p.oid, 'execute') + FROM + pg_catalog.pg_proc p + JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace + WHERE + n.nspname = get_schema_from_str (_name) + AND p.proname = get_entity_from_str (_name)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent function --> %', _name + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_index(_table text, _index text) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + list_indexes (_table, _index)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent index --> %', _index + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_membership(_user text, _role text) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + 1 + FROM + list_memberships (_user) + WHERE + rolname = _role) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent member --> %', _user + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_policy(_policy text, _table text) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + 1 + FROM + pg_class p + JOIN pg_catalog.pg_namespace n ON n.oid = p.relnamespace + JOIN pg_policy pol ON pol.polrelid = p.relfilenode + WHERE + pol.polname = _policy + AND relrowsecurity = 'true' + AND relname = get_entity_from_str (_table) + AND nspname = get_schema_from_str (_table)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent policy or missing relrowsecurity --> %', _policy + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_role(_user text) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + 1 + FROM + pg_roles + WHERE + rolname = _user) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent user --> %', _user + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_schema(_schema text) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + * + FROM + pg_catalog.pg_namespace + WHERE + nspname = _schema) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent schema --> %', _schema + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_security(_table text) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + n.oid, + relname, + n.nspname + FROM + pg_class p + JOIN pg_catalog.pg_namespace n ON n.oid = p.relnamespace + WHERE + relrowsecurity = 'true' + AND relname = get_entity_from_str (_table) + AND nspname = get_schema_from_str (_table)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent security --> %', _name + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_table_grant(_table text, _privilege text, _role text) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + grantee, + privilege_type + FROM + information_schema.role_table_grants + WHERE + table_schema = get_schema_from_str (_table) + AND table_name = get_entity_from_str (_table) + AND privilege_type = _privilege + AND grantee = _role) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent table grant --> %', _privilege + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_table(_table text) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + * + FROM + information_schema.tables + WHERE + table_schema = get_schema_from_str (_table) + AND table_name = get_entity_from_str (_table)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent table --> %', _table + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_trigger(_trigger text) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + pg_trigger.tgname, + n.nspname + FROM + pg_trigger, + pg_catalog.pg_namespace n + WHERE + tgname = get_entity_from_str (_trigger) + AND nspname = get_schema_from_str (_trigger)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent trigger --> %', _trigger + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_type(_type text) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + pg_type.typname, + n.nspname + FROM + pg_type, + pg_catalog.pg_namespace n + WHERE + typname = get_entity_from_str (_type) + AND nspname = get_schema_from_str (_type)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent type --> %', _type + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_view(_view text) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + * + FROM + information_schema.views + WHERE + table_schema = get_schema_from_str (_view) + AND table_name = get_entity_from_str (_view)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent view --> %', _view + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; \ No newline at end of file diff --git a/extensions/@pgpm/verify/sql/pgpm-verify--0.15.5.sql b/extensions/@pgpm/verify/sql/pgpm-verify--0.15.5.sql new file mode 100644 index 00000000..bf6939ab --- /dev/null +++ b/extensions/@pgpm/verify/sql/pgpm-verify--0.15.5.sql @@ -0,0 +1,424 @@ +\echo Use "CREATE EXTENSION pgpm-verify" to load this file. \quit +CREATE FUNCTION get_entity_from_str( + qualified_name text +) RETURNS text AS $EOFCODE$ +DECLARE + parts text[]; +BEGIN + SELECT + parse_ident(qualified_name) INTO parts; + IF cardinality(parts) > 1 THEN + RETURN parts[2]; + ELSE + RETURN parts[1]; + END IF; +END; +$EOFCODE$ LANGUAGE plpgsql STRICT; + +CREATE FUNCTION get_schema_from_str( + qualified_name text +) RETURNS text AS $EOFCODE$ +DECLARE + parts text[]; +BEGIN + SELECT + parse_ident(qualified_name) INTO parts; + IF cardinality(parts) > 1 THEN + RETURN parts[1]; + ELSE + RETURN 'public'; + END IF; +END; +$EOFCODE$ LANGUAGE plpgsql STRICT; + +CREATE FUNCTION list_indexes( + _table text, + _index text +) RETURNS TABLE ( + schema_name text, + table_name text, + index_name text +) AS $EOFCODE$ +SELECT + n.nspname::text AS schema_name, + t.relname::text AS table_name, + i.relname::text AS index_name +FROM + pg_class t, + pg_class i, + pg_index ix, + pg_catalog.pg_namespace n +WHERE + t.oid = ix.indrelid + AND i.oid = ix.indexrelid + AND n.oid = i.relnamespace + AND n.nspname = get_schema_from_str(_table) + AND i.relname = _index + AND t.relname = get_entity_from_str(_table); +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION list_memberships( + _user text +) RETURNS TABLE ( + rolname text +) AS $EOFCODE$ WITH RECURSIVE cte AS ( + SELECT + oid + FROM + pg_roles + WHERE + rolname = _user + UNION ALL + SELECT + m.roleid + FROM + cte + JOIN pg_auth_members m ON m.member = cte.oid +) +SELECT + pg_roles.rolname::text AS rolname +FROM + cte c, + pg_roles +WHERE + pg_roles.oid = c.oid; +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION verify_constraint( + _table text, + _name text +) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + c.conname, + pg_get_constraintdef(c.oid) + FROM + pg_constraint c + WHERE + conname = _name + AND c.conrelid = _table::regclass) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent constraint --> %', _name + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_domain( + _type text +) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + pg_type.typname, + n.nspname + FROM + pg_type, + pg_catalog.pg_namespace n + WHERE + typtype = 'd' + AND typname = get_entity_from_str (_type) + AND nspname = get_schema_from_str (_type)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent type --> %', _type + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_extension( + _extname text +) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + 1 + FROM + pg_available_extensions + WHERE + name = _extname) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent extension --> %', _extname + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_function( + _name text, + _user text DEFAULT NULL +) RETURNS boolean AS $EOFCODE$ +DECLARE + check_user text; + func_oid oid; +BEGIN + IF (_user IS NOT NULL) THEN + check_user = _user; + ELSE + check_user = CURRENT_USER; + END IF; + IF position('(' IN _name) > 0 THEN + func_oid = to_regprocedure(_name); + IF func_oid IS NULL THEN + RAISE EXCEPTION 'Nonexistent function --> %', _name + USING HINT = 'Please check'; + END IF; + IF has_function_privilege(check_user, func_oid, 'execute') THEN + RETURN TRUE; + ELSE + RAISE EXCEPTION 'Nonexistent function --> %', _name + USING HINT = 'Please check'; + END IF; + END IF; + IF EXISTS ( + SELECT + has_function_privilege(check_user, p.oid, 'execute') + FROM + pg_catalog.pg_proc p + JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace + WHERE + n.nspname = get_schema_from_str (_name) + AND p.proname = get_entity_from_str (_name)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent function --> %', _name + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_index( + _table text, + _index text +) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + list_indexes (_table, _index)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent index --> %', _index + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_membership( + _user text, + _role text +) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + 1 + FROM + list_memberships (_user) + WHERE + rolname = _role) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent member --> %', _user + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_policy( + _policy text, + _table text +) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + 1 + FROM + pg_class p + JOIN pg_catalog.pg_namespace n ON n.oid = p.relnamespace + JOIN pg_policy pol ON pol.polrelid = p.relfilenode + WHERE + pol.polname = _policy + AND relrowsecurity = 'true' + AND relname = get_entity_from_str (_table) + AND nspname = get_schema_from_str (_table)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent policy or missing relrowsecurity --> %', _policy + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_role( + _user text +) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + 1 + FROM + pg_roles + WHERE + rolname = _user) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent user --> %', _user + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_schema( + _schema text +) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + * + FROM + pg_catalog.pg_namespace + WHERE + nspname = _schema) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent schema --> %', _schema + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_security( + _table text +) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + n.oid, + relname, + n.nspname + FROM + pg_class p + JOIN pg_catalog.pg_namespace n ON n.oid = p.relnamespace + WHERE + relrowsecurity = 'true' + AND relname = get_entity_from_str (_table) + AND nspname = get_schema_from_str (_table)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent security --> %', _name + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_table_grant( + _table text, + _privilege text, + _role text +) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + grantee, + privilege_type + FROM + information_schema.role_table_grants + WHERE + table_schema = get_schema_from_str (_table) + AND table_name = get_entity_from_str (_table) + AND privilege_type = _privilege + AND grantee = _role) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent table grant --> %', _privilege + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_table( + _table text +) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + * + FROM + information_schema.tables + WHERE + table_schema = get_schema_from_str (_table) + AND table_name = get_entity_from_str (_table)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent table --> %', _table + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_trigger( + _trigger text +) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + pg_trigger.tgname, + n.nspname + FROM + pg_trigger, + pg_catalog.pg_namespace n + WHERE + tgname = get_entity_from_str (_trigger) + AND nspname = get_schema_from_str (_trigger)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent trigger --> %', _trigger + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_type( + _type text +) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + pg_type.typname, + n.nspname + FROM + pg_type, + pg_catalog.pg_namespace n + WHERE + typname = get_entity_from_str (_type) + AND nspname = get_schema_from_str (_type)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent type --> %', _type + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION verify_view( + _view text +) RETURNS boolean AS $EOFCODE$ +BEGIN + IF EXISTS ( + SELECT + * + FROM + information_schema.views + WHERE + table_schema = get_schema_from_str (_view) + AND table_name = get_entity_from_str (_view)) THEN + RETURN TRUE; +ELSE + RAISE EXCEPTION 'Nonexistent view --> %', _view + USING HINT = 'Please check'; +END IF; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; \ No newline at end of file diff --git a/extensions/@pgpm/verify/verify/procedures/get_entity_from_str.sql b/extensions/@pgpm/verify/verify/procedures/get_entity_from_str.sql new file mode 100644 index 00000000..59148db5 --- /dev/null +++ b/extensions/@pgpm/verify/verify/procedures/get_entity_from_str.sql @@ -0,0 +1,7 @@ +-- Verify pgpm-verify:procedures/get_entity_from_str on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; diff --git a/extensions/@pgpm/verify/verify/procedures/get_schema_from_str.sql b/extensions/@pgpm/verify/verify/procedures/get_schema_from_str.sql new file mode 100644 index 00000000..fda49efd --- /dev/null +++ b/extensions/@pgpm/verify/verify/procedures/get_schema_from_str.sql @@ -0,0 +1,7 @@ +-- Verify pgpm-verify:procedures/get_schema_from_str on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; diff --git a/extensions/@pgpm/verify/verify/procedures/list_indexes.sql b/extensions/@pgpm/verify/verify/procedures/list_indexes.sql new file mode 100644 index 00000000..8a11f2b2 --- /dev/null +++ b/extensions/@pgpm/verify/verify/procedures/list_indexes.sql @@ -0,0 +1,7 @@ +-- Verify pgpm-verify:procedures/list_indexes on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; diff --git a/extensions/@pgpm/verify/verify/procedures/list_memberships.sql b/extensions/@pgpm/verify/verify/procedures/list_memberships.sql new file mode 100644 index 00000000..1b00454c --- /dev/null +++ b/extensions/@pgpm/verify/verify/procedures/list_memberships.sql @@ -0,0 +1,7 @@ +-- Verify pgpm-verify:procedures/list_memberships on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; diff --git a/extensions/@pgpm/verify/verify/procedures/verify_constraint.sql b/extensions/@pgpm/verify/verify/procedures/verify_constraint.sql new file mode 100644 index 00000000..fc98e323 --- /dev/null +++ b/extensions/@pgpm/verify/verify/procedures/verify_constraint.sql @@ -0,0 +1,7 @@ +-- Verify procedures/verify_constraint on pg + +BEGIN; + +SELECT verify_function ('public.verify_constraint'); + +ROLLBACK; diff --git a/extensions/@pgpm/verify/verify/procedures/verify_domain.sql b/extensions/@pgpm/verify/verify/procedures/verify_domain.sql new file mode 100644 index 00000000..44eba896 --- /dev/null +++ b/extensions/@pgpm/verify/verify/procedures/verify_domain.sql @@ -0,0 +1,7 @@ +-- Verify procedures/verify_domain on pg + +BEGIN; + +SELECT verify_function ('public.verify_domain'); + +ROLLBACK; diff --git a/extensions/@pgpm/verify/verify/procedures/verify_extension.sql b/extensions/@pgpm/verify/verify/procedures/verify_extension.sql new file mode 100644 index 00000000..5d1d2b81 --- /dev/null +++ b/extensions/@pgpm/verify/verify/procedures/verify_extension.sql @@ -0,0 +1,7 @@ +-- Verify procedures/verify_extension on pg + +BEGIN; + +SELECT verify_function ('public.verify_extension'); + +ROLLBACK; diff --git a/extensions/@pgpm/verify/verify/procedures/verify_function.sql b/extensions/@pgpm/verify/verify/procedures/verify_function.sql new file mode 100644 index 00000000..857015ea --- /dev/null +++ b/extensions/@pgpm/verify/verify/procedures/verify_function.sql @@ -0,0 +1,7 @@ +-- Verify procedures/verify_function on pg + +BEGIN; + +SELECT verify_function ('public.verify_function'); + +ROLLBACK; diff --git a/extensions/@pgpm/verify/verify/procedures/verify_index.sql b/extensions/@pgpm/verify/verify/procedures/verify_index.sql new file mode 100644 index 00000000..ce75113e --- /dev/null +++ b/extensions/@pgpm/verify/verify/procedures/verify_index.sql @@ -0,0 +1,7 @@ +-- Verify procedures/verify_index on pg + +BEGIN; + +SELECT verify_function ('public.verify_index'); + +ROLLBACK; diff --git a/extensions/@pgpm/verify/verify/procedures/verify_membership.sql b/extensions/@pgpm/verify/verify/procedures/verify_membership.sql new file mode 100644 index 00000000..547dfc47 --- /dev/null +++ b/extensions/@pgpm/verify/verify/procedures/verify_membership.sql @@ -0,0 +1,7 @@ +-- Verify procedures/verify_membership on pg + +BEGIN; + +SELECT verify_function ('public.verify_membership'); + +ROLLBACK; diff --git a/extensions/@pgpm/verify/verify/procedures/verify_policy.sql b/extensions/@pgpm/verify/verify/procedures/verify_policy.sql new file mode 100644 index 00000000..36dd6648 --- /dev/null +++ b/extensions/@pgpm/verify/verify/procedures/verify_policy.sql @@ -0,0 +1,7 @@ +-- Verify procedures/verify_policy on pg + +BEGIN; + +SELECT verify_function ('public.verify_policy'); + +ROLLBACK; diff --git a/extensions/@pgpm/verify/verify/procedures/verify_role.sql b/extensions/@pgpm/verify/verify/procedures/verify_role.sql new file mode 100644 index 00000000..717b1179 --- /dev/null +++ b/extensions/@pgpm/verify/verify/procedures/verify_role.sql @@ -0,0 +1,7 @@ +-- Verify procedures/verify_role on pg + +BEGIN; + +SELECT verify_function ('public.verify_role'); + +ROLLBACK; diff --git a/extensions/@pgpm/verify/verify/procedures/verify_schema.sql b/extensions/@pgpm/verify/verify/procedures/verify_schema.sql new file mode 100644 index 00000000..6420dcbb --- /dev/null +++ b/extensions/@pgpm/verify/verify/procedures/verify_schema.sql @@ -0,0 +1,7 @@ +-- Verify procedures/verify_schema on pg + +BEGIN; + +SELECT verify_function ('public.verify_schema'); + +ROLLBACK; diff --git a/extensions/@pgpm/verify/verify/procedures/verify_security.sql b/extensions/@pgpm/verify/verify/procedures/verify_security.sql new file mode 100644 index 00000000..a4e570a3 --- /dev/null +++ b/extensions/@pgpm/verify/verify/procedures/verify_security.sql @@ -0,0 +1,7 @@ +-- Verify procedures/verify_security on pg + +BEGIN; + +SELECT verify_function ('public.verify_security'); + +ROLLBACK; diff --git a/extensions/@pgpm/verify/verify/procedures/verify_table.sql b/extensions/@pgpm/verify/verify/procedures/verify_table.sql new file mode 100644 index 00000000..5a198e3d --- /dev/null +++ b/extensions/@pgpm/verify/verify/procedures/verify_table.sql @@ -0,0 +1,7 @@ +-- Verify procedures/verify_table on pg + +BEGIN; + +SELECT verify_function ('public.verify_table'); + +ROLLBACK; diff --git a/extensions/@pgpm/verify/verify/procedures/verify_table_grant.sql b/extensions/@pgpm/verify/verify/procedures/verify_table_grant.sql new file mode 100644 index 00000000..10efe5a2 --- /dev/null +++ b/extensions/@pgpm/verify/verify/procedures/verify_table_grant.sql @@ -0,0 +1,7 @@ +-- Verify procedures/verify_table_grant on pg + +BEGIN; + +SELECT verify_function ('public.verify_table_grant'); + +ROLLBACK; diff --git a/extensions/@pgpm/verify/verify/procedures/verify_trigger.sql b/extensions/@pgpm/verify/verify/procedures/verify_trigger.sql new file mode 100644 index 00000000..6234bb8d --- /dev/null +++ b/extensions/@pgpm/verify/verify/procedures/verify_trigger.sql @@ -0,0 +1,7 @@ +-- Verify procedures/verify_trigger on pg + +BEGIN; + +SELECT verify_function ('public.verify_trigger'); + +ROLLBACK; diff --git a/extensions/@pgpm/verify/verify/procedures/verify_type.sql b/extensions/@pgpm/verify/verify/procedures/verify_type.sql new file mode 100644 index 00000000..58eb7780 --- /dev/null +++ b/extensions/@pgpm/verify/verify/procedures/verify_type.sql @@ -0,0 +1,7 @@ +-- Verify procedures/verify_type on pg + +BEGIN; + +SELECT verify_function ('public.verify_type'); + +ROLLBACK; diff --git a/extensions/@pgpm/verify/verify/procedures/verify_view.sql b/extensions/@pgpm/verify/verify/procedures/verify_view.sql new file mode 100644 index 00000000..3d4e4431 --- /dev/null +++ b/extensions/@pgpm/verify/verify/procedures/verify_view.sql @@ -0,0 +1,7 @@ +-- Verify procedures/verify_view on pg + +BEGIN; + +SELECT verify_function ('public.verify_view'); + +ROLLBACK; diff --git a/functions/example/handler.json b/functions/example/handler.json index 68ba3675..c9c570eb 100644 --- a/functions/example/handler.json +++ b/functions/example/handler.json @@ -1,7 +1,22 @@ { - "name": "knative-job-example", - "version": "1.1.0", + "name": "node-example", + "version": "1.0.0", "type": "node-graphql", "port": 8083, - "description": "Example Knative job function" + "taskIdentifier": "node-example", + "scope": "platform", + "description": "Example Node.js function — copy this to create a new function", + "requiredSecrets": [], + "requiredConfigs": [], + "payloadSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "A message to process" + } + }, + "additionalProperties": true + }, + "dependencies": {} } diff --git a/functions/example/handler.ts b/functions/example/handler.ts index dc83dc2d..61a412f2 100644 --- a/functions/example/handler.ts +++ b/functions/example/handler.ts @@ -1,14 +1,31 @@ import type { FunctionHandler } from '@constructive-io/fn-runtime'; -const handler: FunctionHandler = async (params: any) => { - if (params.throw) { - throw new Error('THROWN_ERROR'); - } +/** + * Example Node.js function handler. + * + * Copy this directory to create a new function: + * 1. cp -r functions/example functions/my-function + * 2. Edit handler.json (name, description, secrets, configs) + * 3. Implement your logic here + * 4. Run: make register && pgpm kill && make up + * + * The handler receives: + * - params: the job payload (JSON from the caller) + * - context: { client, meta, log, env, job } + * - client/meta: GraphQL clients (tenant-scoped) + * - log: structured logger + * - env: process.env + * - job: { jobId, workerId, databaseId, actorId } + */ +const handler: FunctionHandler = async (params, context) => { + const { log } = context; + + log.info('node-example received payload', { params }); return { - fn: 'example-fn', - message: 'hi I did a lot of work', - body: params + status: 'ok', + received: params, + timestamp: new Date().toISOString() }; }; diff --git a/functions/python-example/handler.json b/functions/python-example/handler.json index 66b98410..6412d1ab 100644 --- a/functions/python-example/handler.json +++ b/functions/python-example/handler.json @@ -1,6 +1,21 @@ { "name": "python-example", - "version": "0.1.0", - "description": "Example Python function", - "type": "python" + "version": "1.0.0", + "type": "python", + "port": 8084, + "taskIdentifier": "python-example", + "scope": "platform", + "description": "Example Python function — copy this to create a new Python function", + "requiredSecrets": [], + "requiredConfigs": [], + "payloadSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "A message to process" + } + }, + "additionalProperties": true + } } diff --git a/functions/python-example/handler.py b/functions/python-example/handler.py index fb15c03b..7eb05f91 100644 --- a/functions/python-example/handler.py +++ b/functions/python-example/handler.py @@ -1,9 +1,23 @@ +""" +Example Python function handler. + +Copy this directory to create a new Python function: + 1. cp -r functions/python-example functions/my-function + 2. Edit handler.json (name, description, secrets, configs) + 3. Implement your logic here + 4. Run: make register && pgpm kill && make up + +The handler receives a dict payload from the job queue +and returns a dict result. +""" + +from datetime import datetime, timezone + + async def handler(payload: dict) -> dict: - """ - Simple echo handler - returns the received payload. - """ + """Process a job payload and return a result.""" return { - "received": payload, "status": "ok", - "message": "Hello from Python!" + "received": payload, + "timestamp": datetime.now(timezone.utc).isoformat(), } diff --git a/functions/send-email/handler.json b/functions/send-email/handler.json index 61d4576e..f9320771 100644 --- a/functions/send-email/handler.json +++ b/functions/send-email/handler.json @@ -3,8 +3,55 @@ "version": "1.6.4", "type": "node-graphql", "port": 8081, - "taskIdentifier": "email:send_email", - "description": "Knative email function that sends emails directly from job payload", + "taskIdentifier": "send-email", + "scope": "platform", + "description": "Sends transactional emails via Mailgun or SMTP", + "requiredSecrets": [ + { "name": "MAILGUN_API_KEY", "required": false }, + { "name": "MAILGUN_DOMAIN", "required": false }, + { "name": "MAILGUN_FROM", "required": false } + ], + "requiredConfigs": [ + { "name": "EMAIL_SEND_USE_SMTP", "required": false }, + { "name": "SMTP_HOST", "required": false }, + { "name": "SMTP_PORT", "required": false }, + { "name": "SMTP_FROM", "required": false }, + { "name": "SEND_EMAIL_DRY_RUN", "required": false } + ], + "payloadSchema": { + "type": "object", + "required": ["to", "subject"], + "properties": { + "to": { + "type": "string", + "format": "email", + "description": "Recipient email address" + }, + "subject": { + "type": "string", + "description": "Email subject line" + }, + "html": { + "type": "string", + "description": "HTML body content" + }, + "text": { + "type": "string", + "description": "Plain text body content" + }, + "from": { + "type": "string", + "format": "email", + "description": "Sender email address (overrides env default)" + }, + "replyTo": { + "type": "string", + "format": "email", + "description": "Reply-to email address" + } + }, + "additionalProperties": false + }, "dependencies": { "@constructive-io/postmaster": "^1.5.2", "@pgpmjs/env": "^2.15.3", diff --git a/functions/send-verification-link/handler.json b/functions/send-verification-link/handler.json index 91b65616..a87868ab 100644 --- a/functions/send-verification-link/handler.json +++ b/functions/send-verification-link/handler.json @@ -3,8 +3,71 @@ "version": "2.6.4", "type": "node-graphql", "port": 8082, - "taskIdentifier": "email:send_verification_link", + "taskIdentifier": "send-verification-link", + "scope": "platform", "description": "Sends invite, password reset, and verification emails", + "requiredSecrets": [ + { "name": "MAILGUN_API_KEY", "required": false }, + { "name": "MAILGUN_DOMAIN", "required": false }, + { "name": "MAILGUN_FROM", "required": false }, + { "name": "MAILGUN_REPLY", "required": false } + ], + "requiredConfigs": [ + { "name": "EMAIL_SEND_USE_SMTP", "required": false }, + { "name": "SMTP_HOST", "required": false }, + { "name": "SMTP_PORT", "required": false }, + { "name": "SMTP_FROM", "required": false }, + { "name": "LOCAL_APP_PORT", "required": false }, + { "name": "SEND_VERIFICATION_LINK_DRY_RUN", "required": false } + ], + "payloadSchema": { + "type": "object", + "required": ["email_type", "email"], + "properties": { + "email_type": { + "type": "string", + "enum": ["invite_email", "forgot_password", "email_verification"], + "description": "Type of verification email to send" + }, + "email": { + "type": "string", + "format": "email", + "description": "Recipient email address" + }, + "invite_type": { + "type": ["number", "string"], + "description": "Invite type identifier (for invite_email)" + }, + "invite_token": { + "type": "string", + "description": "Invitation token (required for invite_email)" + }, + "sender_id": { + "type": "string", + "format": "uuid", + "description": "User ID of the sender (required for invite_email)" + }, + "user_id": { + "type": "string", + "format": "uuid", + "description": "User ID (required for forgot_password)" + }, + "reset_token": { + "type": "string", + "description": "Password reset token (required for forgot_password)" + }, + "email_id": { + "type": "string", + "format": "uuid", + "description": "Email record ID (required for email_verification)" + }, + "verification_token": { + "type": "string", + "description": "Email verification token (required for email_verification)" + } + }, + "additionalProperties": false + }, "dependencies": { "@constructive-io/postmaster": "^1.5.2", "@launchql/mjml": "0.1.1", diff --git a/job/compute-service/README.md b/job/compute-service/README.md new file mode 100644 index 00000000..8a6ec91e --- /dev/null +++ b/job/compute-service/README.md @@ -0,0 +1,30 @@ +# @constructive-io/compute-service + +Platform-aware job service orchestrator. Mirrors `knative-job-service` but uses `ComputeWorker` for database-driven function discovery and invocation tracking. + +## What it starts + +1. HTTP callback server for job completion notifications +2. `ComputeWorker` — polls jobs and dispatches to functions discovered from the database +3. `Scheduler` — handles cron-like scheduled jobs +4. (Optional) In-process function servers from the manifest + +## Usage + +```bash +# As a standalone process +node job/compute-service/dist/run.js + +# Via the dev script (starts compute-service + all functions) +make dev-compute +``` + +## Environment variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `COMPUTE_JOBS_ENABLED` | `true` | Enable/disable the compute worker | +| `JOBS_SCHEMA` | `app_jobs` | PostgreSQL schema for the jobs table | +| `INTERNAL_JOBS_CALLBACK_PORT` | `8080` | Port for the callback HTTP server | +| `COMPUTE_CALLBACK_URL` | — | URL functions POST to on completion | +| `COMPUTE_GATEWAY_URL` | — | Fallback gateway URL for functions without `service_url` | diff --git a/job/compute-service/package.json b/job/compute-service/package.json new file mode 100644 index 00000000..598b935e --- /dev/null +++ b/job/compute-service/package.json @@ -0,0 +1,37 @@ +{ + "name": "@constructive-io/compute-service", + "version": "0.1.0", + "description": "Platform-aware compute service — discovers functions from the database and tracks invocations", + "author": "Constructive ", + "private": true, + "main": "dist/index.js", + "bin": { + "compute-service": "dist/run.js" + }, + "scripts": { + "build": "makage build", + "build:dev": "makage build --dev", + "clean": "makage clean", + "lint": "eslint . --fix", + "start": "node dist/run.js" + }, + "dependencies": { + "@constructive-io/compute-worker": "workspace:^", + "@constructive-io/job-pg": "^2.5.4", + "@constructive-io/job-scheduler": "^2.5.4", + "@constructive-io/job-utils": "^2.5.4", + "@constructive-io/knative-job-fn": "workspace:^", + "@constructive-io/knative-job-server": "workspace:^", + "@pgpmjs/env": "^2.15.3", + "@pgpmjs/logger": "^2.4.3", + "async-retry": "1.3.3", + "pg": "8.20.0" + }, + "devDependencies": { + "@types/async-retry": "^1.4.9", + "@types/node": "^22.10.4", + "@types/pg": "^8.11.0", + "makage": "^0.1.12", + "typescript": "^5.1.6" + } +} diff --git a/job/compute-service/src/index.ts b/job/compute-service/src/index.ts new file mode 100644 index 00000000..fc8b172f --- /dev/null +++ b/job/compute-service/src/index.ts @@ -0,0 +1,442 @@ +/** + * ComputeService — platform-aware job service orchestrator. + * + * Mirrors the KnativeJobsSvc pattern from job/service but swaps the + * static Worker for the platform-aware ComputeWorker which discovers + * functions from the database and tracks invocations. + * + * It starts: + * 1. (optional) In-process function servers from the manifest + * 2. An HTTP callback server for job completion + * 3. A ComputeWorker that polls jobs and dispatches to functions + * 4. A Scheduler for cron-like scheduled jobs + */ + +import ComputeWorker from '@constructive-io/compute-worker'; +import poolManager from '@constructive-io/job-pg'; +import Scheduler from '@constructive-io/job-scheduler'; +import { + getJobPgConfig, + getJobsCallbackPort, + getJobSchema, + getSchedulerHostname, + getWorkerHostname, +} from '@constructive-io/job-utils'; +import jobServerFactory from '@constructive-io/knative-job-server'; +import { parseEnvBoolean } from '@pgpmjs/env'; +import { Logger } from '@pgpmjs/logger'; +import retry from 'async-retry'; +import type { Server as HttpServer } from 'http'; +import { createRequire } from 'module'; +import { Client } from 'pg'; + +import { + loadFunctionRegistry, +} from './registry'; +import type { + ComputeServiceOptions, + ComputeServiceResult, + FunctionName, + FunctionServiceConfig, + FunctionsOptions, + StartedFunction, +} from './types'; + +const functionRegistry = loadFunctionRegistry(); + +const log = new Logger('compute-service'); +const requireFn = createRequire(__filename); + +// ─── Function loading (same pattern as job/service) ────────────────────────── + +interface FunctionRegistryEntry { + moduleName: string; + defaultPort: number; +} + +const resolveFunctionEntry = (name: FunctionName): FunctionRegistryEntry => { + const entry = functionRegistry[name]; + if (!entry) { + throw new Error(`Unknown function "${name}".`); + } + return entry; +}; + +const loadFunctionApp = (moduleName: string) => { + const knativeModuleId = requireFn.resolve('@constructive-io/knative-job-fn'); + delete requireFn.cache[knativeModuleId]; + + const moduleId = requireFn.resolve(moduleName); + delete requireFn.cache[moduleId]; + + const mod = requireFn(moduleName) as { default?: { listen: (port: number, cb?: () => void) => unknown } }; + const app = mod.default ?? mod; + + if (!app || typeof (app as { listen?: unknown }).listen !== 'function') { + throw new Error(`Function module "${moduleName}" does not export a listenable app.`); + } + + return app as { listen: (port: number, cb?: () => void) => unknown }; +}; + +const shouldEnableFunctions = (options?: FunctionsOptions): boolean => { + if (!options) return false; + if (typeof options.enabled === 'boolean') return options.enabled; + return Boolean(options.services?.length); +}; + +const normalizeFunctionServices = ( + options?: FunctionsOptions +): FunctionServiceConfig[] => { + if (!shouldEnableFunctions(options)) return []; + + if (!options?.services?.length) { + return Object.keys(functionRegistry).map((name) => ({ + name: name as FunctionName + })); + } + + return options.services; +}; + +const resolveFunctionPort = (service: FunctionServiceConfig): number => { + const entry = resolveFunctionEntry(service.name); + return service.port ?? entry.defaultPort; +}; + +const ensureUniquePorts = (services: FunctionServiceConfig[]) => { + const usedPorts = new Set(); + for (const service of services) { + const port = resolveFunctionPort(service); + if (usedPorts.has(port)) { + throw new Error(`Function port ${port} is assigned more than once.`); + } + usedPorts.add(port); + } +}; + +const startFunction = async ( + service: FunctionServiceConfig, + functionServers: Map +): Promise => { + const entry = resolveFunctionEntry(service.name); + const port = resolveFunctionPort(service); + const app = loadFunctionApp(entry.moduleName); + + await new Promise((resolve, reject) => { + const server = app.listen(port, () => { + log.info(`function:${service.name} listening on ${port}`); + resolve(); + }) as HttpServer & { on?: (event: string, cb: (err: Error) => void) => void }; + + if (server?.on) { + server.on('error', (err) => { + log.error(`function:${service.name} failed to start`, err); + reject(err); + }); + } + + functionServers.set(service.name, server); + }); + + return { name: service.name, port }; +}; + +const startFunctions = async ( + options: FunctionsOptions | undefined, + functionServers: Map +): Promise => { + const services = normalizeFunctionServices(options); + if (!services.length) return []; + + ensureUniquePorts(services); + + const started: StartedFunction[] = []; + for (const service of services) { + started.push(await startFunction(service, functionServers)); + } + + return started; +}; + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +type JobRunner = { + listen: () => void; + stop?: () => Promise | void; +}; + +const listenApp = async ( + app: { listen: (port: number, host?: string) => HttpServer }, + port: number, + host?: string +): Promise => + new Promise((resolveListen, rejectListen) => { + const server = host ? app.listen(port, host) : app.listen(port); + + const cleanup = () => { + server.off('listening', handleListen); + server.off('error', handleError); + }; + + const handleListen = () => { + cleanup(); + resolveListen(server); + }; + + const handleError = (err: Error) => { + cleanup(); + rejectListen(err); + }; + + server.once('listening', handleListen); + server.once('error', handleError); + }); + +const closeServer = async (server?: HttpServer | null): Promise => { + if (!server || !server.listening) return; + await new Promise((resolveClose, rejectClose) => { + server.close((err) => { + if (err) { + rejectClose(err); + return; + } + resolveClose(); + }); + }); +}; + +// ─── ComputeService ────────────────────────────────────────────────────────── + +export class ComputeService { + private options: ComputeServiceOptions; + private started = false; + private result: ComputeServiceResult = { + functions: [], + jobs: false + }; + private functionServers = new Map(); + private jobsHttpServer?: HttpServer; + private worker?: JobRunner; + private scheduler?: JobRunner; + private jobsPoolManager?: { close: () => Promise }; + + constructor(options: ComputeServiceOptions = {}) { + this.options = options; + } + + async start(): Promise { + if (this.started) return this.result; + this.started = true; + this.result = { + functions: [], + jobs: false + }; + + if (shouldEnableFunctions(this.options.functions)) { + log.info('starting functions'); + this.result.functions = await startFunctions( + this.options.functions, + this.functionServers + ); + } + + if (this.options.jobs?.enabled !== false) { + log.info('starting compute jobs service'); + await this.startJobs(); + this.result.jobs = true; + } + + return this.result; + } + + async stop(): Promise { + if (!this.started) return; + this.started = false; + + if (this.worker?.stop) { + await this.worker.stop(); + } + if (this.scheduler?.stop) { + await this.scheduler.stop(); + } + this.worker = undefined; + this.scheduler = undefined; + + await closeServer(this.jobsHttpServer); + this.jobsHttpServer = undefined; + + if (this.jobsPoolManager) { + await this.jobsPoolManager.close(); + this.jobsPoolManager = undefined; + } + + for (const server of this.functionServers.values()) { + await closeServer(server); + } + this.functionServers.clear(); + } + + private async startJobs(): Promise { + const pgPool = poolManager.getPool(); + const jobsApp = jobServerFactory(pgPool); + const callbackPort = getJobsCallbackPort(); + this.jobsHttpServer = await listenApp(jobsApp, callbackPort); + + // Platform-aware worker: discovers functions from the database + this.worker = new ComputeWorker({ + pgPool, + workerId: getWorkerHostname(), + }); + + this.scheduler = new Scheduler({ + pgPool, + tasks: [], // ComputeWorker accepts any task from the DB + workerId: getSchedulerHostname(), + }); + + this.jobsPoolManager = poolManager; + + this.worker.listen(); + this.scheduler.listen(); + } +} + +// ─── Env-based configuration ───────────────────────────────────────────────── + +const parseList = (value?: string): string[] => { + if (!value) return []; + return value.split(',').map((item) => item.trim()).filter(Boolean); +}; + +const parsePortMap = (value?: string): Record => { + if (!value) return {}; + const trimmed = value.trim(); + if (!trimmed) return {}; + if (trimmed.startsWith('{')) { + try { + const parsed = JSON.parse(trimmed) as Record; + return Object.entries(parsed).reduce>((acc, [key, port]) => { + const portNumber = Number(port); + if (Number.isFinite(portNumber)) { + acc[key] = portNumber; + } + return acc; + }, {}); + } catch { + return {}; + } + } + return trimmed.split(',').reduce>((acc, pair) => { + const [rawName, rawPort] = pair.split(/[:=]/).map((item) => item.trim()); + const port = Number(rawPort); + if (rawName && Number.isFinite(port)) { + acc[rawName] = port; + } + return acc; + }, {}); +}; + +const buildFunctionsOptionsFromEnv = (): ComputeServiceOptions['functions'] => { + const rawFunctions = (process.env.CONSTRUCTIVE_FUNCTIONS || '').trim(); + if (!rawFunctions) return undefined; + + const portMap = parsePortMap(process.env.CONSTRUCTIVE_FUNCTION_PORTS); + const normalized = rawFunctions.toLowerCase(); + + if (normalized === 'all' || normalized === '*') { + return { enabled: true }; + } + + const names = parseList(rawFunctions) as FunctionName[]; + if (!names.length) return undefined; + + const services: FunctionServiceConfig[] = names.map((name) => ({ + name, + port: portMap[name] + })); + + return { enabled: true, services }; +}; + +export const buildComputeServiceOptionsFromEnv = (): ComputeServiceOptions => ({ + jobs: { + enabled: parseEnvBoolean(process.env.COMPUTE_JOBS_ENABLED) ?? true + }, + functions: buildFunctionsOptionsFromEnv() +}); + +// ─── Prereqs ───────────────────────────────────────────────────────────────── + +export const waitForComputePrereqs = async (): Promise => { + log.info('waiting for compute prereqs'); + let client: Client | null = null; + try { + const cfg = getJobPgConfig(); + client = new Client({ + host: cfg.host, + port: cfg.port, + user: cfg.user, + password: cfg.password, + database: cfg.database + }); + await client.connect(); + + const schema = getJobSchema(); + await client.query(`SELECT * FROM "${schema}".jobs LIMIT 1;`); + + // Also verify the infra schema is deployed + await client.query( + 'SELECT count(*) FROM constructive_infra_public.platform_function_definitions LIMIT 1' + ); + log.info('compute prereqs satisfied (jobs table + infra schema present)'); + } catch (error) { + log.error(error); + throw new Error('compute-service boot failed — jobs table or infra schema not ready'); + } finally { + if (client) { + void client.end(); + } + } +}; + +// ─── Boot ──────────────────────────────────────────────────────────────────── + +export const bootCompute = async (): Promise => { + log.info('attempting to boot compute-service'); + await retry( + async () => { + await waitForComputePrereqs(); + }, + { + retries: 10, + factor: 2 + } + ); + + const options = buildComputeServiceOptionsFromEnv(); + + const pgConfig = getJobPgConfig(); + log.info('[compute-service] Starting with config:', { + database: pgConfig.database, + host: pgConfig.host, + port: pgConfig.port, + schema: getJobSchema(), + callbackPort: getJobsCallbackPort(), + workerHostname: getWorkerHostname(), + schedulerHostname: getSchedulerHostname(), + jobsEnabled: options.jobs?.enabled ?? true, + functionsEnabled: shouldEnableFunctions(options.functions), + functions: normalizeFunctionServices(options.functions).map(s => s.name) + }); + + if (options.jobs?.enabled === false) { + log.info('compute jobs disabled; skipping startup'); + return; + } + + const server = new ComputeService(options); + await server.start(); +}; + +export * from './types'; diff --git a/job/compute-service/src/registry.ts b/job/compute-service/src/registry.ts new file mode 100644 index 00000000..249e3734 --- /dev/null +++ b/job/compute-service/src/registry.ts @@ -0,0 +1,84 @@ +/** + * Function registry loader for the in-process function server. + * + * Sources, in priority order: + * 1. FUNCTIONS_REGISTRY env var + * Format: "name:moduleName:port,..." (port optional) + * Example: "send-email:@org/send-email-fn:8081,foo:@org/foo-fn" + * 2. FUNCTIONS_MANIFEST_PATH env var pointing to a JSON file with shape + * { functions: [{ name, dir, port, type, moduleName? }] } + * 3. Default file: /generated/functions-manifest.json + * + * If no source resolves, the registry is empty; callers throw on lookup of + * an unknown function (preserves the legacy "Unknown function X" behaviour). + */ +import * as fs from 'fs'; +import * as path from 'path'; + +export interface FunctionRegistryEntry { + moduleName: string; + defaultPort: number; +} + +export type FunctionRegistry = Record; + +const DEFAULT_MODULE_PREFIX = '@constructive-io/'; +const DEFAULT_MODULE_SUFFIX = '-fn'; + +const conventionalModuleName = (name: string): string => + `${DEFAULT_MODULE_PREFIX}${name}${DEFAULT_MODULE_SUFFIX}`; + +const parseEnvRegistry = (raw: string): FunctionRegistry => { + const out: FunctionRegistry = {}; + for (const pair of raw.split(',')) { + const trimmed = pair.trim(); + if (!trimmed) continue; + const [name, moduleName, portStr] = trimmed.split(':').map((s) => s.trim()); + if (!name) continue; + const portNumber = portStr ? Number(portStr) : NaN; + out[name] = { + moduleName: moduleName || conventionalModuleName(name), + defaultPort: Number.isFinite(portNumber) ? portNumber : 0, + }; + } + return out; +}; + +interface ManifestEntry { + name: string; + dir?: string; + port?: number; + type?: string; + moduleName?: string; +} + +const fromManifestEntry = (entry: ManifestEntry): FunctionRegistryEntry => ({ + moduleName: entry.moduleName ?? conventionalModuleName(entry.name), + defaultPort: typeof entry.port === 'number' ? entry.port : 0, +}); + +const loadManifestFile = (manifestPath: string): FunctionRegistry => { + const raw = fs.readFileSync(manifestPath, 'utf-8'); + const parsed = JSON.parse(raw) as { functions?: ManifestEntry[] }; + const out: FunctionRegistry = {}; + for (const entry of parsed.functions ?? []) { + if (!entry.name) continue; + out[entry.name] = fromManifestEntry(entry); + } + return out; +}; + +export const loadFunctionRegistry = ( + env: NodeJS.ProcessEnv = process.env, + cwd: string = process.cwd() +): FunctionRegistry => { + if (env.FUNCTIONS_REGISTRY) { + return parseEnvRegistry(env.FUNCTIONS_REGISTRY); + } + const manifestPath = + env.FUNCTIONS_MANIFEST_PATH ?? path.join(cwd, 'generated', 'functions-manifest.json'); + if (fs.existsSync(manifestPath)) { + return loadManifestFile(manifestPath); + } + return {}; +}; diff --git a/job/compute-service/src/run.ts b/job/compute-service/src/run.ts new file mode 100644 index 00000000..745aa4ba --- /dev/null +++ b/job/compute-service/src/run.ts @@ -0,0 +1,9 @@ +#!/usr/bin/env node + +export { bootCompute, waitForComputePrereqs } from './index'; + +import { bootCompute } from './index'; + +if (require.main === module) { + void bootCompute(); +} diff --git a/job/compute-service/src/types.ts b/job/compute-service/src/types.ts new file mode 100644 index 00000000..25d4dcaa --- /dev/null +++ b/job/compute-service/src/types.ts @@ -0,0 +1,30 @@ +export type FunctionName = string; + +export type FunctionServiceConfig = { + name: FunctionName; + port?: number; +}; + +export type FunctionsOptions = { + enabled?: boolean; + services?: FunctionServiceConfig[]; +}; + +export type JobsOptions = { + enabled?: boolean; +}; + +export type ComputeServiceOptions = { + functions?: FunctionsOptions; + jobs?: JobsOptions; +}; + +export type StartedFunction = { + name: FunctionName; + port: number; +}; + +export type ComputeServiceResult = { + functions: StartedFunction[]; + jobs: boolean; +}; diff --git a/job/compute-service/tsconfig.esm.json b/job/compute-service/tsconfig.esm.json new file mode 100644 index 00000000..800d7506 --- /dev/null +++ b/job/compute-service/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "dist/esm", + "module": "es2022", + "rootDir": "src/", + "declaration": false + } +} diff --git a/job/compute-service/tsconfig.json b/job/compute-service/tsconfig.json new file mode 100644 index 00000000..1a9d5696 --- /dev/null +++ b/job/compute-service/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src/" + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules", "**/*.spec.*", "**/*.test.*"] +} diff --git a/job/compute-worker/README.md b/job/compute-worker/README.md new file mode 100644 index 00000000..bb1b9e62 --- /dev/null +++ b/job/compute-worker/README.md @@ -0,0 +1,20 @@ +# @constructive-io/compute-worker + +Platform-aware job worker that discovers functions from the database (`constructive_infra_public.platform_function_definitions`) and tracks invocations in `platform_function_invocations`. + +## How it works + +1. Poll `app_jobs.jobs` for the next pending job +2. Lazy-resolve the function definition from DB (TTL-cached) +3. Create an invocation record (`status=running`) +4. HTTP POST to the function's `service_url` +5. Update invocation to `completed` or `failed` with duration + +## Key differences from knative-job-worker + +| Feature | knative-job-worker | compute-worker | +|---------|-------------------|----------------| +| Function discovery | Static manifest / env vars | Database query (cached) | +| Invocation tracking | None | `platform_function_invocations` table | +| Task filtering | `JOBS_SUPPORTED` env var | Accepts any registered task | +| URL resolution | Gateway URL + dev map | `service_url` from DB definition | diff --git a/job/compute-worker/package.json b/job/compute-worker/package.json new file mode 100644 index 00000000..1741545e --- /dev/null +++ b/job/compute-worker/package.json @@ -0,0 +1,26 @@ +{ + "name": "@constructive-io/compute-worker", + "version": "0.1.0", + "description": "Platform-aware job worker — discovers functions from constructive_infra_public and tracks invocations", + "author": "Constructive ", + "private": true, + "main": "dist/index.js", + "scripts": { + "build": "makage build", + "build:dev": "makage build --dev", + "clean": "makage clean", + "lint": "eslint . --fix" + }, + "dependencies": { + "@constructive-io/job-pg": "^2.5.4", + "@constructive-io/job-utils": "^2.5.4", + "@pgpmjs/logger": "^2.4.3", + "pg": "8.20.0" + }, + "devDependencies": { + "@types/node": "^22.10.4", + "@types/pg": "^8.11.0", + "makage": "^0.1.12", + "typescript": "^5.1.6" + } +} diff --git a/job/compute-worker/src/cache.ts b/job/compute-worker/src/cache.ts new file mode 100644 index 00000000..831ee457 --- /dev/null +++ b/job/compute-worker/src/cache.ts @@ -0,0 +1,44 @@ +/** + * Simple TTL cache — same pattern as agentic-server. + * Entries expire after the configured TTL. + */ + +interface CacheEntry { + value: T; + expires_at: number; +} + +export class TtlCache { + private store = new Map>(); + private ttl_ms: number; + + constructor(ttl_ms: number) { + this.ttl_ms = ttl_ms; + } + + get(key: string): T | undefined { + const entry = this.store.get(key); + if (!entry) return undefined; + if (Date.now() > entry.expires_at) { + this.store.delete(key); + return undefined; + } + return entry.value; + } + + set(key: string, value: T): void { + this.store.set(key, { value, expires_at: Date.now() + this.ttl_ms }); + } + + delete(key: string): void { + this.store.delete(key); + } + + clear(): void { + this.store.clear(); + } + + get size(): number { + return this.store.size; + } +} diff --git a/job/compute-worker/src/discovery.ts b/job/compute-worker/src/discovery.ts new file mode 100644 index 00000000..01d6e598 --- /dev/null +++ b/job/compute-worker/src/discovery.ts @@ -0,0 +1,101 @@ +/** + * FunctionDiscovery — lazy, cached lookups against + * constructive_infra_public.platform_function_definitions. + * + * When a job arrives the worker calls `resolve(taskIdentifier)`. + * On cache miss, a single SQL query fetches the function definition + * and caches it for `ttlMs` (default 60 s). Subsequent calls for the + * same task_identifier are served from memory. + */ + +import { Logger } from '@pgpmjs/logger'; +import type { Pool } from 'pg'; + +import { TtlCache } from './cache'; +import type { PlatformFunctionDefinition } from './types'; + +const log = new Logger('compute:discovery'); + +const RESOLVE_SQL = ` + SELECT + id, name, task_identifier, service_url, + is_invocable, is_built_in, max_attempts, + priority, queue_name, scope, namespace_id, + required_configs, required_secrets, description + FROM constructive_infra_public.platform_function_definitions + WHERE task_identifier = $1 + LIMIT 1 +`; + +const LIST_INVOCABLE_SQL = ` + SELECT + id, name, task_identifier, service_url, + is_invocable, is_built_in, max_attempts, + priority, queue_name, scope, namespace_id, + required_configs, required_secrets, description + FROM constructive_infra_public.platform_function_definitions + WHERE is_invocable = true + ORDER BY name +`; + +export class FunctionDiscovery { + private cache: TtlCache; + private pool: Pool; + + constructor(pool: Pool, ttlMs = 60_000) { + this.pool = pool; + this.cache = new TtlCache(ttlMs); + } + + /** + * Lazily resolve a function definition by task_identifier. + * Returns null if not registered. Results are TTL-cached. + */ + async resolve(taskIdentifier: string): Promise { + const cached = this.cache.get(taskIdentifier); + if (cached !== undefined) { + log.debug(`cache hit: ${taskIdentifier}`); + return cached; + } + + log.debug(`cache miss: ${taskIdentifier}, querying DB`); + try { + const { rows } = await this.pool.query(RESOLVE_SQL, [taskIdentifier]); + const def = (rows[0] as PlatformFunctionDefinition) ?? null; + this.cache.set(taskIdentifier, def); + if (def) { + log.info(`resolved function: ${def.name} (${taskIdentifier}) → ${def.service_url ?? 'no url'}`); + } else { + log.warn(`no function registered for task_identifier="${taskIdentifier}"`); + } + return def; + } catch (err: any) { + log.error(`failed to resolve "${taskIdentifier}": ${err.message}`); + return null; + } + } + + /** + * List all invocable function definitions. + * Not cached — intended for startup diagnostics / admin endpoints. + */ + async listInvocable(): Promise { + try { + const { rows } = await this.pool.query(LIST_INVOCABLE_SQL); + return rows as PlatformFunctionDefinition[]; + } catch (err: any) { + log.error(`failed to list invocable functions: ${err.message}`); + return []; + } + } + + /** Invalidate cached entry for a specific task. */ + invalidate(taskIdentifier: string): void { + this.cache.delete(taskIdentifier); + } + + /** Clear the entire cache. */ + invalidateAll(): void { + this.cache.clear(); + } +} diff --git a/job/compute-worker/src/index.ts b/job/compute-worker/src/index.ts new file mode 100644 index 00000000..b28b662a --- /dev/null +++ b/job/compute-worker/src/index.ts @@ -0,0 +1,352 @@ +/** + * ComputeWorker — Platform-aware job worker. + * + * Unlike the original knative-job-worker which looks up functions from a + * static manifest, ComputeWorker queries constructive_infra_public + * .platform_function_definitions to discover registered functions and + * tracks every invocation in platform_function_invocations. + * + * Flow: + * 1. Poll app_jobs.jobs for the next job + * 2. Lazy-resolve the function definition from DB (cached) + * 3. Create an invocation record (status=running) + * 4. HTTP POST to the function's service_url + * 5. Update invocation to completed/failed with duration + */ + +import poolManager from '@constructive-io/job-pg'; +import type { PgClientLike } from '@constructive-io/job-utils'; +import * as jobs from '@constructive-io/job-utils'; +import { Logger } from '@pgpmjs/logger'; +import type { Pool, PoolClient } from 'pg'; + +import { FunctionDiscovery } from './discovery'; +import { InvocationTracker } from './invocation'; +import { compute_request } from './req'; +import type { ComputeJobRow, ComputeWorkerOptions } from './types'; + +const DEFAULT_DATABASE_ID = '00000000-0000-0000-0000-000000000000'; + +export { TtlCache } from './cache'; +export { FunctionDiscovery } from './discovery'; +export { InvocationTracker } from './invocation'; +export type { ComputeRequestOptions } from './req'; +export { compute_request } from './req'; +export type { + ComputeJobRow, + ComputeWorkerOptions, + CreateInvocationInput, + FunctionRequirement, + InvocationStatus, + PlatformFunctionDefinition, +} from './types'; + +const log = new Logger('compute:worker'); + +export default class ComputeWorker { + idleDelay: number; + workerId: string; + pgPool: Pool; + doNextTimer?: NodeJS.Timeout; + _initialized?: boolean; + listenClient?: PoolClient; + listenRelease?: () => void; + stopped?: boolean; + + readonly discovery: FunctionDiscovery; + readonly tracker: InvocationTracker; + + private callbackUrl?: string; + private gatewayUrl?: string; + + constructor(opts: ComputeWorkerOptions) { + this.idleDelay = opts.idleDelay ?? 15_000; + this.workerId = opts.workerId ?? 'compute-worker-0'; + this.pgPool = opts.pgPool; + this.discovery = new FunctionDiscovery(this.pgPool, opts.cacheTtlMs); + this.tracker = new InvocationTracker(this.pgPool); + + this.callbackUrl = process.env.COMPUTE_CALLBACK_URL + || process.env.INTERNAL_JOBS_CALLBACK_URL; + + this.gatewayUrl = process.env.COMPUTE_GATEWAY_URL + || process.env.INTERNAL_GATEWAY_URL; + + poolManager.onClose(async () => { + await jobs.releaseJobs(this.pgPool, { workerId: this.workerId }); + }); + } + + // ─── Lifecycle ─────────────────────────────────────────────────────── + + async initialize(client: PgClientLike): Promise { + if (this._initialized) return; + + await jobs.releaseJobs(client, { workerId: this.workerId }); + this._initialized = true; + + const fns = await this.discovery.listInvocable(); + log.info(`discovered ${fns.length} invocable function(s): ${fns.map(f => f.name).join(', ') || '(none)'}`); + + await this.doNext(client); + } + + async listen(): Promise { + if (this.stopped) return; + let client: PoolClient; + let release: () => void; + try { + client = await this.pgPool.connect(); + release = () => client.release(); + } catch (err) { + log.error('Error connecting with notify listener', err); + if (err instanceof Error && err.stack) { + log.debug(err.stack); + } + if (!this.stopped) { + setTimeout(() => this.listen(), 5000); + } + return; + } + if (this.stopped) { + release(); + return; + } + this.listenClient = client; + this.listenRelease = release; + client.on('notification', () => { + if (this.doNextTimer) { + this.doNext(client); + } + }); + + const schema = process.env.JOBS_SCHEMA || 'app_jobs'; + client.query(`LISTEN "${schema}:jobs:insert"`); + client.on('error', (e: unknown) => { + if (this.stopped) { + release(); + return; + } + log.error('Error with database notify listener', e); + if (e instanceof Error && e.stack) { + log.debug(e.stack); + } + release(); + if (!this.stopped) { + this.listen(); + } + }); + log.info(`${this.workerId} connected and looking for jobs...`); + this.doNext(client); + } + + async stop(): Promise { + this.stopped = true; + if (this.doNextTimer) { + clearTimeout(this.doNextTimer); + this.doNextTimer = undefined; + } + const client = this.listenClient; + const release = this.listenRelease; + this.listenClient = undefined; + this.listenRelease = undefined; + + if (client && release) { + client.removeAllListeners('notification'); + client.removeAllListeners('error'); + try { + const schema = process.env.JOBS_SCHEMA || 'app_jobs'; + await client.query(`UNLISTEN "${schema}:jobs:insert"`); + } catch { + // Ignore listener cleanup errors during shutdown. + } + release(); + } + } + + // ─── Main loop ─────────────────────────────────────────────────────── + + async doNext(client: PgClientLike): Promise { + if (this.stopped) return; + if (!this._initialized) { + return await this.initialize(client); + } + + log.debug('checking for jobs...'); + if (this.doNextTimer) { + clearTimeout(this.doNextTimer); + this.doNextTimer = undefined; + } + + try { + const job = (await jobs.getJob(client, { + workerId: this.workerId, + supportedTaskNames: null, + })) as ComputeJobRow | undefined; + + if (!job || !job.id) { + if (!this.stopped) { + this.doNextTimer = setTimeout( + () => this.doNext(client), + this.idleDelay + ); + } + return; + } + + const start = process.hrtime(); + let err: Error | null = null; + try { + await this.doWork(job); + } catch (error) { + err = error as Error; + } + const durationRaw = process.hrtime(start); + const duration = ((durationRaw[0] * 1e9 + durationRaw[1]) / 1e6).toFixed(2); + const jobId = job.id; + + try { + if (err) { + await this.handleError(client, { err, job, duration }); + } else { + await this.handleSuccess(client, { job, duration }); + } + } catch (fatalError: unknown) { + await this.handleFatalError(client, { err, fatalError, jobId }); + } + if (!this.stopped) { + return this.doNext(client); + } + return; + } catch (err: unknown) { + if (!this.stopped) { + this.doNextTimer = setTimeout( + () => this.doNext(client), + this.idleDelay + ); + } + } + } + + // ─── Work dispatch ─────────────────────────────────────────────────── + + async doWork(job: ComputeJobRow): Promise { + const { task_identifier, payload } = job; + log.debug('starting work on job', { + id: job.id, + task: task_identifier, + databaseId: job.database_id, + }); + + const fn = await this.discovery.resolve(task_identifier); + if (!fn) { + throw new Error(`Function "${task_identifier}" is not registered in platform_function_definitions`); + } + if (!fn.is_invocable) { + throw new Error(`Function "${fn.name}" (${task_identifier}) is not invocable`); + } + + const url = this.resolveUrl(fn.service_url, task_identifier); + if (!url) { + throw new Error( + `No service URL for "${task_identifier}". Set service_url in platform_function_definitions or COMPUTE_GATEWAY_URL env var.` + ); + } + + const databaseId = job.database_id || DEFAULT_DATABASE_ID; + + const { id: invocationId } = await this.tracker.create({ + function_id: fn.id, + task_identifier, + payload, + job_id: job.id, + database_id: databaseId, + actor_id: job.actor_id, + }); + + const reqStart = process.hrtime(); + try { + await compute_request(url, { + body: payload, + database_id: databaseId, + actor_id: job.actor_id, + entity_id: job.entity_id, + worker_id: this.workerId, + job_id: job.id, + invocation_id: invocationId, + callback_url: this.callbackUrl, + }); + + const elapsed = process.hrtime(reqStart); + const ms = Math.round((elapsed[0] * 1e9 + elapsed[1]) / 1e6); + await this.tracker.complete(invocationId, ms); + } catch (err: any) { + const elapsed = process.hrtime(reqStart); + const ms = Math.round((elapsed[0] * 1e9 + elapsed[1]) / 1e6); + await this.tracker.fail(invocationId, ms, err.message); + throw err; + } + } + + /** + * Resolve the HTTP URL for a function. + * Priority: service_url from DB → gateway development map → gateway_url pattern + */ + private resolveUrl( + serviceUrl: string | null, + taskIdentifier: string + ): string | null { + if (serviceUrl) return serviceUrl; + + const devMap = jobs.getJobGatewayDevMap(); + if (devMap && devMap[taskIdentifier]) { + return devMap[taskIdentifier]; + } + + if (this.gatewayUrl) { + return `${this.gatewayUrl.replace(/\/$/, '')}/${taskIdentifier}`; + } + return null; + } + + // ─── Result handlers ───────────────────────────────────────────────── + + async handleFatalError( + _client: PgClientLike, + { err, fatalError, jobId }: { err: Error | null; fatalError: unknown; jobId: ComputeJobRow['id'] } + ): Promise { + const when = err ? `after failure '${err.message}'` : 'after success'; + log.error(`Failed to release job '${jobId}' ${when}; committing seppuku`); + await poolManager.close(); + log.error(String(fatalError)); + process.exit(1); + } + + async handleError( + client: PgClientLike, + { err, job, duration }: { err: Error; job: ComputeJobRow; duration: string } + ): Promise { + log.error( + `Failed task ${job.id} (${job.task_identifier}) with error ${err.message} (${duration}ms)` + ); + if (err.stack) { + log.debug(err.stack); + } + await jobs.failJob(client, { + workerId: this.workerId, + jobId: job.id, + message: err.message, + }); + } + + async handleSuccess( + _client: PgClientLike, + { job, duration }: { job: ComputeJobRow; duration: string } + ): Promise { + log.info( + `Completed task ${job.id} (${job.task_identifier}) in ${duration}ms` + ); + } +} + +export { ComputeWorker }; diff --git a/job/compute-worker/src/invocation.ts b/job/compute-worker/src/invocation.ts new file mode 100644 index 00000000..6650f7c6 --- /dev/null +++ b/job/compute-worker/src/invocation.ts @@ -0,0 +1,101 @@ +/** + * InvocationTracker — records function invocations in + * constructive_infra_public.platform_function_invocations. + * + * Lifecycle: + * 1. `create()` — inserts a row with status='running' before dispatch + * 2. `complete()` — updates to status='completed' with result + duration + * 3. `fail()` — updates to status='failed' with error + duration + */ + +import { Logger } from '@pgpmjs/logger'; +import type { Pool } from 'pg'; + +import type { CreateInvocationInput } from './types'; + +const log = new Logger('compute:invocation'); + +const CREATE_SQL = ` + INSERT INTO constructive_infra_public.platform_function_invocations + (id, function_id, task_identifier, payload, job_id, database_id, actor_id, status, started_at) + VALUES + (gen_random_uuid(), $1, $2, $3::jsonb, $4, $5, $6, 'running', now()) + RETURNING id, started_at +`; + +const COMPLETE_SQL = ` + UPDATE constructive_infra_public.platform_function_invocations + SET + status = 'completed', + completed_at = now(), + duration_ms = $2, + result = $3::jsonb + WHERE id = $1 +`; + +const FAIL_SQL = ` + UPDATE constructive_infra_public.platform_function_invocations + SET + status = 'failed', + completed_at = now(), + duration_ms = $2, + error = $3 + WHERE id = $1 +`; + +export class InvocationTracker { + private pool: Pool; + + constructor(pool: Pool) { + this.pool = pool; + } + + /** + * Create an invocation record before dispatching the function. + * Returns the invocation ID and start timestamp. + */ + async create(input: CreateInvocationInput): Promise<{ id: string; started_at: Date }> { + const payload_json = input.payload != null ? JSON.stringify(input.payload) : null; + try { + const { rows } = await this.pool.query(CREATE_SQL, [ + input.function_id, + input.task_identifier, + payload_json, + String(input.job_id), + input.database_id ?? null, + input.actor_id ?? null, + ]); + const row = rows[0]; + log.debug(`created invocation ${row.id} for ${input.task_identifier}`); + return { id: row.id, started_at: row.started_at }; + } catch (err: any) { + log.error(`failed to create invocation for ${input.task_identifier}: ${err.message}`); + throw err; + } + } + + /** + * Mark an invocation as completed with result and duration. + */ + async complete(invocation_id: string, duration_ms: number, result?: unknown): Promise { + const result_json = result != null ? JSON.stringify(result) : null; + try { + await this.pool.query(COMPLETE_SQL, [invocation_id, duration_ms, result_json]); + log.debug(`completed invocation ${invocation_id} (${duration_ms}ms)`); + } catch (err: any) { + log.error(`failed to complete invocation ${invocation_id}: ${err.message}`); + } + } + + /** + * Mark an invocation as failed with error and duration. + */ + async fail(invocation_id: string, duration_ms: number, error: string): Promise { + try { + await this.pool.query(FAIL_SQL, [invocation_id, duration_ms, error]); + log.debug(`failed invocation ${invocation_id}: ${error}`); + } catch (err: any) { + log.error(`failed to record failure for invocation ${invocation_id}: ${err.message}`); + } + } +} diff --git a/job/compute-worker/src/req.ts b/job/compute-worker/src/req.ts new file mode 100644 index 00000000..70c3ac98 --- /dev/null +++ b/job/compute-worker/src/req.ts @@ -0,0 +1,96 @@ +/** + * Platform-aware HTTP dispatch. + * + * Instead of resolving URLs from a static manifest or gateway env var, + * the compute worker reads `service_url` from the function definition + * stored in constructive_infra_public.platform_function_definitions. + * + * Falls back to a gateway URL pattern when service_url is not set. + */ + +import http from 'node:http'; +import https from 'node:https'; +import { URL } from 'node:url'; + +import { Logger } from '@pgpmjs/logger'; + +const log = new Logger('compute:req'); + +export interface ComputeRequestOptions { + body: unknown; + database_id?: string; + actor_id?: string; + entity_id?: string; + worker_id: string; + job_id: string | number; + invocation_id: string; + callback_url?: string; +} + +/** + * Dispatch a job to a function's HTTP endpoint. + * + * @param url - The function's service URL (from platform_function_definitions.service_url + * or derived from a gateway pattern) + * @param opts - Request metadata including payload, headers, and tracking IDs + */ +export function compute_request( + url: string, + opts: ComputeRequestOptions +): Promise { + log.info('dispatching job', { + url, + worker_id: opts.worker_id, + job_id: opts.job_id, + invocation_id: opts.invocation_id, + database_id: opts.database_id, + }); + + return new Promise((resolve, reject) => { + let parsed: URL; + try { + parsed = new URL(url); + } catch (e) { + return reject(e); + } + + const is_https = parsed.protocol === 'https:'; + const client = is_https ? https : http; + const payload = JSON.stringify(opts.body); + + const req = client.request( + { + hostname: parsed.hostname, + port: parsed.port || (is_https ? 443 : 80), + path: parsed.pathname + parsed.search, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(payload), + 'X-Worker-Id': opts.worker_id, + 'X-Job-Id': String(opts.job_id), + 'X-Invocation-Id': opts.invocation_id, + ...(opts.database_id ? { 'X-Database-Id': opts.database_id } : {}), + ...(opts.actor_id ? { 'X-Actor-Id': opts.actor_id } : {}), + ...(opts.entity_id ? { 'X-Entity-Id': opts.entity_id } : {}), + ...(opts.callback_url ? { 'X-Callback-Url': opts.callback_url } : {}), + }, + }, + (res) => { + res.on('data', () => {}); + res.on('end', () => { + log.debug(`request completed for job[${opts.job_id}]`); + resolve(true); + }); + } + ); + + req.on('error', (error) => { + log.error(`request error for job[${opts.job_id}]`, error); + reject(error); + }); + + req.write(payload); + req.end(); + }); +} diff --git a/job/compute-worker/src/types.ts b/job/compute-worker/src/types.ts new file mode 100644 index 00000000..0a6a9918 --- /dev/null +++ b/job/compute-worker/src/types.ts @@ -0,0 +1,71 @@ +import type { Pool } from 'pg'; + +// ─── Platform Function Definition ──────────────────────────────────────────── + +export interface FunctionRequirement { + name: string; + required: boolean; +} + +export interface PlatformFunctionDefinition { + id: string; + name: string; + task_identifier: string; + service_url: string | null; + is_invocable: boolean; + is_built_in: boolean; + max_attempts: number | null; + priority: number | null; + queue_name: string | null; + scope: string | null; + namespace_id: string | null; + required_configs: FunctionRequirement[] | null; + required_secrets: FunctionRequirement[] | null; + description: string | null; +} + +// ─── Invocation Record ─────────────────────────────────────────────────────── + +export type InvocationStatus = 'running' | 'completed' | 'failed'; + +export interface CreateInvocationInput { + function_id: string; + task_identifier: string; + payload: unknown; + job_id: string | number; + database_id?: string; + actor_id?: string; +} + +export interface InvocationRecord { + id: string; + function_id: string; + task_identifier: string; + status: InvocationStatus; + started_at: string; + completed_at: string | null; + duration_ms: number | null; + payload: unknown; + result: unknown; + error: string | null; +} + +// ─── Job Row ───────────────────────────────────────────────────────────────── + +export interface ComputeJobRow { + id: number | string; + task_identifier: string; + payload?: unknown; + database_id?: string; + actor_id?: string; + entity_id?: string; +} + +// ─── Worker Options ────────────────────────────────────────────────────────── + +export interface ComputeWorkerOptions { + pgPool: Pool; + idleDelay?: number; + workerId?: string; + cacheTtlMs?: number; +} diff --git a/job/compute-worker/tsconfig.esm.json b/job/compute-worker/tsconfig.esm.json new file mode 100644 index 00000000..800d7506 --- /dev/null +++ b/job/compute-worker/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "dist/esm", + "module": "es2022", + "rootDir": "src/", + "declaration": false + } +} diff --git a/job/compute-worker/tsconfig.json b/job/compute-worker/tsconfig.json new file mode 100644 index 00000000..1a9d5696 --- /dev/null +++ b/job/compute-worker/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src/" + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules", "**/*.spec.*", "**/*.test.*"] +} diff --git a/k8s/overlays/local-simple/compute-service.yaml b/k8s/overlays/local-simple/compute-service.yaml new file mode 100644 index 00000000..81f1bf1e --- /dev/null +++ b/k8s/overlays/local-simple/compute-service.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: compute-service + labels: + app: compute-service +spec: + replicas: 1 + selector: + matchLabels: + app: compute-service + template: + metadata: + labels: + app: compute-service + spec: + containers: + - name: compute-service + image: constructive-functions:local + command: ["node"] + args: ["job/compute-service/dist/run.js"] + envFrom: + - configMapRef: + name: constructive + - configMapRef: + name: functions-registry + - secretRef: + name: pg-credentials + env: + - name: NODE_ENV + value: "development" + - name: JOBS_SCHEMA + value: "app_jobs" + - name: COMPUTE_JOBS_ENABLED + value: "true" + - name: JOBS_SUPPORT_ANY + value: "true" + - name: INTERNAL_JOBS_CALLBACK_PORT + value: "8080" + - name: INTERNAL_JOBS_CALLBACK_URL + value: "http://compute-service.constructive-functions.svc.cluster.local:8080/callback" + - name: COMPUTE_CALLBACK_URL + value: "http://compute-service.constructive-functions.svc.cluster.local:8080/callback" + - name: JOBS_CALLBACK_HOST + value: "compute-service.constructive-functions.svc.cluster.local" + - name: HOSTNAME + valueFrom: + fieldRef: + fieldPath: metadata.name + ports: + - containerPort: 8080 + name: jobs-http + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" +--- +apiVersion: v1 +kind: Service +metadata: + name: compute-service + labels: + app: compute-service +spec: + type: ClusterIP + selector: + app: compute-service + ports: + - name: jobs-http + port: 8080 + targetPort: jobs-http diff --git a/packages/fbp-evaluator/package.json b/packages/fbp-evaluator/package.json new file mode 100644 index 00000000..40edae82 --- /dev/null +++ b/packages/fbp-evaluator/package.json @@ -0,0 +1,13 @@ +{ + "name": "@fbp/evaluator", + "version": "1.1.0", + "private": true, + "main": "src/index.ts", + "types": "src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "@fbp/types": "workspace:*" + } +} diff --git a/packages/fbp-evaluator/src/definitions/core.ts b/packages/fbp-evaluator/src/definitions/core.ts new file mode 100644 index 00000000..852d1894 --- /dev/null +++ b/packages/fbp-evaluator/src/definitions/core.ts @@ -0,0 +1,229 @@ +import type { NodeDefinitionWithImpl } from '../types'; + +/** + * Core node definitions with implementations. + * These nodes handle JSON manipulation, flow control, and string operations. + */ + +/** + * core/json/select - Extract a value from JSON by path + * + * Props: + * - path: string (dot-path like "a.b.c" or "data.user.email") + * + * Inputs: + * - obj: json (the object to extract from) + * + * Outputs: + * - value: any (the extracted value) + */ +export const jsonSelectDef: NodeDefinitionWithImpl = { + context: 'js', + name: 'select', + category: 'json', + icon: 'circle', + inputs: [ + { name: 'obj', type: 'json' } + ], + outputs: [ + { name: 'value', type: 'any' } + ], + props: [ + { name: 'path', type: 'string', default: '' } + ], + description: 'Extract a value from JSON by dot-path', + impl: (inputs, props) => { + const { obj } = inputs; + const { path } = props; + + if (!path || obj === undefined || obj === null) { + return { value: undefined }; + } + + const parts = path.split('.'); + let current = obj; + + for (const part of parts) { + if (current === undefined || current === null) { + return { value: undefined }; + } + // Handle array index access like "items.0.name" + const index = parseInt(part, 10); + if (!isNaN(index) && Array.isArray(current)) { + current = current[index]; + } else if (typeof current === 'object') { + current = current[part]; + } else { + return { value: undefined }; + } + } + + return { value: current }; + } +}; + +/** + * core/json/object - Build a JSON object from named inputs + * + * This node has dynamic inputs - any input wired to it becomes a key in the output object. + * The implementation receives all inputs as a Record. + * + * Inputs: + * - (dynamic) arbitrary named inputs + * + * Outputs: + * - value: json (the constructed object) + */ +export const jsonObjectDef: NodeDefinitionWithImpl = { + context: 'js', + name: 'object', + category: 'json', + icon: 'braces', + inputs: [], // Dynamic inputs - any input name is valid + outputs: [ + { name: 'value', type: 'json' } + ], + props: [], + description: 'Build a JSON object from named inputs', + impl: (inputs) => { + // All inputs become keys in the output object + return { value: { ...inputs } }; + } +}; + +/** + * core/flow/guard - Stop the flow if a condition fails + * + * Inputs: + * - ok: boolean (the condition to check) + * - error?: json (optional error info) + * + * Outputs: + * - pass: signal (emitted if ok is true) + * - fail: signal (emitted if ok is false) + * - error: json (the error if failed) + */ +export const flowGuardDef: NodeDefinitionWithImpl = { + context: 'js', + name: 'guard', + category: 'flow', + icon: 'zap', + inputs: [ + { name: 'ok', type: 'boolean' }, + { name: 'error', type: 'json' } + ], + outputs: [ + { name: 'pass', type: 'signal' }, + { name: 'fail', type: 'signal' }, + { name: 'error', type: 'json' } + ], + props: [], + description: 'Stop the flow if a condition fails', + impl: (inputs) => { + const { ok, error } = inputs; + + if (ok) { + return { + pass: true, + fail: false, + error: null + }; + } else { + return { + pass: false, + fail: true, + error: error || { message: 'Guard condition failed' } + }; + } + } +}; + +/** + * core/string/template - Build a string from a template with placeholders + * + * Props: + * - template: string (template with {{placeholder}} syntax) + * + * Inputs: + * - (dynamic) values to substitute into the template + * + * Outputs: + * - value: string (the resulting string) + */ +export const stringTemplateDef: NodeDefinitionWithImpl = { + context: 'js', + name: 'template', + category: 'string', + icon: 'quote', + inputs: [], // Dynamic inputs based on template placeholders + outputs: [ + { name: 'value', type: 'string' } + ], + props: [ + { name: 'template', type: 'string', default: '' } + ], + description: 'Build a string from a template with {{placeholder}} syntax', + impl: (inputs, props) => { + const { template } = props; + + if (!template) { + return { value: '' }; + } + + // Replace {{placeholder}} with input values + const result = template.replace(/\{\{(\w+)\}\}/g, (match: string, key: string) => { + const value = inputs[key]; + if (value === undefined || value === null) { + return match; // Keep placeholder if no value + } + return String(value); + }); + + return { value: result }; + } +}; + +/** + * core/string/concat - Concatenate strings with an optional separator + * + * Props: + * - prefix: string (optional prefix) + * - suffix: string (optional suffix) + * + * Inputs: + * - value: string (the main value) + * + * Outputs: + * - value: string (the resulting string) + */ +export const stringConcatDef: NodeDefinitionWithImpl = { + context: 'js', + name: 'concat', + category: 'string', + icon: 'link', + inputs: [ + { name: 'value', type: 'string' } + ], + outputs: [ + { name: 'value', type: 'string' } + ], + props: [ + { name: 'prefix', type: 'string', default: '' }, + { name: 'suffix', type: 'string', default: '' } + ], + description: 'Concatenate strings with optional prefix/suffix', + impl: (inputs, props) => { + const { value = '' } = inputs; + const { prefix = '', suffix = '' } = props; + + return { value: `${prefix}${value}${suffix}` }; + } +}; + +export const coreDefinitions: NodeDefinitionWithImpl[] = [ + jsonSelectDef, + jsonObjectDef, + flowGuardDef, + stringTemplateDef, + stringConcatDef +]; diff --git a/packages/fbp-evaluator/src/definitions/math.ts b/packages/fbp-evaluator/src/definitions/math.ts new file mode 100644 index 00000000..a97dc614 --- /dev/null +++ b/packages/fbp-evaluator/src/definitions/math.ts @@ -0,0 +1,56 @@ +import type { NodeDefinitionWithImpl } from '../types'; + +/** + * Math node definitions with implementations for testing. + */ + +export const constNumberDef: NodeDefinitionWithImpl = { + context: 'js', + name: 'number', + category: 'const', + icon: 'hash', + outputs: [{ name: 'value', type: 'number' }], + props: [{ name: 'value', type: 'number', default: 0 }], + description: 'Outputs a constant number value', + impl: (_inputs, props) => ({ + value: props.value ?? 0 + }) +}; + +export const addDef: NodeDefinitionWithImpl = { + context: 'js', + name: 'add', + category: 'math', + icon: 'plus', + inputs: [ + { name: 'a', type: 'number' }, + { name: 'b', type: 'number' } + ], + outputs: [{ name: 'sum', type: 'number' }], + description: 'Adds two numbers', + impl: (inputs) => ({ + sum: (inputs.a ?? 0) + (inputs.b ?? 0) + }) +}; + +export const multiplyDef: NodeDefinitionWithImpl = { + context: 'js', + name: 'multiply', + category: 'math', + icon: 'x', + inputs: [ + { name: 'a', type: 'number' }, + { name: 'b', type: 'number' } + ], + outputs: [{ name: 'product', type: 'number' }], + description: 'Multiplies two numbers', + impl: (inputs) => ({ + product: (inputs.a ?? 0) * (inputs.b ?? 0) + }) +}; + +export const mathDefinitions: NodeDefinitionWithImpl[] = [ + constNumberDef, + addDef, + multiplyDef +]; diff --git a/packages/fbp-evaluator/src/definitions/net.ts b/packages/fbp-evaluator/src/definitions/net.ts new file mode 100644 index 00000000..ea591f17 --- /dev/null +++ b/packages/fbp-evaluator/src/definitions/net.ts @@ -0,0 +1,143 @@ +import type { NodeDefinitionWithImpl } from '../types'; + +/** + * Network node definitions with implementations. + * These nodes handle GraphQL requests and related operations. + */ + +/** + * net/graphql/request - Execute GraphQL query or mutation + * + * Props: + * - document: string (GraphQL query/mutation document) + * - operationName?: string (optional operation name) + * - endpoint: string (GraphQL endpoint URL) + * - timeout?: number (request timeout in ms, default 30000) + * + * Inputs: + * - variables: json (GraphQL variables) + * - headers?: json (additional headers) + * + * Outputs: + * - data: json (response data) + * - error?: json (error if any) + * - ok: boolean (true if request succeeded without errors) + */ +export const graphqlRequestDef: NodeDefinitionWithImpl = { + context: 'js', + name: 'request', + category: 'graphql', + icon: 'graphql', + inputs: [ + { name: 'variables', type: 'json' }, + { name: 'headers', type: 'json' } + ], + outputs: [ + { name: 'data', type: 'json' }, + { name: 'error', type: 'json' }, + { name: 'ok', type: 'boolean' } + ], + props: [ + { name: 'document', type: 'string', default: '' }, + { name: 'operationName', type: 'string' }, + { name: 'endpoint', type: 'string', default: '' }, + { name: 'timeout', type: 'number', default: 30000 } + ], + description: 'Execute a GraphQL query or mutation', + impl: async (inputs, props) => { + const { variables = {}, headers = {} } = inputs; + const { document, operationName, endpoint, timeout = 30000 } = props; + + if (!endpoint) { + return { + data: null, + error: { message: 'No endpoint specified' }, + ok: false + }; + } + + if (!document) { + return { + data: null, + error: { message: 'No document specified' }, + ok: false + }; + } + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...headers + }, + body: JSON.stringify({ + query: document, + variables, + operationName: operationName || undefined + }), + signal: controller.signal + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + return { + data: null, + error: { + message: `HTTP error: ${response.status} ${response.statusText}`, + statusCode: response.status + }, + ok: false + }; + } + + const json = await response.json(); + + if (json.errors && json.errors.length > 0) { + return { + data: json.data || null, + error: json.errors, + ok: false + }; + } + + return { + data: json.data || null, + error: null, + ok: true + }; + } catch (error) { + clearTimeout(timeoutId); + + if (error instanceof Error) { + if (error.name === 'AbortError') { + return { + data: null, + error: { message: `Request timed out after ${timeout}ms` }, + ok: false + }; + } + return { + data: null, + error: { message: error.message }, + ok: false + }; + } + + return { + data: null, + error: { message: String(error) }, + ok: false + }; + } + } +}; + +export const netDefinitions: NodeDefinitionWithImpl[] = [ + graphqlRequestDef +]; diff --git a/packages/fbp-evaluator/src/definitions/ui.ts b/packages/fbp-evaluator/src/definitions/ui.ts new file mode 100644 index 00000000..5b32c7dd --- /dev/null +++ b/packages/fbp-evaluator/src/definitions/ui.ts @@ -0,0 +1,202 @@ +import type { NodeDefinitionWithImpl } from '../types'; + +/** + * UI component node definitions with implementations for testing. + * These produce vdom JSON structures. + */ + +// Type coercion helper for boundary nodes +function coerceValue(value: any, valueType: string): any { + if (value === undefined || value === null) return value; + switch (valueType) { + case 'number': + return typeof value === 'number' ? value : parseFloat(String(value)) || 0; + case 'string': + return String(value); + case 'boolean': + return value === true || value === 'true' || value === 1; + case 'Element': + case 'any': + default: + return value; + } +} + +// Boundary node definitions for graph inputs/outputs +export const graphInputDef: NodeDefinitionWithImpl = { + context: 'core', + name: 'graphInput', + category: 'graph', + icon: 'arrow-right', + inputs: [], + outputs: [{ name: 'value', type: 'any' }], + props: [ + { name: 'valueType', type: 'enum', default: 'any', options: ['any', 'number', 'string', 'boolean', 'Element'] }, + { name: 'default', type: 'any' } + ], + description: 'Graph input boundary node', + impl: (_inputs, props) => ({ value: coerceValue(props?.value ?? props?.default, props?.valueType ?? 'any') }) +}; + +export const graphOutputDef: NodeDefinitionWithImpl = { + context: 'core', + name: 'graphOutput', + category: 'graph', + icon: 'arrow-left', + inputs: [{ name: 'value', type: 'any' }], + outputs: [], + props: [ + { name: 'valueType', type: 'enum', default: 'any', options: ['any', 'number', 'string', 'boolean', 'Element'] } + ], + description: 'Graph output boundary node', + impl: (inputs, props) => ({ value: coerceValue(inputs.value, props?.valueType ?? 'any') }) +}; + +export const graphPropDef: NodeDefinitionWithImpl = { + context: 'core', + name: 'graphProp', + category: 'graph', + icon: 'settings', + inputs: [], + outputs: [{ name: 'value', type: 'any' }], + props: [ + { name: 'valueType', type: 'enum', default: 'any', options: ['any', 'number', 'string', 'boolean', 'Element'] }, + { name: 'default', type: 'any' } + ], + description: 'Graph property boundary node', + impl: (_inputs, props) => ({ value: coerceValue(props?.value ?? props?.default, props?.valueType ?? 'any') }) +}; + +export const pageDef: NodeDefinitionWithImpl = { + context: 'js', + name: 'Page', + category: 'layout', + icon: 'file', + inputs: [ + { name: 'children', type: 'Element[]', multi: true } + ], + outputs: [{ name: 'element', type: 'Element' }], + props: [ + { name: 'className', type: 'string', default: '' }, + { name: 'key', type: 'string', required: true } + ], + description: 'A page container component', + impl: (inputs, props) => ({ + element: { + type: 'Page', + key: props.key, + props: { className: props.className }, + children: inputs.children ?? [] + } + }) +}; + +export const formDef: NodeDefinitionWithImpl = { + context: 'js', + name: 'Form', + category: 'form', + icon: 'file-text', + inputs: [ + { name: 'children', type: 'Element[]', multi: true } + ], + outputs: [{ name: 'element', type: 'Element' }], + props: [ + { name: 'className', type: 'string', default: '' }, + { name: 'key', type: 'string', required: true } + ], + description: 'A form container component', + impl: (inputs, props) => ({ + element: { + type: 'Form', + key: props.key, + props: { className: props.className }, + children: inputs.children ?? [] + } + }) +}; + +export const inputDef: NodeDefinitionWithImpl = { + context: 'js', + name: 'Input', + category: 'form', + icon: 'text-cursor', + inputs: [], + outputs: [{ name: 'element', type: 'Element' }], + props: [ + { name: 'key', type: 'string', required: true }, + { name: 'name', type: 'string', required: true }, + { name: 'type', type: 'string', default: 'text' }, + { name: 'placeholder', type: 'string', default: '' } + ], + description: 'An input field component', + impl: (_inputs, props) => ({ + element: { + type: 'Input', + key: props.key, + props: { + name: props.name, + type: props.type ?? 'text', + placeholder: props.placeholder ?? '' + } + } + }) +}; + +export const buttonDef: NodeDefinitionWithImpl = { + context: 'js', + name: 'Button', + category: 'form', + icon: 'square', + inputs: [], + outputs: [{ name: 'element', type: 'Element' }], + props: [ + { name: 'key', type: 'string', required: true }, + { name: 'type', type: 'string', default: 'button' }, + { name: 'text', type: 'string', required: true } + ], + description: 'A button component', + impl: (_inputs, props) => ({ + element: { + type: 'Button', + key: props.key, + props: { + type: props.type ?? 'button', + text: props.text + } + } + }) +}; + +export const textDef: NodeDefinitionWithImpl = { + context: 'js', + name: 'Text', + category: 'content', + icon: 'type', + inputs: [], + outputs: [{ name: 'element', type: 'Element' }], + props: [ + { name: 'key', type: 'string', required: true }, + { name: 'content', type: 'string', required: true } + ], + description: 'A text content component', + impl: (_inputs, props) => ({ + element: { + type: 'Text', + key: props.key, + props: { + content: props.content + } + } + }) +}; + +export const uiDefinitions: NodeDefinitionWithImpl[] = [ + pageDef, + formDef, + inputDef, + buttonDef, + textDef, + graphInputDef, + graphOutputDef, + graphPropDef +]; diff --git a/packages/fbp-evaluator/src/evaluate.ts b/packages/fbp-evaluator/src/evaluate.ts new file mode 100644 index 00000000..b303d4f9 --- /dev/null +++ b/packages/fbp-evaluator/src/evaluate.ts @@ -0,0 +1,252 @@ +import type { Graph, Node, Edge, Port } from '@fbp/types'; +import type { NodeDefinitionWithImpl, EvaluateOptions } from './types'; + +/** + * Build a definition map key from context and name. + */ +function defKey(context: string, name: string): string { + return `${context}:${name}`; +} + +/** + * Evaluate a graph starting from the specified output node/port. + * Uses lazy evaluation - only evaluates nodes that are needed for the output. + * Fully async to support async node implementations. + * + * @param graph - The graph to evaluate + * @param options - Evaluation options including definitions, output node/port, and external inputs + * @returns Promise resolving to the value at the specified output port + */ +export async function evaluate(graph: Graph, options: EvaluateOptions): Promise { + const { definitions, outputNode, outputPort, inputs = {}, props = {} } = options; + + // Build lookup maps + const nodeMap = new Map(); + for (const node of graph.nodes) { + nodeMap.set(node.name, node); + } + + const defMap = new Map(); + for (const def of definitions) { + defMap.set(defKey(def.context, def.name), def); + } + + // Graph-level context used as default for node lookups + const graphContext = graph.context || ''; + + // Build edge lookup: destination node -> destination port -> edges (in array order) + const edgesByDst = new Map>(); + for (const edge of graph.edges) { + const dstNode = edge.dst.node; + const dstPort = edge.dst.port; + + if (!edgesByDst.has(dstNode)) { + edgesByDst.set(dstNode, new Map()); + } + const portMap = edgesByDst.get(dstNode)!; + if (!portMap.has(dstPort)) { + portMap.set(dstPort, []); + } + portMap.get(dstPort)!.push(edge); + } + + // Cache for evaluated node outputs to avoid re-evaluation + const cache = new Map>(); + + /** + * Recursively evaluate a node and return all its outputs. + * Fully async to support async node implementations. + */ + async function evaluateNode(nodeName: string): Promise> { + // Check cache first + if (cache.has(nodeName)) { + return cache.get(nodeName)!; + } + + const node = nodeMap.get(nodeName); + if (!node) { + throw new Error(`Node not found: ${nodeName}`); + } + + // Handle system nodes (graphInput, graphOutput, graphProp) + // System nodes are identified by their type name — reserved, no definition lookup needed + if (node.type === 'graphInput') { + // Get the port name from the portName property + const portNameProp = node.props?.find(p => p.name === 'portName'); + const inputName = (portNameProp?.value as string) || nodeName; + // Check external inputs first, then fall back to node's default prop + let value = inputs[inputName]; + if (value === undefined) { + const defaultProp = node.props?.find(p => p.name === 'default'); + value = defaultProp?.value; + } + const result = { value }; + cache.set(nodeName, result); + return result; + } + + if (node.type === 'graphProp') { + // Get the prop name from the propName property + const propNameProp = node.props?.find(p => p.name === 'propName'); + const propName = (propNameProp?.value as string) || nodeName; + const value = props[propName]; + const result = { value }; + cache.set(nodeName, result); + return result; + } + + if (node.type === 'graphOutput') { + // graphOutput is a pass-through: evaluate its upstream 'value' input + const portEdges = edgesByDst.get(nodeName); + const edges = portEdges?.get('value') ?? []; + let value: any = undefined; + if (edges.length > 0) { + const edge = edges[0]; + const upstreamOutputs = await evaluateNode(edge.src.node); + value = upstreamOutputs[edge.src.port]; + } + const result = { value }; + cache.set(nodeName, result); + return result; + } + + // Handle subnet nodes (type: 'subnet' with inline nodes/edges) + if (node.type === 'subnet' && node.nodes && node.edges) { + // Collect inputs for the subnet by evaluating upstream nodes + const subnetInputs: Record = {}; + const subnetPortEdges = edgesByDst.get(nodeName); + + if (node.inputs && subnetPortEdges) { + for (const inputPort of node.inputs) { + const edges = subnetPortEdges.get(inputPort.name) ?? []; + if (edges.length > 0) { + const edge = edges[0]; + const upstreamOutputs = await evaluateNode(edge.src.node); + subnetInputs[inputPort.name] = upstreamOutputs[edge.src.port]; + } + } + } + + // Find the output boundary nodes in the subnet + const subnetOutputs: Record = {}; + + if (node.outputs) { + for (const outputPort of node.outputs) { + // Find the graphOutput boundary node by matching portName property + const outputBoundaryNode = node.nodes.find(n => { + if (n.type !== 'graphOutput') return false; + const portNameProp = n.props?.find(p => p.name === 'portName'); + return (portNameProp?.value as string) === outputPort.name; + }); + + if (!outputBoundaryNode) { + throw new Error(`Output boundary node not found for port: ${outputPort.name}`); + } + + // Recursively evaluate the subnet's internal graph + const subnetResult = await evaluate( + { + name: `${nodeName}_subnet`, + context: graphContext || undefined, + nodes: node.nodes, + edges: node.edges + }, + { + definitions, + outputNode: outputBoundaryNode.name, + outputPort: 'value', + inputs: subnetInputs + } + ); + + subnetOutputs[outputPort.name] = subnetResult; + } + } + + cache.set(nodeName, subnetOutputs); + return subnetOutputs; + } + + // Get the definition for this node — resolve context from node override or graph default + const nodeContext = node.context || graphContext; + const definition = defMap.get(defKey(nodeContext, node.type)); + if (!definition) { + throw new Error(`No definition found for node: ${nodeContext}:${node.type}`); + } + + // Collect inputs by evaluating upstream nodes + const nodeInputs: Record = {}; + const portEdges = edgesByDst.get(nodeName); + + if (definition.inputs) { + for (const inputPort of definition.inputs) { + const edges = portEdges?.get(inputPort.name) ?? []; + + if (inputPort.multi) { + const values = await Promise.all(edges.map(async edge => { + const upstreamOutputs = await evaluateNode(edge.src.node); + return upstreamOutputs[edge.src.port]; + })); + nodeInputs[inputPort.name] = values; + } else { + if (edges.length > 0) { + const edge = edges[0]; + const upstreamOutputs = await evaluateNode(edge.src.node); + nodeInputs[inputPort.name] = upstreamOutputs[edge.src.port]; + } + } + } + } + + // Get props from node instance (override definition defaults) + const nodeProps: Record = {}; + if (definition.props) { + for (const propDef of definition.props) { + const instanceProp = node.props?.find(p => p.name === propDef.name); + if (instanceProp !== undefined && instanceProp.value !== undefined) { + nodeProps[propDef.name] = instanceProp.value; + } else if (propDef.default !== undefined) { + nodeProps[propDef.name] = propDef.default; + } + } + } + + // Digital asset: definition has an internal graph — evaluate it recursively + if (definition.graph) { + const assetGraph = definition.graph; + + // Find all output boundary nodes in the asset's graph + const assetOutputs: Record = {}; + const outputBoundaryNodes = assetGraph.nodes.filter(n => n.type === 'graphOutput'); + + for (const outputBoundaryNode of outputBoundaryNodes) { + const portNameProp = outputBoundaryNode.props?.find(p => p.name === 'portName'); + const portName = (portNameProp?.value as string) || outputBoundaryNode.name; + + const result = await evaluate( + { name: `${nodeName}_asset`, context: assetGraph.context, nodes: assetGraph.nodes, edges: assetGraph.edges }, + { definitions, outputNode: outputBoundaryNode.name, outputPort: 'value', inputs: nodeInputs, props: nodeProps } + ); + + assetOutputs[portName] = result; + } + + cache.set(nodeName, assetOutputs); + return assetOutputs; + } + + // Leaf node: call the implementation function + if (!definition.impl) { + throw new Error(`No implementation found for node: ${nodeContext}:${node.type}`); + } + + const outputs = await definition.impl(nodeInputs, nodeProps); + + cache.set(nodeName, outputs); + return outputs; + } + + // Evaluate starting from the output node + const outputs = await evaluateNode(outputNode); + return outputs[outputPort]; +} diff --git a/packages/fbp-evaluator/src/index.ts b/packages/fbp-evaluator/src/index.ts new file mode 100644 index 00000000..5faf65ef --- /dev/null +++ b/packages/fbp-evaluator/src/index.ts @@ -0,0 +1,8 @@ +export { evaluate } from './evaluate'; +export type { NodeImplFn, NodeDefinitionWithImpl, EvaluateOptions, PropDefinitionWithOptions } from './types'; + +// Re-export node definitions for convenience +export { mathDefinitions, constNumberDef, addDef, multiplyDef } from './definitions/math'; +export { uiDefinitions, pageDef, formDef, inputDef, buttonDef, textDef, graphInputDef, graphOutputDef, graphPropDef } from './definitions/ui'; +export { coreDefinitions, jsonSelectDef, jsonObjectDef, flowGuardDef, stringTemplateDef, stringConcatDef } from './definitions/core'; +export { netDefinitions, graphqlRequestDef } from './definitions/net'; diff --git a/packages/fbp-evaluator/src/types.ts b/packages/fbp-evaluator/src/types.ts new file mode 100644 index 00000000..a1aa4275 --- /dev/null +++ b/packages/fbp-evaluator/src/types.ts @@ -0,0 +1,45 @@ +import type { NodeDefinition, PropDefinition } from '@fbp/types'; + +/** + * A function that implements a node's computation. + * Takes inputs (keyed by port name) and props, returns outputs (keyed by port name). + * Can be sync or async - the evaluator will await the result. + */ +export type NodeImplFn = ( + inputs: Record, + props: Record +) => Record | Promise>; + +/** + * Extended PropDefinition with runtime-only options for enum/select types. + * The options field is not part of the JSON schema but is used by the UI. + */ +export interface PropDefinitionWithOptions extends PropDefinition { + options?: string[]; +} + +/** + * A NodeDefinition extended with an optional implementation function. + * The impl function is not part of the JSON schema (not serializable), + * but is used at runtime for evaluation. + */ +export interface NodeDefinitionWithImpl extends Omit { + props?: PropDefinitionWithOptions[]; + impl?: NodeImplFn; +} + +/** + * Options for the evaluate function. + */ +export interface EvaluateOptions { + /** Node definitions with implementations */ + definitions: NodeDefinitionWithImpl[]; + /** The node to get output from */ + outputNode: string; + /** The port to get output from */ + outputPort: string; + /** External inputs to provide to graphInput nodes (keyed by portName property value) */ + inputs?: Record; + /** Props to provide to graphProp nodes (keyed by propName property value) */ + props?: Record; +} diff --git a/packages/fbp-graph-editor/package.json b/packages/fbp-graph-editor/package.json new file mode 100644 index 00000000..18e61ff8 --- /dev/null +++ b/packages/fbp-graph-editor/package.json @@ -0,0 +1,25 @@ +{ + "name": "@fbp/graph-editor", + "version": "1.1.0", + "private": true, + "main": "src/index.ts", + "types": "src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "@fbp/types": "workspace:*", + "@radix-ui/react-context-menu": "^2.2.2", + "@radix-ui/react-dropdown-menu": "^2.1.2", + "@radix-ui/react-select": "^2.1.2", + "@radix-ui/react-switch": "^1.1.1", + "@radix-ui/react-tooltip": "^1.1.3", + "clsx": "^2.1.1", + "prism-react-renderer": "^2.4.1", + "react-icons": "^4.12.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } +} diff --git a/packages/fbp-graph-editor/src/components/CodeEditor.tsx b/packages/fbp-graph-editor/src/components/CodeEditor.tsx new file mode 100644 index 00000000..427e366e --- /dev/null +++ b/packages/fbp-graph-editor/src/components/CodeEditor.tsx @@ -0,0 +1,124 @@ +import React, { useCallback, useRef, useState, useEffect } from 'react'; +import { Highlight, themes } from 'prism-react-renderer'; +import { clsx } from 'clsx'; + +interface CodeEditorProps { + value: string; + onChange: (value: string) => void; + language?: string; + placeholder?: string; + className?: string; +} + +export function CodeEditor({ + value, + onChange, + language = 'graphql', + placeholder = '', + className +}: CodeEditorProps) { + const textareaRef = useRef(null); + const [isFocused, setIsFocused] = useState(false); + + // Sync scroll between textarea and highlighted code + const handleScroll = useCallback(() => { + const textarea = textareaRef.current; + const pre = textarea?.parentElement?.querySelector('pre'); + if (textarea && pre) { + pre.scrollTop = textarea.scrollTop; + pre.scrollLeft = textarea.scrollLeft; + } + }, []); + + // Auto-resize textarea to fit content + useEffect(() => { + const textarea = textareaRef.current; + if (textarea) { + textarea.style.height = 'auto'; + textarea.style.height = `${Math.max(120, textarea.scrollHeight)}px`; + } + }, [value]); + + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + // Handle Tab key for indentation + if (e.key === 'Tab') { + e.preventDefault(); + const textarea = e.currentTarget; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const newValue = value.substring(0, start) + ' ' + value.substring(end); + onChange(newValue); + // Set cursor position after the inserted spaces + setTimeout(() => { + textarea.selectionStart = textarea.selectionEnd = start + 2; + }, 0); + } + }, [value, onChange]); + + return ( +
+ + {({ className: highlightClassName, style, tokens, getLineProps, getTokenProps }) => ( +
+            {tokens.map((line, i) => (
+              
+ {line.map((token, key) => ( + + ))} +
+ ))} +
+ )} +
+