matrx-runtime 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,146 @@
1
+ """matrx-runtime — the Request Management Layer.
2
+
3
+ You hand us a request; we own its entire life and its whole nested tree —
4
+ identity, nesting, status, every failure and success, accumulated cost and any
5
+ metered limit, context propagation, cancellation, and resume — WITHOUT knowing or
6
+ caring what the work is. Payload-blind: never chat, agents, providers, tokens, or
7
+ messages. Feature data stays in the consumer's tables, reached via an opaque
8
+ `link_kind`/`link_id`.
9
+
10
+ Design: docs/runtime/REQUEST_MANAGEMENT_LAYER.md. Public API = exactly __all__.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from matrx_runtime.lifecycle import (
16
+ ALLOWED_TRANSITIONS,
17
+ InvalidExecutionTransition,
18
+ TransitionResult,
19
+ can_transition,
20
+ event_for_transition,
21
+ is_terminal,
22
+ transition,
23
+ )
24
+ from matrx_runtime.engine import (
25
+ DEFAULT_LEASE_SECONDS,
26
+ BudgetExceeded,
27
+ ConcurrentTransition,
28
+ DeadlineExceeded,
29
+ ExecutionCancelled,
30
+ ExecutionEngine,
31
+ ExecutionNotFound,
32
+ ExecutionScope,
33
+ InvalidLeaseState,
34
+ LimitExceeded,
35
+ )
36
+ from matrx_runtime.models import (
37
+ TERMINAL_STATUSES,
38
+ ContextMode,
39
+ ExecutionCheckpoint,
40
+ ExecutionContext,
41
+ ExecutionControl,
42
+ ExecutionError,
43
+ ExecutionEvent,
44
+ ExecutionEventKind,
45
+ ExecutionOutcome,
46
+ ExecutionStatus,
47
+ ExecutionTree,
48
+ ExecutionTreeNode,
49
+ GlobalExecution,
50
+ MeterEntry,
51
+ MeterEvent,
52
+ Origin,
53
+ Request,
54
+ )
55
+ from matrx_runtime.store import ExecutionStore, InMemoryExecutionStore
56
+ from matrx_runtime.orm_store import OrmExecutionStore
57
+ from matrx_runtime.origins import (
58
+ OriginSpec,
59
+ OriginSyncResult,
60
+ is_caller_allowed,
61
+ sync_origins,
62
+ )
63
+ from matrx_runtime.db import PACKAGE_DB_NAME, apply_schema, bind_to_host, bootstrap_db
64
+
65
+ __version__ = "0.0.1"
66
+
67
+
68
+ def configure(*, db_config_name: str | None = None) -> None:
69
+ """Host-injection entry point (called once at startup; mirrors matrx-rag).
70
+ `db_config_name` is the matrx-orm pool the host already registered — we alias
71
+ the package's `"matrx_runtime"` name onto it and register the `runtime.*`
72
+ Models the `OrmExecutionStore` needs. Standalone installs call `bootstrap_db()`."""
73
+ from matrx_runtime import _config
74
+
75
+ if db_config_name is not None:
76
+ _config.set_db_config_name(db_config_name)
77
+ from matrx_orm import is_database_registered
78
+
79
+ from matrx_runtime.db import bind_to_host as _bind
80
+
81
+ if is_database_registered(db_config_name):
82
+ _bind(db_config_name)
83
+ _config.mark_configured()
84
+
85
+
86
+ def is_configured() -> bool:
87
+ from matrx_runtime import _config
88
+
89
+ return _config.is_configured()
90
+
91
+
92
+ __all__ = [
93
+ # lifecycle
94
+ "transition",
95
+ "can_transition",
96
+ "is_terminal",
97
+ "event_for_transition",
98
+ "TransitionResult",
99
+ "InvalidExecutionTransition",
100
+ "ALLOWED_TRANSITIONS",
101
+ # engine
102
+ "ExecutionEngine",
103
+ "ExecutionScope",
104
+ "ExecutionNotFound",
105
+ "ConcurrentTransition",
106
+ "BudgetExceeded",
107
+ "LimitExceeded",
108
+ "DeadlineExceeded",
109
+ "ExecutionCancelled",
110
+ "InvalidLeaseState",
111
+ "DEFAULT_LEASE_SECONDS",
112
+ # store
113
+ "ExecutionStore",
114
+ "InMemoryExecutionStore",
115
+ "OrmExecutionStore",
116
+ # origin registry
117
+ "OriginSpec",
118
+ "OriginSyncResult",
119
+ "sync_origins",
120
+ "is_caller_allowed",
121
+ # host wiring / db binding
122
+ "configure",
123
+ "is_configured",
124
+ "bind_to_host",
125
+ "bootstrap_db",
126
+ "apply_schema",
127
+ "PACKAGE_DB_NAME",
128
+ # models / enums
129
+ "GlobalExecution",
130
+ "ExecutionStatus",
131
+ "ExecutionEventKind",
132
+ "ContextMode",
133
+ "ExecutionContext",
134
+ "ExecutionError",
135
+ "ExecutionOutcome",
136
+ "ExecutionControl",
137
+ "ExecutionEvent",
138
+ "ExecutionCheckpoint",
139
+ "ExecutionTree",
140
+ "ExecutionTreeNode",
141
+ "MeterEvent",
142
+ "MeterEntry",
143
+ "Origin",
144
+ "Request",
145
+ "TERMINAL_STATUSES",
146
+ ]
@@ -0,0 +1,52 @@
1
+ """Host-injection registry — the seam between matrx-runtime and its host.
2
+
3
+ The host calls :func:`matrx_runtime.configure` once at startup; the package reads
4
+ back what it needs here. Kept tiny and dependency-free (the boundary gate stays
5
+ green). Today the only seam is the matrx-orm pool name the spine persists to; the
6
+ Coordinator/context drops will add their seams here the same way.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+
13
+ _registry: dict[str, object] = {
14
+ # matrx-orm pool identifier the host registered via
15
+ # register_database_from_env. matrx_runtime.configure(db_config_name=...)
16
+ # aliases PACKAGE_DB_NAME onto it.
17
+ "db_config_name": None,
18
+ }
19
+
20
+ _configured = False
21
+
22
+
23
+ def set_db_config_name(name: str) -> None:
24
+ _registry["db_config_name"] = name
25
+
26
+
27
+ def is_configured() -> bool:
28
+ return _configured
29
+
30
+
31
+ def mark_configured() -> None:
32
+ global _configured
33
+ _configured = True
34
+
35
+
36
+ def get_db_config_name() -> str:
37
+ """Resolve the host's matrx-orm pool name.
38
+
39
+ Order: the name injected via ``configure(db_config_name=...)`` → the
40
+ ``MATRX_RUNTIME_DB_NAME`` env var (name of an already-registered config).
41
+ Raises if neither is set — running the ORM-bound store with no configured
42
+ pool is a wiring bug, surfaced loudly rather than silently no-op'd.
43
+ """
44
+ name = _registry.get("db_config_name") or os.environ.get("MATRX_RUNTIME_DB_NAME")
45
+ if not name:
46
+ raise RuntimeError(
47
+ "matrx-runtime: no database configured. Call "
48
+ "matrx_runtime.configure(db_config_name=<registered matrx-orm pool>) "
49
+ "at host startup, set MATRX_RUNTIME_DB_NAME, or use "
50
+ "matrx_runtime.bootstrap_db() for a standalone pool."
51
+ )
52
+ return str(name)
@@ -0,0 +1,217 @@
1
+ """Database binding for the execution spine.
2
+
3
+ matrx-runtime ships ONE canonical copy of its `runtime.*` model classes
4
+ (``matrx_runtime.db.models_runtime``) — generated from the live database by the
5
+ host's ``db/generate.py`` run (the ``output:`` entry in aidream's
6
+ ``db/matrx_orm.yaml``). Every model bakes ``_database = "matrx_runtime"``
7
+ (:data:`PACKAGE_DB_NAME`). What that name points at is decided exactly once:
8
+
9
+ * **Hosted** — the host registered its own pool and calls
10
+ ``matrx_runtime.configure(db_config_name=...)``, which calls
11
+ :func:`bind_to_host`: a matrx-orm *name alias* maps ``"matrx_runtime"`` onto
12
+ the host's config. One registration, one physical pool — the package NEVER
13
+ opens a second connection.
14
+ * **Standalone** — :func:`bootstrap_db` registers the pool from
15
+ ``MATRX_RUNTIME_POSTGRES_*`` env vars under :data:`PACKAGE_DB_NAME`.
16
+
17
+ This mirrors matrx-rag's binding exactly (the proven pattern). There is
18
+ deliberately NO ``SET search_path``: every model fully-qualifies its schema
19
+ (``runtime.global_execution``), so the transaction pooler resetting backend
20
+ state between transactions is a non-issue.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import os
26
+ from typing import TYPE_CHECKING, Any
27
+ from urllib.parse import quote
28
+
29
+ if TYPE_CHECKING:
30
+ from asyncpg import Pool
31
+
32
+ # The config name every matrx-runtime model class is bound to. Hosts alias it to
33
+ # their registered pool (bind_to_host); standalone registers it as a real config
34
+ # (bootstrap_db).
35
+ PACKAGE_DB_NAME = "matrx_runtime"
36
+
37
+ # Env prefix for TRUE standalone credentials. Deliberately NOT "MATRX_RUNTIME_DB":
38
+ # a MATRX_RUNTIME_DB_NAME env var would mean "name of an already-registered
39
+ # matrx-orm config", which register_database_from_env would misread as the
40
+ # database name (the same trap matrx-rag documents).
41
+ STANDALONE_ENV_PREFIX = "MATRX_RUNTIME_POSTGRES"
42
+
43
+ _models_registered = False
44
+
45
+
46
+ def connection_url() -> str:
47
+ """Resolve the postgres connection URL for ad-hoc / migration use.
48
+
49
+ Priority:
50
+ 1. ``RUNTIME_DATABASE_URL`` — explicit override.
51
+ 2. ``DATABASE_URL`` — generic shared URL.
52
+ 3. ``MATRX_RUNTIME_POSTGRES_*`` — the package's standalone credential set.
53
+ 4. ``SUPABASE_MATRIX_*`` — legacy aidream-flavored fields.
54
+
55
+ Raises ``RuntimeError`` if none resolve to a usable URL.
56
+ """
57
+ explicit = os.environ.get("RUNTIME_DATABASE_URL") or os.environ.get("DATABASE_URL")
58
+ if explicit:
59
+ return explicit
60
+
61
+ for prefix in (STANDALONE_ENV_PREFIX, "SUPABASE_MATRIX"):
62
+ host = os.environ.get(f"{prefix}_HOST")
63
+ user = os.environ.get(f"{prefix}_USER")
64
+ password = os.environ.get(f"{prefix}_PASSWORD")
65
+ if prefix == "SUPABASE_MATRIX":
66
+ database = os.environ.get("SUPABASE_MATRIX_DATABASE_NAME") or "postgres"
67
+ else:
68
+ database = os.environ.get(f"{prefix}_NAME") or "postgres"
69
+ port = os.environ.get(f"{prefix}_PORT") or "5432"
70
+ if host and user and password:
71
+ return (
72
+ f"postgresql://{quote(user, safe='')}:{quote(password, safe='')}"
73
+ f"@{host}:{port}/{database}"
74
+ )
75
+
76
+ raise RuntimeError(
77
+ "matrx-runtime: cannot build database URL — set RUNTIME_DATABASE_URL, "
78
+ f"DATABASE_URL, the {STANDALONE_ENV_PREFIX}_HOST/USER/PASSWORD trio, or "
79
+ "the SUPABASE_MATRIX_HOST/USER/PASSWORD trio."
80
+ )
81
+
82
+
83
+ def _register_models() -> None:
84
+ """Import the generated model module so its classes register.
85
+
86
+ The module ends with ``model_registry.register_all(..., skip_existing=True)``
87
+ so a host's earlier same-name registration always wins.
88
+ """
89
+ global _models_registered
90
+ if _models_registered:
91
+ return
92
+ from matrx_runtime.db import models_runtime # noqa: F401 (registers on import)
93
+
94
+ _models_registered = True
95
+
96
+
97
+ def bind_to_host(db_config_name: str) -> None:
98
+ """Hosted entry — point :data:`PACKAGE_DB_NAME` at the host's pool.
99
+
100
+ Registers a matrx-orm name alias (idempotent for the same target) and
101
+ registers the package's ``runtime.*`` model classes. Called automatically by
102
+ ``matrx_runtime.configure(db_config_name=...)``.
103
+ """
104
+ from matrx_orm import is_database_registered, register_database_alias
105
+
106
+ if not is_database_registered(PACKAGE_DB_NAME):
107
+ register_database_alias(PACKAGE_DB_NAME, db_config_name)
108
+ _register_models()
109
+
110
+
111
+ def bootstrap_db(
112
+ *,
113
+ env_prefix: str = STANDALONE_ENV_PREFIX,
114
+ additional_schemas: tuple[str, ...] = ("runtime",),
115
+ env_var_overrides: dict[str, str] | None = None,
116
+ ) -> str:
117
+ """Standalone entry — register the package's own pool from env vars.
118
+
119
+ Reads ``{env_prefix}_HOST/_PORT/_NAME/_USER/_PASSWORD`` and registers the
120
+ database under :data:`PACKAGE_DB_NAME`, then registers the model set.
121
+ Idempotent: a host that already registered/aliased the name turns this into
122
+ a no-op model-registration call. Returns :data:`PACKAGE_DB_NAME`.
123
+ """
124
+ from matrx_orm import is_database_registered, register_database_from_env
125
+
126
+ if not is_database_registered(PACKAGE_DB_NAME):
127
+ ok = register_database_from_env(
128
+ name=PACKAGE_DB_NAME,
129
+ env_prefix=env_prefix,
130
+ additional_schemas=list(additional_schemas),
131
+ env_var_overrides=env_var_overrides,
132
+ )
133
+ if not ok:
134
+ raise RuntimeError(
135
+ f"matrx-runtime standalone bootstrap failed: missing {env_prefix}_* "
136
+ "env vars. Either set them, or run hosted via "
137
+ "matrx_runtime.configure(db_config_name=<registered pool name>)."
138
+ )
139
+ _register_models()
140
+ return PACKAGE_DB_NAME
141
+
142
+
143
+ def ensure_bound() -> str:
144
+ """Resolve the pool name matrx-runtime should use, binding lazily if needed.
145
+
146
+ Order: an already-registered/aliased :data:`PACKAGE_DB_NAME` → the
147
+ host-injected ``db_config_name`` (bound on first use) → raise. The
148
+ ExecutionStore adapter calls this so the models resolve to a live pool even
149
+ if ``configure`` was passed a name but ``bind_to_host`` hasn't run yet.
150
+ """
151
+ from matrx_orm import is_database_registered
152
+
153
+ if is_database_registered(PACKAGE_DB_NAME):
154
+ _register_models()
155
+ return PACKAGE_DB_NAME
156
+
157
+ from matrx_runtime._config import get_db_config_name
158
+
159
+ name = get_db_config_name() # raises if neither injected name nor env is set
160
+ if is_database_registered(name):
161
+ bind_to_host(name)
162
+ return PACKAGE_DB_NAME
163
+ # Unregistered name: hand it to matrx-orm unchanged so its own
164
+ # "Configuration not found" error surfaces.
165
+ return name
166
+
167
+
168
+ def bootstrap_sql_files() -> list[Any]:
169
+ """The bundled `bootstrap/*.sql` files in apply order (`NNN_*.sql`)."""
170
+ from pathlib import Path
171
+
172
+ return sorted((Path(__file__).parent / "bootstrap").glob("*.sql"))
173
+
174
+
175
+ async def apply_schema(*, pool: Pool | None = None) -> list[str]:
176
+ """Create the standalone substrate on a fresh database — schemas, enums, the
177
+ actor/timestamp functions, the minimal identity shim, and the `runtime.*` tables +
178
+ indexes + triggers + RLS. Applies the bundled `bootstrap/*.sql` in order; every
179
+ statement is idempotent + NON-DESTRUCTIVE, so this is a pure no-op against a host
180
+ (aidream) that already owns the schema. Standalone flow: `bootstrap_db()` then
181
+ `await apply_schema()`. Returns the file names applied.
182
+
183
+ Design + the non-destructive invariant: docs/runtime/STANDALONE_SUBSTRATE.md.
184
+ """
185
+ from matrx_orm.operations.dynamic_admin import execute_sql_script
186
+
187
+ applied: list[str] = []
188
+ for path in bootstrap_sql_files():
189
+ # execute_sql_script rides asyncpg's simple-query protocol (no bound
190
+ # params), which is what makes a multi-statement idempotent DDL
191
+ # script runnable in one call — matrx_orm.operations.dynamic_admin
192
+ # is the ORM's sanctioned surface for this shape.
193
+ if pool is not None:
194
+ await execute_sql_script(path.read_text(), pool=pool)
195
+ else:
196
+ await execute_sql_script(path.read_text(), config_name=ensure_bound())
197
+ applied.append(path.name)
198
+ return applied
199
+
200
+
201
+ def get_models() -> dict[str, Any]:
202
+ """Return name → Model class for everything the package ships."""
203
+ _register_models()
204
+ from matrx_runtime.db import models_runtime
205
+
206
+ out: dict[str, Any] = {}
207
+ for attr in vars(models_runtime).values():
208
+ if isinstance(attr, type) and getattr(attr, "_table_name", None):
209
+ out[attr.__name__] = attr
210
+ return out
211
+
212
+
213
+ async def get_pool() -> Pool:
214
+ """Return the shared matrx-orm asyncpg pool (the host's pool when hosted)."""
215
+ from matrx_orm.core.async_db_manager import AsyncDatabaseManager
216
+
217
+ return await AsyncDatabaseManager.get_pool(ensure_bound())
@@ -0,0 +1,82 @@
1
+ -- matrx-runtime standalone substrate — 000: schemas + enums + actor/timestamp triggers.
2
+ -- Idempotent + NON-DESTRUCTIVE: every statement is a no-op where the object already
3
+ -- exists (e.g. inside aidream). Portable: no auth.uid(), no Supabase-only objects.
4
+ -- See docs/runtime/STANDALONE_SUBSTRATE.md.
5
+
6
+ create schema if not exists platform;
7
+ create schema if not exists iam;
8
+ create schema if not exists runtime;
9
+
10
+ -- Visibility tiers (platform.visibility). Create only if absent — never alter a host's.
11
+ do $do$
12
+ begin
13
+ if not exists (
14
+ select 1 from pg_type t join pg_namespace n on n.oid = t.typnamespace
15
+ where t.typname = 'visibility' and n.nspname = 'platform'
16
+ ) then
17
+ create type platform.visibility as enum ('private', 'internal', 'link', 'public');
18
+ end if;
19
+ end
20
+ $do$;
21
+
22
+ -- Permission levels (public.permission_level). Create only if absent.
23
+ do $do$
24
+ begin
25
+ if not exists (
26
+ select 1 from pg_type t join pg_namespace n on n.oid = t.typnamespace
27
+ where t.typname = 'permission_level' and n.nspname = 'public'
28
+ ) then
29
+ create type public.permission_level as enum ('viewer', 'editor', 'admin');
30
+ end if;
31
+ end
32
+ $do$;
33
+
34
+ -- updated_at / version stamping. CREATE-IF-ABSENT (never CREATE OR REPLACE): a host's
35
+ -- own version is NEVER overwritten — the only truly non-destructive rule, now that the
36
+ -- host's functions evolve independently (e.g. aidream's _stamp_actor gained an auth.uid
37
+ -- fallback). The standalone body below is the portable baseline.
38
+ do $do$
39
+ begin
40
+ if not exists (
41
+ select 1 from pg_proc p join pg_namespace n on n.oid = p.pronamespace
42
+ where p.proname = '_touch_row' and n.nspname = 'platform'
43
+ ) then
44
+ execute $exec$
45
+ create function platform._touch_row()
46
+ returns trigger language plpgsql as $fn$
47
+ begin
48
+ new.updated_at := now();
49
+ if tg_op = 'UPDATE' then new.version := old.version + 1; end if;
50
+ return new;
51
+ end
52
+ $fn$;
53
+ $exec$;
54
+ end if;
55
+ end
56
+ $do$;
57
+
58
+ -- created_by / updated_by stamping. PORTABLE baseline: resolves the actor from the
59
+ -- app.user_id GUC ONLY (no auth.uid() — so it CREATEs on vanilla Postgres). A host on
60
+ -- Supabase keeps its own richer version (e.g. COALESCE(app.user_id, auth.uid())) — this
61
+ -- is create-if-absent, so that version is never touched. The runtime engine sets
62
+ -- created_by explicitly regardless, so attribution is correct on either.
63
+ do $do$
64
+ begin
65
+ if not exists (
66
+ select 1 from pg_proc p join pg_namespace n on n.oid = p.pronamespace
67
+ where p.proname = '_stamp_actor' and n.nspname = 'platform'
68
+ ) then
69
+ execute $exec$
70
+ create function platform._stamp_actor()
71
+ returns trigger language plpgsql as $fn$
72
+ declare uid uuid := nullif(current_setting('app.user_id', true), '')::uuid;
73
+ begin
74
+ if tg_op = 'INSERT' then new.created_by := coalesce(new.created_by, uid); end if;
75
+ new.updated_by := coalesce(uid, new.updated_by);
76
+ return new;
77
+ end
78
+ $fn$;
79
+ $exec$;
80
+ end if;
81
+ end
82
+ $do$;
@@ -0,0 +1,84 @@
1
+ -- matrx-runtime standalone substrate — 010: MINIMAL portable identity.
2
+ -- The smallest thing that satisfies runtime's organization FK + registry-driven RLS.
3
+ -- Every object is create-if-absent: in aidream the real (richer) organizations +
4
+ -- iam.has_access + registries already exist and are kept UNTOUCHED. This file only
5
+ -- materializes a minimal shim on a bare DB. See docs/runtime/STANDALONE_SUBSTRATE.md.
6
+
7
+ -- The organization the governed private root is scoped to (global_request.organization_id
8
+ -- FK target). aidream's is 13 cols → auth.users; standalone gets an id-only shim.
9
+ create table if not exists public.organizations (
10
+ id uuid primary key default gen_random_uuid(),
11
+ name text,
12
+ created_at timestamptz not null default now()
13
+ );
14
+
15
+ -- The registries has_access reads (token → table, and the composition/containment edges).
16
+ create table if not exists platform.entity_types (
17
+ token text primary key,
18
+ schema_name text not null,
19
+ table_name text not null,
20
+ is_component boolean not null default false
21
+ );
22
+
23
+ create table if not exists platform.entity_relationships (
24
+ child_type text not null,
25
+ parent_type text not null,
26
+ fk_column text not null,
27
+ kind text not null,
28
+ primary key (child_type, parent_type, kind)
29
+ );
30
+
31
+ -- Portable, fail-closed access resolver. Mirrors aidream's iam.has_access but uses the
32
+ -- app.user_id GUC (not auth.uid()) and resolves only owner + composition + public — the
33
+ -- minimal safe set (grants / org-admin / containment are the host's richer layer). Create
34
+ -- only if absent: aidream's full version is NEVER overwritten.
35
+ do $do$
36
+ begin
37
+ if not exists (
38
+ select 1 from pg_proc p join pg_namespace n on n.oid = p.pronamespace
39
+ where p.proname = 'has_access' and n.nspname = 'iam'
40
+ ) then
41
+ execute $exec$
42
+ create function iam.has_access(p_type text, p_id uuid, p_required permission_level default 'viewer')
43
+ returns boolean language plpgsql stable security definer
44
+ set search_path to 'public', 'platform', 'iam'
45
+ as $fn$
46
+ declare
47
+ v_schema text; v_table text; v_is_component boolean;
48
+ v_uid uuid := nullif(current_setting('app.user_id', true), '')::uuid;
49
+ v_vis platform.visibility; v_owner uuid; v_org uuid;
50
+ v_parent_type text; v_parent_col text; v_parent_id uuid;
51
+ begin
52
+ if v_uid is null then return false; end if;
53
+
54
+ select schema_name, table_name, coalesce(is_component, false)
55
+ into v_schema, v_table, v_is_component
56
+ from platform.entity_types where token = p_type;
57
+ if v_schema is null then return false; end if;
58
+
59
+ -- component: access is exactly the parent's, full depth
60
+ if v_is_component then
61
+ select parent_type, fk_column into v_parent_type, v_parent_col
62
+ from platform.entity_relationships where child_type = p_type and kind = 'composition' limit 1;
63
+ if v_parent_type is null then return false; end if;
64
+ execute format('select %I from %I.%I where id=$1', v_parent_col, v_schema, v_table)
65
+ into v_parent_id using p_id;
66
+ if v_parent_id is null then return false; end if;
67
+ return iam.has_access(v_parent_type, v_parent_id, p_required);
68
+ end if;
69
+
70
+ -- standard entity: owner or public-read
71
+ begin
72
+ execute format('select visibility, created_by, organization_id from %I.%I where id=$1', v_schema, v_table)
73
+ into v_vis, v_owner, v_org using p_id;
74
+ exception when others then return false; end;
75
+ if v_owner is null and v_vis is null then return false; end if;
76
+ if v_owner = v_uid then return true; end if;
77
+ if v_vis = 'public' and p_required = 'viewer' then return true; end if;
78
+ return false;
79
+ end
80
+ $fn$;
81
+ $exec$;
82
+ end if;
83
+ end
84
+ $do$;