@vibeorm/runtime 2.0.0-alpha.8 → 2.0.0-alpha.9

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,131 @@
1
+ /**
2
+ * Tenant scoping — `$scoped` (framework handoff: one scoped handle per request).
3
+ *
4
+ * A CALLER-CONFIGURED row-scoping layer: the base client plus a total
5
+ * per-model tenancy map plus a scope value produce a NEW handle whose every
6
+ * query is structurally confined to that tenant. Complements `@@policy`
7
+ * (schema-declared, fail-closed) — the two do NOT combine: `$scoped` refuses
8
+ * on a client whose schema declares any `@@policy`.
9
+ *
10
+ * Implemented as PURE args rewriting over the built delegates (the views.ts /
11
+ * policy.ts pattern — ZERO query-builder edits; every emitted statement is a
12
+ * shape the catalogs already pin). The rewrite relies on three verified
13
+ * builder properties:
14
+ * 1. findUnique/connect wheres compile their FULL where object — extra
15
+ * fields and `AND` groups become filters (query-builder findUnique,
16
+ * nested-writes resolveTargetKey), so injecting the tenant predicate
17
+ * into a unique where IS the verified lookup, inside whatever
18
+ * transaction the write already runs.
19
+ * 2. A guard field beyond the conflict target forces upsert onto the
20
+ * transactional find-then-write fallback (PG-2), whose probe honors the
21
+ * full where — so an injected upsert can never fire ON CONFLICT against
22
+ * another tenant's row.
23
+ * 3. Cursors compile from caller-supplied values only (no row lookup), so
24
+ * cursor pagination needs no rewrite to stay leak-free.
25
+ *
26
+ * Deny-by-default: every argument node that touches a scoped model either
27
+ * gets the predicate injected where semantics allow, or the call REFUSES with
28
+ * a typed `ScopedQueryError { model, path, reason }` before any SQL runs.
29
+ * Raw SQL is absent from the scoped surface; the deliberate escape is the
30
+ * BASE client, which this layer never mutates.
31
+ */
32
+ import type { RelationIR } from "@vibeorm/schema";
33
+ import type { FieldMeta, RuntimeMeta } from "./model-meta.ts";
34
+ import type { ClientRow, DynamicClient } from "./client.ts";
35
+ /** Scope values a tenant column can carry (matches String / Int / BigInt columns). */
36
+ export type ScopeValue = string | number | bigint;
37
+ /**
38
+ * What happens when a scoped model appears as a CHILD inside another model's
39
+ * nested write: `"verify"` (default) rewrites reference verbs into
40
+ * tenant-verified lookups; `"refuse"` rejects every reference verb (nested
41
+ * `create` stays allowed under both — it is forced into the tenant).
42
+ */
43
+ export type ScopedNestedMode = "verify" | "refuse";
44
+ /** One model's classification in the tenancy map. */
45
+ export type ScopedModelEntry = "none" | {
46
+ readonly column: string;
47
+ readonly nested?: ScopedNestedMode;
48
+ } | {
49
+ readonly through: string;
50
+ readonly nested?: ScopedNestedMode;
51
+ };
52
+ /** The per-model tenancy map — must classify EVERY model (totality). */
53
+ export type ScopedModelsMap = Readonly<Record<string, ScopedModelEntry>>;
54
+ /** The `$scoped` argument. */
55
+ export type ScopedClientConfig = {
56
+ readonly scope: ScopeValue | null;
57
+ readonly models: ScopedModelsMap;
58
+ };
59
+ type ResolvedEntry = {
60
+ readonly kind: "none";
61
+ } | {
62
+ readonly kind: "column";
63
+ readonly field: FieldMeta;
64
+ readonly nested: ScopedNestedMode;
65
+ } | {
66
+ readonly kind: "through";
67
+ readonly relation: RelationIR;
68
+ readonly targetModel: string;
69
+ readonly nested: ScopedNestedMode;
70
+ };
71
+ type ResolvedScopedModels = ReadonlyMap<string, ResolvedEntry>;
72
+ /**
73
+ * Validate the models map against the runtime meta: totality, known
74
+ * columns/relations, scopeable column types, acyclic through-chains ending on
75
+ * a { column } model. Cached by (map, meta) object identity — the framework
76
+ * holds one map constant and calls `$scoped` per request.
77
+ */
78
+ export declare function resolveScopedModels(params: {
79
+ meta: RuntimeMeta;
80
+ models: ScopedModelsMap;
81
+ }): ResolvedScopedModels;
82
+ /** A rewritten call: which base-delegate method to run, with which args. */
83
+ export type RewrittenScopedCall = {
84
+ readonly model: string;
85
+ readonly method: string;
86
+ readonly args: ClientRow | undefined;
87
+ };
88
+ /** A validated, scope-bound rewriter — the pure core a binding's tests drive. */
89
+ export type ScopeRewriter = {
90
+ readonly rewrite: (call: {
91
+ model: string;
92
+ method: string;
93
+ args?: ClientRow;
94
+ }) => RewrittenScopedCall;
95
+ };
96
+ /**
97
+ * Validate a models map + scope against the runtime meta and return the pure
98
+ * per-call rewriter (`ScopedQueryError` on refusals). This is `$scoped`'s
99
+ * engine without the client plumbing — conformance rewrite-vectors run here.
100
+ */
101
+ export declare function createScopeRewriter(params: {
102
+ meta: RuntimeMeta;
103
+ models: ScopedModelsMap;
104
+ scope: ScopeValue | null;
105
+ }): ScopeRewriter;
106
+ /**
107
+ * Rewrite ONE delegate call for a scoped handle — pure: same inputs, same
108
+ * output; refusals throw `ScopedQueryError` before any SQL could run. This is
109
+ * the function the conformance rewrite-vectors pin.
110
+ */
111
+ export declare function rewriteScopedCall(params: {
112
+ meta: RuntimeMeta;
113
+ resolved: ResolvedScopedModels;
114
+ scope: ScopeValue | null;
115
+ preds: ReadonlyMap<string, ClientRow | null>;
116
+ model: string;
117
+ method: string;
118
+ args: ClientRow | undefined;
119
+ }): RewrittenScopedCall;
120
+ /**
121
+ * Attach `$scoped` to a built client. Validation happens per call:
122
+ * `@@policy` schemas refuse (the two scoping layers do not combine), the
123
+ * models map is resolved against the schema (cached by object identity), and
124
+ * the scope value is type-checked against every scoped column.
125
+ */
126
+ export declare function attachScoped(params: {
127
+ client: DynamicClient;
128
+ meta: RuntimeMeta;
129
+ }): void;
130
+ export {};
131
+ //# sourceMappingURL=scoped.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scoped.d.ts","sourceRoot":"","sources":["../src/scoped.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibeorm/runtime",
3
- "version": "2.0.0-alpha.8",
3
+ "version": "2.0.0-alpha.9",
4
4
  "description": "Driver-agnostic query engine and client runtime for VibeORM v2",
5
5
  "keywords": [
6
6
  "orm",
@@ -41,11 +41,12 @@
41
41
  "bun": ">=1.2.0"
42
42
  },
43
43
  "dependencies": {
44
- "@vibeorm/schema": "2.0.0-alpha.6",
45
- "@vibeorm/sql": "2.0.0-alpha.6"
44
+ "@vibeorm/schema": "2.0.0-alpha.7",
45
+ "@vibeorm/sql": "2.0.0-alpha.7"
46
46
  },
47
47
  "devDependencies": {
48
- "@electric-sql/pglite": "^0.5.4"
48
+ "@electric-sql/pglite": "^0.5.4",
49
+ "@vibeorm/scoped-conformance": "0.1.0-alpha.0"
49
50
  },
50
51
  "publishConfig": {
51
52
  "access": "public"