@vibeorm/runtime 2.6.0 → 3.0.0
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.
- package/LICENSE +21 -0
- package/README.md +3 -1
- package/dist/adapter-kit/adapter-lifecycle.d.ts +79 -0
- package/dist/adapter-kit/deferred-control.d.ts +58 -0
- package/dist/adapter-kit/index.d.ts +10 -3
- package/dist/adapter-kit/index.js +317 -24
- package/dist/adapter-kit/index.js.map +10 -7
- package/dist/adapter-kit/nested-options.d.ts +16 -2
- package/dist/adapter-kit/row-changes.d.ts +15 -1
- package/dist/adapter-kit/savepoint-gate.d.ts +17 -3
- package/dist/adapter-kit/transaction-budget.d.ts +23 -13
- package/dist/adapter-kit/transaction-outcome.d.ts +167 -0
- package/dist/adapter.d.ts +103 -11
- package/dist/client.d.ts +7 -0
- package/dist/codecs.d.ts +35 -0
- package/dist/config.d.ts +15 -0
- package/dist/extensions.d.ts +10 -0
- package/dist/find-page.d.ts +9 -2
- package/dist/index.d.ts +8 -4
- package/dist/index.js +4086 -1295
- package/dist/index.js.map +40 -28
- package/dist/keyset-iterator.d.ts +12 -2
- package/dist/keyset.d.ts +65 -10
- package/dist/lateral-projection.d.ts +31 -1
- package/dist/model-meta.d.ts +12 -0
- package/dist/nested-fold.d.ts +98 -0
- package/dist/nested-update-data.d.ts +11 -0
- package/dist/nested-writes.d.ts +38 -1
- package/dist/query-builder.d.ts +78 -19
- package/dist/read-only.d.ts +5 -0
- package/dist/relation-key.d.ts +50 -0
- package/dist/relation-loader.d.ts +13 -1
- package/dist/relation-plan.d.ts +16 -0
- package/dist/strict-args.d.ts +1 -0
- package/dist/upsert-fold.d.ts +77 -0
- package/dist/write-scope.d.ts +9 -0
- package/package.json +7 -5
- package/dist/adapter-kit/index.d.ts.map +0 -1
- package/dist/adapter-kit/nested-options.d.ts.map +0 -1
- package/dist/adapter-kit/row-changes.d.ts.map +0 -1
- package/dist/adapter-kit/savepoint-gate.d.ts.map +0 -1
- package/dist/adapter-kit/savepoints.d.ts.map +0 -1
- package/dist/adapter-kit/session.d.ts.map +0 -1
- package/dist/adapter-kit/sqlite-session.d.ts.map +0 -1
- package/dist/adapter-kit/transaction-budget.d.ts.map +0 -1
- package/dist/adapter.d.ts.map +0 -1
- package/dist/advisory-key.d.ts.map +0 -1
- package/dist/advisory-lock.d.ts.map +0 -1
- package/dist/bulk-upsert.d.ts.map +0 -1
- package/dist/client-types.d.ts.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/codecs.d.ts.map +0 -1
- package/dist/computed.d.ts.map +0 -1
- package/dist/database-module.d.ts.map +0 -1
- package/dist/db-now.d.ts.map +0 -1
- package/dist/diagnostics/index.d.ts.map +0 -1
- package/dist/diagnostics/insight.d.ts.map +0 -1
- package/dist/diagnostics/plan.d.ts.map +0 -1
- package/dist/diagnostics/preview.d.ts.map +0 -1
- package/dist/diagnostics/statement-diagnostics.d.ts.map +0 -1
- package/dist/diagnostics/types.d.ts.map +0 -1
- package/dist/diagnostics/workload.d.ts.map +0 -1
- package/dist/extensions.d.ts.map +0 -1
- package/dist/find-page.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/keyset-iterator.d.ts.map +0 -1
- package/dist/keyset-projection.d.ts.map +0 -1
- package/dist/keyset.d.ts.map +0 -1
- package/dist/lateral-projection.d.ts.map +0 -1
- package/dist/model-meta.d.ts.map +0 -1
- package/dist/module-context.d.ts.map +0 -1
- package/dist/nested-writes.d.ts.map +0 -1
- package/dist/policy-operation.d.ts.map +0 -1
- package/dist/policy.d.ts.map +0 -1
- package/dist/query-builder.d.ts.map +0 -1
- package/dist/read-only.d.ts.map +0 -1
- package/dist/relation-key.d.ts.map +0 -1
- package/dist/relation-loader.d.ts.map +0 -1
- package/dist/relation-plan.d.ts.map +0 -1
- package/dist/render-cache.d.ts.map +0 -1
- package/dist/rls-context.d.ts.map +0 -1
- package/dist/rls-readiness.d.ts.map +0 -1
- package/dist/scoped.d.ts.map +0 -1
- package/dist/sql-access.d.ts.map +0 -1
- package/dist/sql.d.ts.map +0 -1
- package/dist/strict-args.d.ts.map +0 -1
- package/dist/telemetry/collector.d.ts.map +0 -1
- package/dist/telemetry/config.d.ts.map +0 -1
- package/dist/telemetry/fingerprint.d.ts.map +0 -1
- package/dist/telemetry/index.d.ts.map +0 -1
- package/dist/telemetry/recorder.d.ts.map +0 -1
- package/dist/telemetry/statement.d.ts.map +0 -1
- package/dist/telemetry/types.d.ts.map +0 -1
- package/dist/transaction-row-changes.d.ts.map +0 -1
- package/dist/validators.d.ts.map +0 -1
- package/dist/views.d.ts.map +0 -1
- package/dist/write-scope.d.ts.map +0 -1
|
@@ -27,7 +27,12 @@
|
|
|
27
27
|
import type { SqlDialect } from "@vibeorm/sql";
|
|
28
28
|
import type { KeysetTimestampPrecision } from "./keyset.ts";
|
|
29
29
|
import type { RuntimeMeta } from "./model-meta.ts";
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* The one method this iterator needs — a model delegate satisfies it. A
|
|
32
|
+
* wrapper that adds filters to `where` must return the delegate's result array
|
|
33
|
+
* itself: that array carries the query binding the engine checks, and a copy
|
|
34
|
+
* (`rows.map(…)`) loses it, so the next page is refused (`keyset-token-query`).
|
|
35
|
+
*/
|
|
31
36
|
export type KeysetPageSource = {
|
|
32
37
|
readonly findMany: (args: Record<string, unknown>) => Promise<Record<string, unknown>[]>;
|
|
33
38
|
};
|
|
@@ -43,11 +48,16 @@ export type KeysetIterateOptions = {
|
|
|
43
48
|
readonly select?: Record<string, unknown>;
|
|
44
49
|
readonly omit?: Record<string, unknown>;
|
|
45
50
|
readonly include?: Record<string, unknown>;
|
|
46
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Resume from a checkpoint minted by a previous run over the SAME ordering,
|
|
53
|
+
* filters and scope — a checkpoint from another `where` or `scope` is refused.
|
|
54
|
+
*/
|
|
47
55
|
readonly after?: string;
|
|
48
56
|
/** Consumer cancellation: stops future page fetches; ends iteration cleanly. */
|
|
49
57
|
readonly signal?: AbortSignal;
|
|
50
58
|
readonly timestampPrecision?: KeysetTimestampPrecision;
|
|
59
|
+
/** The caller's owner key (a user or tenant id) the checkpoints are bound to. */
|
|
60
|
+
readonly scope?: string;
|
|
51
61
|
};
|
|
52
62
|
export type KeysetIterator = AsyncIterableIterator<Record<string, unknown>> & {
|
|
53
63
|
/** Always present: `break` and an exception both have to be able to release the traversal. */
|
package/dist/keyset.d.ts
CHANGED
|
@@ -7,17 +7,23 @@
|
|
|
7
7
|
* right place. That is the whole difference from the legacy `cursor`, which
|
|
8
8
|
* addresses a row by a unique key and is left untouched by this module.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* Three rules this module exists to enforce:
|
|
11
11
|
* - a position is only meaningful against ONE effective ordering, so the
|
|
12
12
|
* token carries that ordering's signature and a mismatch is refused loudly
|
|
13
13
|
* rather than silently paginating the wrong sequence;
|
|
14
|
+
* - a position is only meaningful for the QUERY it came from, so the token
|
|
15
|
+
* also carries a digest of the normalized `where` (including the filters
|
|
16
|
+
* `$scoped` / `$withPolicy` add) and of the caller's optional
|
|
17
|
+
* `keyset.scope`, and a mismatch is refused the same way;
|
|
14
18
|
* - a position's values are encoded LOSSLESSLY per scalar type — never
|
|
15
19
|
* through a float64 detour and never from a rounded display value.
|
|
16
20
|
*
|
|
17
|
-
* The token is ENCODED, not encrypted: anyone holding it can read the
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
+
* The token is ENCODED, not encrypted: anyone holding it can read the model,
|
|
22
|
+
* the ordering and the ordering values inside, and can confirm a guessed
|
|
23
|
+
* filter or scope against the digest. It is also NOT an authorization
|
|
24
|
+
* credential — it selects a position, never a row; every query consuming one
|
|
25
|
+
* still compiles the caller's own `where` and keeps its `$scoped` /
|
|
26
|
+
* `$withPolicy` / RLS filters.
|
|
21
27
|
*/
|
|
22
28
|
import type { SqlDialect } from "@vibeorm/sql";
|
|
23
29
|
import type { FieldMeta, ModelMeta } from "./model-meta.ts";
|
|
@@ -28,6 +34,11 @@ export type KeysetNulls = "first" | "last";
|
|
|
28
34
|
* millisecond-exact values. See {@link compileKeysetOrderPlan}.
|
|
29
35
|
*/
|
|
30
36
|
export type KeysetTimestampPrecision = "milliseconds";
|
|
37
|
+
/**
|
|
38
|
+
* The query a token is bound to: a digest of the normalized `where` and the
|
|
39
|
+
* caller's optional `keyset.scope`. See {@link keysetQueryBinding}.
|
|
40
|
+
*/
|
|
41
|
+
export type KeysetQueryBinding = string;
|
|
31
42
|
/** One order term, already resolved by the query builder. */
|
|
32
43
|
export type KeysetOrderInput = {
|
|
33
44
|
readonly field: FieldMeta;
|
|
@@ -51,7 +62,11 @@ export type KeysetOrderPlan = {
|
|
|
51
62
|
};
|
|
52
63
|
/** Per-scalar lossless encoding tag stored in the token. */
|
|
53
64
|
export type KeysetValueTag = "s" | "i" | "b" | "f" | "c" | "t" | "o" | "y";
|
|
54
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Token version. A token from another version is refused, never guessed at —
|
|
67
|
+
* including version 1, minted before tokens were bound to their query: it
|
|
68
|
+
* cannot prove which filters it came from.
|
|
69
|
+
*/
|
|
55
70
|
export declare const KEYSET_TOKEN_VERSION: number;
|
|
56
71
|
/** Token prefix — `vk` for "vibe keyset", then the version. */
|
|
57
72
|
export declare const KEYSET_TOKEN_PREFIX: string;
|
|
@@ -97,8 +112,44 @@ export declare function keysetRowValues(params: {
|
|
|
97
112
|
plan: KeysetOrderPlan;
|
|
98
113
|
row: Readonly<Record<string, unknown>>;
|
|
99
114
|
}): readonly unknown[];
|
|
115
|
+
/**
|
|
116
|
+
* The query a keyset token is bound to: the first 128 bits (hex) of a SHA-256
|
|
117
|
+
* over the canonical `where` and the caller's optional scope.
|
|
118
|
+
*
|
|
119
|
+
* `where` is the filter the ENGINE compiles — including the predicates
|
|
120
|
+
* `$scoped` and `$withPolicy` merge into it — so a token minted in one tenant's
|
|
121
|
+
* handle is refused in another's without any caller scope. `scope` is the
|
|
122
|
+
* caller's own owner key (a user or tenant id) for cases the filters do not
|
|
123
|
+
* show, e.g. native RLS context. A digest is not a secret: a holder can
|
|
124
|
+
* confirm a guessed filter value or scope against it.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* keysetQueryBinding({ where: { status: "OPEN" }, scope: userId })
|
|
128
|
+
*/
|
|
129
|
+
export declare function keysetQueryBinding(params: {
|
|
130
|
+
where: unknown;
|
|
131
|
+
scope?: string;
|
|
132
|
+
}): KeysetQueryBinding;
|
|
133
|
+
/**
|
|
134
|
+
* Hidden channel on a `findMany` result that carried keyset arguments: the
|
|
135
|
+
* binding the ENGINE computed for it. The iterator mints its checkpoints with
|
|
136
|
+
* it, so a source that merges filters into `where` (`$scoped`, `$withPolicy`)
|
|
137
|
+
* yields checkpoints that source accepts. Non-enumerable, never serialized.
|
|
138
|
+
*/
|
|
139
|
+
export declare const KEYSET_RESULT_BINDING: unique symbol;
|
|
140
|
+
/** Attach the engine's binding to a result array (see {@link KEYSET_RESULT_BINDING}). */
|
|
141
|
+
export declare function attachKeysetBinding<T extends object>(params: {
|
|
142
|
+
rows: T;
|
|
143
|
+
binding: KeysetQueryBinding;
|
|
144
|
+
}): T;
|
|
145
|
+
/** The engine's binding a result array carries, if any. */
|
|
146
|
+
export declare function keysetBindingOf(params: {
|
|
147
|
+
rows: unknown;
|
|
148
|
+
}): KeysetQueryBinding | undefined;
|
|
100
149
|
/**
|
|
101
150
|
* Mint a continuation token from a row that carries every ordering value.
|
|
151
|
+
* `binding` is the query it continues ({@link keysetQueryBinding}); omitted,
|
|
152
|
+
* it is the unfiltered, unscoped query.
|
|
102
153
|
*
|
|
103
154
|
* The row must contain each ordering field — a missing one is refused rather
|
|
104
155
|
* than encoded as a null, because a silently null position paginates the wrong
|
|
@@ -108,15 +159,19 @@ export declare function keysetRowValues(params: {
|
|
|
108
159
|
export declare function encodeKeysetToken(params: {
|
|
109
160
|
plan: KeysetOrderPlan;
|
|
110
161
|
row: Readonly<Record<string, unknown>>;
|
|
162
|
+
binding?: KeysetQueryBinding;
|
|
111
163
|
}): string;
|
|
112
164
|
/**
|
|
113
|
-
* Read a token back into ordering values, validating version, model
|
|
114
|
-
* effective order. A token minted under a different
|
|
115
|
-
*
|
|
116
|
-
*
|
|
165
|
+
* Read a token back into ordering values, validating version, model, the
|
|
166
|
+
* effective order and the query binding. A token minted under a different
|
|
167
|
+
* ordering, for different filters or another scope (or for another model, or
|
|
168
|
+
* by another version) is refused with a precise, non-guessing error — never
|
|
169
|
+
* used to paginate a sequence it does not describe. `binding` is the query
|
|
170
|
+
* presenting it; omitted, the unfiltered, unscoped query.
|
|
117
171
|
*/
|
|
118
172
|
export declare function decodeKeysetToken(params: {
|
|
119
173
|
plan: KeysetOrderPlan;
|
|
120
174
|
token: string;
|
|
175
|
+
binding?: KeysetQueryBinding;
|
|
121
176
|
}): readonly unknown[];
|
|
122
177
|
//# sourceMappingURL=keyset.d.ts.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ColRef } from "@vibeorm/sql";
|
|
2
|
-
import type {
|
|
2
|
+
import type { FieldMeta } from "./model-meta.ts";
|
|
3
|
+
import type { RelationSelection, RelationTreeNode } from "./relation-plan.ts";
|
|
3
4
|
/** Child table qualifier, scoped independently inside each lateral. */
|
|
4
5
|
export declare const LATERAL_CHILD_ALIAS: string;
|
|
5
6
|
/** Shared SQL/decode descriptor. Its order is the resolved physical projection, never model order. */
|
|
@@ -11,6 +12,35 @@ export type LateralProjection = {
|
|
|
11
12
|
export declare function lateralProjection(params: {
|
|
12
13
|
node: RelationTreeNode;
|
|
13
14
|
}): LateralProjection;
|
|
15
|
+
/**
|
|
16
|
+
* B3 (SQL review): a list column of BigInt/Decimal/Bytes has no faithful JSON
|
|
17
|
+
* wire form, so the lateral projection refuses it. ONE predicate, read by the
|
|
18
|
+
* refusal ({@link lateralProjection}) and by the R-02 nesting rule
|
|
19
|
+
* ({@link nestedLateralSelection}), so a level that would refuse never nests.
|
|
20
|
+
*/
|
|
21
|
+
export declare function lateralRefusesField(params: {
|
|
22
|
+
field: FieldMeta;
|
|
23
|
+
}): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* R-02 (round-trip campaign): the deeper level of a `"join"` relation node that
|
|
26
|
+
* rides INSIDE its lateral — each child carries one more positional slot per
|
|
27
|
+
* nested relation (`__rel_<name>`, its own nested lateral's JSON) — or `null`
|
|
28
|
+
* when that level keeps today's batched `"query"` loads. The builder (nesting
|
|
29
|
+
* the SQL) and the loader (decoding the slots) both read THIS function, so
|
|
30
|
+
* the payload and its decoder can never disagree. Cached per node.
|
|
31
|
+
*
|
|
32
|
+
* A level nests only in the shapes the campaign measured (EPIC 1 spike, EPIC 5
|
|
33
|
+
* A/B): foreign-key links (to-many FK, to-one either side), plain column
|
|
34
|
+
* orders, no `_count` at that level. Implicit many-to-many children and counts
|
|
35
|
+
* below level 1 keep the query strategy (with R-03a's derived counts) —
|
|
36
|
+
* unmeasured, so unchanged. (An include's orderBy is validated to scalars
|
|
37
|
+
* upstream; the order check here is defensive.) A level whose projection the
|
|
38
|
+
* lateral would refuse ({@link lateralRefusesField}, e.g. a `BigInt[]` column)
|
|
39
|
+
* keeps the query strategy too — before R-02 it loaded through it (RV4 B1).
|
|
40
|
+
*/
|
|
41
|
+
export declare function nestedLateralSelection(params: {
|
|
42
|
+
node: RelationTreeNode;
|
|
43
|
+
}): RelationSelection | null;
|
|
14
44
|
/**
|
|
15
45
|
* PG-3 (SQL review round 2): order fields the lateral subselect must project
|
|
16
46
|
* HIDDEN — the aggregate's explicit `ORDER BY "__sub".…` (N2) can only
|
package/dist/model-meta.d.ts
CHANGED
|
@@ -152,6 +152,18 @@ export type RuntimeMeta = {
|
|
|
152
152
|
*/
|
|
153
153
|
readonly extensionOperators?: ReadonlyMap<string, ExtensionOperatorFn>;
|
|
154
154
|
};
|
|
155
|
+
/**
|
|
156
|
+
* The postgres type of a Json field's column (of its elements, for a list):
|
|
157
|
+
* `json` for `@db.Json` in any letter case — `.prisma` files say `@db.Json`,
|
|
158
|
+
* `db pull` writes `@db.json` and migrate accepts both — else `jsonb`. The
|
|
159
|
+
* ONE rule for every json/jsonb decision in the runtime: filters, list
|
|
160
|
+
* operands, order/distinct casts, write cells and the nested-write fold.
|
|
161
|
+
* Migrate normalizes the same annotation case-insensitively on its own
|
|
162
|
+
* (`normalizeNativeType`). Meaningful on postgres only.
|
|
163
|
+
*/
|
|
164
|
+
export declare function jsonStorageOf(params: {
|
|
165
|
+
field: FieldMeta;
|
|
166
|
+
}): "json" | "jsonb";
|
|
155
167
|
/**
|
|
156
168
|
* Delegate key for a model name: the first character lowercased, nothing else
|
|
157
169
|
* (the Prisma convention — `User` → `user`, `URLMap` → `uRLMap`). Keeping the
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A nested write as ONE statement (round-trip campaign EPIC F, narrow scope).
|
|
3
|
+
*
|
|
4
|
+
* A top-level `create`, or an `update` with scalar data, whose ONLY nested
|
|
5
|
+
* operation is ONE homogeneous FK-side list of scalar children (`create: [...]`
|
|
6
|
+
* or `createMany`) runs today as parent statement + child statement inside a
|
|
7
|
+
* transaction (BEGIN · parent · children · COMMIT; a savepoint inside a
|
|
8
|
+
* caller's transaction). On a dialect with a statement-chain strategy
|
|
9
|
+
* (postgres: pg, bun, pglite) it is one statement instead:
|
|
10
|
+
*
|
|
11
|
+
* ```sql
|
|
12
|
+
* WITH "__vibe_parent" AS (INSERT … RETURNING … | UPDATE … WHERE <full where> RETURNING …),
|
|
13
|
+
* "__vibe_children" AS (INSERT INTO child (cols…, fk) SELECT src.cols…, "__vibe_parent".key
|
|
14
|
+
* FROM "__vibe_parent" CROSS JOIN (<typed VALUES rows>) AS src RETURNING …)
|
|
15
|
+
* SELECT parent row, CASE WHEN <a step returned nothing> THEN <fail> ELSE <rows changed> END
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Every child takes the parent key FROM the parent step, so an empty parent
|
|
19
|
+
* writes no child and a child BEFORE trigger sees the parent (review O-07).
|
|
20
|
+
* The in-statement veto (shape gate, `report-epic-f.md` §1) makes the
|
|
21
|
+
* STATEMENT fail when the parent step is empty (an UPDATE that matched
|
|
22
|
+
* nothing, a write a trigger suppressed) or, for a `create` list, when the
|
|
23
|
+
* child step returned fewer rows than items (EPIC W: one item → no row; N
|
|
24
|
+
* items → fewer than N, the `minRows` veto) — exactly where today's flow raises
|
|
25
|
+
* not-found. The database then undoes every effect, trigger writes included,
|
|
26
|
+
* so the statement needs no transaction at top level and no savepoint inside
|
|
27
|
+
* one (decision D1). The runtime turns the veto back into today's error.
|
|
28
|
+
*
|
|
29
|
+
* `createMany` carries no child veto: it never checks its row count (a
|
|
30
|
+
* trigger-suppressed child is silently skipped, like the top-level
|
|
31
|
+
* createMany's count).
|
|
32
|
+
*
|
|
33
|
+
* This module only BUILDS the statement (pure). Eligibility is a closed
|
|
34
|
+
* allowlist; anything else returns `null` and the whole call keeps today's
|
|
35
|
+
* path. Engine-level exclusions (native RLS, `@@policy` schemas) are the
|
|
36
|
+
* caller's.
|
|
37
|
+
*/
|
|
38
|
+
import { VibeError } from "@vibeorm/schema";
|
|
39
|
+
import type { ChainStatement, SqlDialect, SqlStatement } from "@vibeorm/sql";
|
|
40
|
+
import type { ModelMeta, RuntimeMeta } from "./model-meta.ts";
|
|
41
|
+
import type { QueryMethod } from "./query-builder.ts";
|
|
42
|
+
/** Step names and the row-source alias. Models whose table starts with `__vibe_` never fold. */
|
|
43
|
+
export declare const NESTED_FOLD_NAMES: {
|
|
44
|
+
readonly parent: string;
|
|
45
|
+
readonly children: string;
|
|
46
|
+
readonly source: string;
|
|
47
|
+
};
|
|
48
|
+
/** The folded statement plus what the runtime needs to read its result or its veto. */
|
|
49
|
+
export type NestedFold = {
|
|
50
|
+
readonly statement: ChainStatement;
|
|
51
|
+
/** Model whose row the statement returns (the parent). */
|
|
52
|
+
readonly model: ModelMeta;
|
|
53
|
+
/** Veto token → today's error for that outcome. */
|
|
54
|
+
readonly vetoes: ReadonlyMap<string, () => VibeError>;
|
|
55
|
+
/** The per-call nonce every veto marker carries (a user value cannot know it). */
|
|
56
|
+
readonly nonce: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Fold the call into one statement, or return `null` (today's path). Builds
|
|
60
|
+
* the same plans today's flow builds (`plan`), so validation, defaults and
|
|
61
|
+
* codecs are unchanged; plan-time refusals raise today's errors (a child's now
|
|
62
|
+
* before any SQL, see below).
|
|
63
|
+
*
|
|
64
|
+
* Allowlist: statement-chain dialect with RETURNING; exactly ONE nested op, an
|
|
65
|
+
* FK-side to-many list (`toManyFk`) on a different table, not `__vibe_`
|
|
66
|
+
* prefixed; one verb — `create` (one or more items) or `createMany` without
|
|
67
|
+
* `skipDuplicates`; at least one item, none with nested writes, all defining
|
|
68
|
+
* the same fields; at most {@link RUNTIME_CONFIG}`.nestedFoldMaxChildren`
|
|
69
|
+
* items; no protected write scope on the verbs; an update must carry scalar
|
|
70
|
+
* data; one child statement (no parameter-budget chunks), no DEFAULT cell, no
|
|
71
|
+
* Json cell on a non-`jsonb` column, no column mixing `dbNow()` with values,
|
|
72
|
+
* `dbNow()` only on a timestamptz/timestamp/date column; no raw SQL fragment
|
|
73
|
+
* anywhere.
|
|
74
|
+
*
|
|
75
|
+
* The child plan is compiled BEFORE any SQL is sent (EPIC F decision 1): a
|
|
76
|
+
* child that validation refuses throws here, so it now outranks a parent
|
|
77
|
+
* database error (today's path compiled it after the parent statement ran).
|
|
78
|
+
*/
|
|
79
|
+
export declare function buildNestedFold(params: {
|
|
80
|
+
dialect: SqlDialect;
|
|
81
|
+
meta: RuntimeMeta;
|
|
82
|
+
model: ModelMeta;
|
|
83
|
+
method: "create" | "update";
|
|
84
|
+
data: Record<string, unknown>;
|
|
85
|
+
where?: Record<string, unknown>;
|
|
86
|
+
skipUpdatedAt?: boolean;
|
|
87
|
+
/** Per-call random nonce bound into every veto marker (review RV3 MN-3). */
|
|
88
|
+
nonce: string;
|
|
89
|
+
plan: (params: {
|
|
90
|
+
model: string;
|
|
91
|
+
method: QueryMethod;
|
|
92
|
+
args: Record<string, unknown>;
|
|
93
|
+
}) => {
|
|
94
|
+
statement: SqlStatement;
|
|
95
|
+
batch?: readonly SqlStatement[];
|
|
96
|
+
};
|
|
97
|
+
}): NestedFold | null;
|
|
98
|
+
//# sourceMappingURL=nested-fold.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ModelMeta } from "./model-meta.ts";
|
|
2
|
+
/** Resolve only a nested update's body, consistently across execution and argument walkers. */
|
|
3
|
+
export declare function resolveNestedUpdateData(params: {
|
|
4
|
+
model: ModelMeta;
|
|
5
|
+
isList: boolean;
|
|
6
|
+
operand: Record<string, unknown>;
|
|
7
|
+
}): {
|
|
8
|
+
readonly data: Record<string, unknown>;
|
|
9
|
+
readonly wrapped: boolean;
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=nested-update-data.d.ts.map
|
package/dist/nested-writes.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SqlDialect, SqlStatement } from "@vibeorm/sql";
|
|
2
2
|
import type { ModelMeta, RuntimeMeta } from "./model-meta.ts";
|
|
3
3
|
import type { QueryMethod } from "./query-builder.ts";
|
|
4
|
+
import type { RelationLink } from "./relation-plan.ts";
|
|
4
5
|
export type ClientRowLike = Record<string, unknown>;
|
|
5
6
|
/**
|
|
6
7
|
* Executes ORM statements against ONE adapter (plain or transactional).
|
|
@@ -14,6 +15,12 @@ export type StatementRunner = {
|
|
|
14
15
|
method: QueryMethod;
|
|
15
16
|
args: Record<string, unknown>;
|
|
16
17
|
}) => Promise<ClientRowLike>;
|
|
18
|
+
/** `single`, but `null` when the where matched no row — the caller raises its own not-found. */
|
|
19
|
+
readonly singleOrNull: (params: {
|
|
20
|
+
model: string;
|
|
21
|
+
method: QueryMethod;
|
|
22
|
+
args: Record<string, unknown>;
|
|
23
|
+
}) => Promise<ClientRowLike | null>;
|
|
17
24
|
readonly rows: (params: {
|
|
18
25
|
model: string;
|
|
19
26
|
method: QueryMethod;
|
|
@@ -36,7 +43,10 @@ export type WriteContext = {
|
|
|
36
43
|
readonly dialect: SqlDialect;
|
|
37
44
|
/** Runner on the client's current adapter (may already be transactional). */
|
|
38
45
|
readonly run: StatementRunner;
|
|
39
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Runs `fn` with a runner bound to a (nested) transaction — or, inside a
|
|
48
|
+
* transaction an array batch or the ORM call already owns, on that one.
|
|
49
|
+
*/
|
|
40
50
|
readonly transactional: <T>(fn: (run: StatementRunner) => Promise<T>) => Promise<T>;
|
|
41
51
|
};
|
|
42
52
|
/** Does this `data` object carry nested relation operations? */
|
|
@@ -44,6 +54,31 @@ export declare function hasNestedWrites(params: {
|
|
|
44
54
|
model: ModelMeta;
|
|
45
55
|
data: unknown;
|
|
46
56
|
}): boolean;
|
|
57
|
+
/** One relation's nested-write verbs, as `partitionData` found them. */
|
|
58
|
+
export type NestedOp = {
|
|
59
|
+
readonly name: string;
|
|
60
|
+
readonly link: RelationLink;
|
|
61
|
+
readonly verbs: Record<string, unknown>;
|
|
62
|
+
};
|
|
63
|
+
/** Split write data into scalar columns and nested relation ops (validating verb names). */
|
|
64
|
+
export declare function partitionData(params: {
|
|
65
|
+
meta: RuntimeMeta;
|
|
66
|
+
model: ModelMeta;
|
|
67
|
+
data: Record<string, unknown>;
|
|
68
|
+
phase: "create" | "update";
|
|
69
|
+
}): {
|
|
70
|
+
scalars: Record<string, unknown>;
|
|
71
|
+
ops: NestedOp[];
|
|
72
|
+
};
|
|
73
|
+
/** A verb's operand as its list of object items (refusing an array on a to-one relation). */
|
|
74
|
+
export declare function toItems(params: {
|
|
75
|
+
op: NestedOp;
|
|
76
|
+
verb: string;
|
|
77
|
+
operand: unknown;
|
|
78
|
+
}): Record<string, unknown>[];
|
|
79
|
+
/** Every row defines the same key set — the batching precondition (see below). */
|
|
80
|
+
/** Whether every row defines the same keys (an undefined value counts as absent). */
|
|
81
|
+
export declare function sameDefinedKeys(rows: readonly Record<string, unknown>[]): boolean;
|
|
47
82
|
/**
|
|
48
83
|
* `create` with nested relation operations. Returns the parent row as the
|
|
49
84
|
* INSERT produced it (relation loading/projection are the client's follow-up).
|
|
@@ -63,5 +98,7 @@ export declare function updateWithNested(params: {
|
|
|
63
98
|
model: ModelMeta;
|
|
64
99
|
where: Record<string, unknown>;
|
|
65
100
|
data: Record<string, unknown>;
|
|
101
|
+
/** Suppress only the parent's automatic timestamp, never the child's. */
|
|
102
|
+
skipUpdatedAt?: boolean;
|
|
66
103
|
}): Promise<ClientRowLike>;
|
|
67
104
|
//# sourceMappingURL=nested-writes.d.ts.map
|
package/dist/query-builder.d.ts
CHANGED
|
@@ -32,6 +32,17 @@ export type AggregateSpec = {
|
|
|
32
32
|
};
|
|
33
33
|
/** How included relations are fetched. */
|
|
34
34
|
export type RelationStrategy = "query" | "join";
|
|
35
|
+
/**
|
|
36
|
+
* R-03b: one level-1 `_count` entry the root find statement projects as a
|
|
37
|
+
* correlated `COUNT(*)` column, so the loader reads it off the row instead of
|
|
38
|
+
* sending its grouped count statement.
|
|
39
|
+
*/
|
|
40
|
+
export type EmbeddedCount = {
|
|
41
|
+
/** The counted relation (`posts`) — the `_count` key. */
|
|
42
|
+
readonly name: string;
|
|
43
|
+
/** The root row's column carrying the count (`__vibe_count_0`), deleted after reading. */
|
|
44
|
+
readonly column: string;
|
|
45
|
+
};
|
|
35
46
|
/**
|
|
36
47
|
* A built query plus everything the client needs after execution. The plan is
|
|
37
48
|
* data only: the client decides how to run it.
|
|
@@ -65,7 +76,9 @@ export type QueryPlan = {
|
|
|
65
76
|
/**
|
|
66
77
|
* How relations load. With `"join"` on a find method the base statement IS
|
|
67
78
|
* the lateral statement (level-1 relations ride along as JSON columns);
|
|
68
|
-
* deeper levels
|
|
79
|
+
* deeper levels nest inside them where {@link nestedLateralSelection}
|
|
80
|
+
* admits (R-02, `nestedLaterals`) and otherwise fall back to `"query"`
|
|
81
|
+
* batches (v1 semantics).
|
|
69
82
|
*/
|
|
70
83
|
readonly relationStrategy: RelationStrategy;
|
|
71
84
|
/** True when `take` produced a LIMIT. */
|
|
@@ -102,6 +115,18 @@ export type QueryPlan = {
|
|
|
102
115
|
* update 0, so its number is not a row count at all.
|
|
103
116
|
*/
|
|
104
117
|
readonly upsertRowCount?: number;
|
|
118
|
+
/**
|
|
119
|
+
* find methods under the `"query"` strategy only (R-03b): the level-1
|
|
120
|
+
* `_count` entries this statement already projects. Absent when none.
|
|
121
|
+
*/
|
|
122
|
+
readonly embeddedCounts?: readonly EmbeddedCount[];
|
|
123
|
+
/**
|
|
124
|
+
* find methods under `"join"` only (R-02): the lateral statement nests the
|
|
125
|
+
* deeper relation levels {@link nestedLateralSelection} admits inside the
|
|
126
|
+
* depth-1 laterals, so the loader decodes them from the payload instead of
|
|
127
|
+
* querying them. Absent when the statement nests nothing.
|
|
128
|
+
*/
|
|
129
|
+
readonly nestedLaterals?: boolean;
|
|
105
130
|
/** The caller's `select`, replayed on that re-select. */
|
|
106
131
|
readonly selectArg: Readonly<Record<string, unknown>> | null;
|
|
107
132
|
/** How to decode aggregate/groupBy/count-select rows; `null` elsewhere. */
|
|
@@ -110,26 +135,20 @@ export type QueryPlan = {
|
|
|
110
135
|
readonly groupByFields: readonly string[] | null;
|
|
111
136
|
/** Negative `take`: the SQL ordering was flipped; re-reverse rows after fetch. */
|
|
112
137
|
readonly reverseRows: boolean;
|
|
113
|
-
/**
|
|
114
|
-
* `distinct` + an explicit user orderBy (N1, board #22): the arrangement
|
|
115
|
-
* DISTINCT ON needs demotes the user's order behind the distinct fields, so
|
|
116
|
-
* the client re-sorts the FINAL rows by these keys — Prisma's presentation
|
|
117
|
-
* order, on every dialect. `nulls` is resolved at build time to the
|
|
118
|
-
* dialect's own NULL placement so the re-sort mirrors what the SQL order
|
|
119
|
-
* would have produced. `null` when no re-sort is needed.
|
|
120
|
-
*/
|
|
121
|
-
readonly resortBy: readonly ResortKey[] | null;
|
|
122
|
-
};
|
|
123
|
-
/** One client-side re-sort key (see QueryPlan.resortBy). */
|
|
124
|
-
export type ResortKey = {
|
|
125
|
-
readonly field: string;
|
|
126
|
-
readonly direction: "asc" | "desc";
|
|
127
|
-
readonly nulls: "first" | "last";
|
|
128
|
-
/** B4 mirror: Decimal ranks numerically — the SQL side ordered a number. */
|
|
129
|
-
readonly decimal: boolean;
|
|
130
138
|
};
|
|
131
139
|
/** Column alias for COUNT(*) — postgres and sqlite/mysql name it differently otherwise. */
|
|
132
140
|
export declare const COUNT_ALIAS: string;
|
|
141
|
+
/**
|
|
142
|
+
* A model's full row projection as SQL text, every column qualified by
|
|
143
|
+
* `qualify` and read in the form `findMany` reads it ({@link projectionRef}:
|
|
144
|
+
* postgres `Float[]`/`Json[]` as `::text`). For statements built outside the
|
|
145
|
+
* query builder — extension SQL — whose rows decode through the model's plan.
|
|
146
|
+
*/
|
|
147
|
+
export declare function compileProjection(params: {
|
|
148
|
+
dialect: SqlDialect;
|
|
149
|
+
model: ModelMeta;
|
|
150
|
+
qualify: string;
|
|
151
|
+
}): string;
|
|
133
152
|
/**
|
|
134
153
|
* Compile a Prisma-shaped where object; `undefined` when it constrains nothing.
|
|
135
154
|
*
|
|
@@ -167,6 +186,19 @@ export declare function compileSelect(params: {
|
|
|
167
186
|
select: unknown;
|
|
168
187
|
allowEmpty?: boolean;
|
|
169
188
|
}): string[];
|
|
189
|
+
/**
|
|
190
|
+
* Expand the compound-key sugar the GENERATED `…WhereUniqueInput` emits:
|
|
191
|
+
* `{ projectId_userId: { projectId, userId } }` becomes
|
|
192
|
+
* `{ projectId: …, userId: … }`, so a composite `@@id`/`@@unique` compiles
|
|
193
|
+
* exactly like the flat form. Models without a composite key (the common case)
|
|
194
|
+
* return their where object untouched, allocation-free.
|
|
195
|
+
*/
|
|
196
|
+
export declare function expandCompoundWhere(params: {
|
|
197
|
+
model: ModelMeta;
|
|
198
|
+
where: Record<string, unknown>;
|
|
199
|
+
/** Argument label for errors — `"where"` (default) or `"cursor"` (#4). */
|
|
200
|
+
what?: string;
|
|
201
|
+
}): Record<string, unknown>;
|
|
170
202
|
/**
|
|
171
203
|
* Does this (already compound-expanded) where address at most one row? The
|
|
172
204
|
* predicate `assertUniqueWhere` enforces, exported so a batch that stands in
|
|
@@ -206,6 +238,22 @@ export declare function upsertUsesNativeStatement(params: {
|
|
|
206
238
|
model: ModelMeta;
|
|
207
239
|
args: Record<string, unknown>;
|
|
208
240
|
}): boolean;
|
|
241
|
+
/**
|
|
242
|
+
* The rows split into the runs `createMany` inserts as one column group each,
|
|
243
|
+
* in INPUT order — or `null` when grouping would reorder them. A dialect with
|
|
244
|
+
* the per-row DEFAULT keyword keeps every row in one VALUES list (one run). On
|
|
245
|
+
* sqlite rows group by their COMPILED column set in first-appearance order
|
|
246
|
+
* ({@link compileCreateRows}), so item order survives only when every group is
|
|
247
|
+
* one contiguous run of items; otherwise autoincrement ids would be handed out
|
|
248
|
+
* of item order. Compiles each row exactly as the plan will (app defaults and
|
|
249
|
+
* `@updatedAt` included), so a row that cannot compile throws the plan's own
|
|
250
|
+
* error here.
|
|
251
|
+
*/
|
|
252
|
+
export declare function createManyRowRuns<T>(params: {
|
|
253
|
+
dialect: SqlDialect;
|
|
254
|
+
model: ModelMeta;
|
|
255
|
+
data: readonly T[];
|
|
256
|
+
}): readonly (readonly T[])[] | null;
|
|
209
257
|
/**
|
|
210
258
|
* Compile a raw `orderBy` argument into keyset order inputs — the ONE parsing
|
|
211
259
|
* path the iterator and the `after` bound share, so a token's signature can
|
|
@@ -217,12 +265,17 @@ export declare function compileKeysetOrderInputs(params: {
|
|
|
217
265
|
model: ModelMeta;
|
|
218
266
|
orderBy: unknown;
|
|
219
267
|
}): KeysetOrderInput[];
|
|
220
|
-
/**
|
|
268
|
+
/**
|
|
269
|
+
* The `keyset: { … }` per-query options bag: `timestampPrecision` (the
|
|
270
|
+
* millisecond assertion) and `scope` (the caller's owner key, mixed into the
|
|
271
|
+
* token's query binding).
|
|
272
|
+
*/
|
|
221
273
|
export declare function parseKeysetOptions(params: {
|
|
222
274
|
model: ModelMeta;
|
|
223
275
|
value: unknown;
|
|
224
276
|
}): {
|
|
225
277
|
timestampPrecision?: KeysetTimestampPrecision;
|
|
278
|
+
scope?: string;
|
|
226
279
|
};
|
|
227
280
|
/** The column a relation's JSON payload is exposed under in the outer row. */
|
|
228
281
|
export declare function lateralColumnAlias(params: {
|
|
@@ -264,5 +317,11 @@ export declare function buildQuery(params: {
|
|
|
264
317
|
args?: Record<string, unknown>;
|
|
265
318
|
defaultRelationStrategy?: RelationStrategy;
|
|
266
319
|
clientOptions?: BuilderClientOptions;
|
|
320
|
+
/**
|
|
321
|
+
* R-03b: `false` keeps level-1 counts out of the statement. A write's
|
|
322
|
+
* read-back (dialects without RETURNING) plans a find whose counts the
|
|
323
|
+
* write's own finalize loads with the grouped statement (RV4 m1).
|
|
324
|
+
*/
|
|
325
|
+
embedCounts?: boolean;
|
|
267
326
|
}): QueryPlan;
|
|
268
327
|
//# sourceMappingURL=query-builder.d.ts.map
|
package/dist/read-only.d.ts
CHANGED
|
@@ -105,6 +105,11 @@ export type LazyOperationBridge = {
|
|
|
105
105
|
reason: unknown;
|
|
106
106
|
}) => void;
|
|
107
107
|
};
|
|
108
|
+
/** @internal Inherit authentic classifier provenance without exposing a marker property. */
|
|
109
|
+
export declare function inheritReadOnlyReceiver(params: {
|
|
110
|
+
source: unknown;
|
|
111
|
+
target?: object;
|
|
112
|
+
}): boolean;
|
|
108
113
|
/**
|
|
109
114
|
* Attach `$readOnly` to a built client. Called once per client build; the
|
|
110
115
|
* facade itself is built lazily on the first call, so it always sees the FINAL
|
package/dist/relation-key.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { KeyTableMember, SqlDialect } from "@vibeorm/sql";
|
|
2
|
+
import type { FieldMeta } from "./model-meta.ts";
|
|
1
3
|
/** Compare decoded relation keys by value, including DateTime milliseconds and binary bytes. */
|
|
2
4
|
export declare function relationValueKey(params: {
|
|
3
5
|
value: unknown;
|
|
@@ -20,4 +22,52 @@ export declare function relationValueKey(params: {
|
|
|
20
22
|
export declare function relationTupleKey(params: {
|
|
21
23
|
values: readonly unknown[];
|
|
22
24
|
}): string | null;
|
|
25
|
+
/** EPIC R: alias of the key table (a `KeyTableJoin` from `@vibeorm/sql`). */
|
|
26
|
+
export declare const KEY_TABLE_ALIAS: string;
|
|
27
|
+
/** EPIC R: the key table's ordinal column — the index of the key row the database paired. */
|
|
28
|
+
export declare const KEY_ORDINAL_ALIAS: string;
|
|
29
|
+
/**
|
|
30
|
+
* Must the DATABASE decide which keys are equal?
|
|
31
|
+
*
|
|
32
|
+
* Comparing decoded keys in JavaScript is exact bytes. That is the database's
|
|
33
|
+
* own equality only where text compares bytes: postgres and sqlite
|
|
34
|
+
* (`conflictKeyEquality.textIsBinaryByDefault`). Mysql's default
|
|
35
|
+
* `utf8mb4_0900_ai_ci` equates `acme`/`Acme`/`ACME` and `cafe`/`Café`, and its
|
|
36
|
+
* PAD SPACE collations equate `x ` and `x` — the foreign key, the join table and
|
|
37
|
+
* every `IN` accept such a key, so an exact JavaScript comparison disagrees
|
|
38
|
+
* with the database (EPIC R for includes, release 3.0 D4 for nested writes).
|
|
39
|
+
*
|
|
40
|
+
* Any text (`String`) member switches the WHOLE tuple to the key table. A
|
|
41
|
+
* declared `@collation` does not opt out: it is the schema's claim, and
|
|
42
|
+
* migrate never alters an existing column's collation, so the physical column
|
|
43
|
+
* can still fold. Non-text keys (Int, BigInt, DateTime, Bytes, …) read back
|
|
44
|
+
* canonically on both sides and keep the exact comparison.
|
|
45
|
+
*
|
|
46
|
+
* Postgres (release 3.0 lane n, review "Fixes 2" and "Fixes 3"): a `citext`
|
|
47
|
+
* key member switches the tuple too — citext compares case-insensitively, and
|
|
48
|
+
* a citext foreign key or join column stores `acme` under the key `Acme`.
|
|
49
|
+
* Every other postgres key, and every sqlite key, compares by value, and its
|
|
50
|
+
* SQL is byte-identical to before.
|
|
51
|
+
*/
|
|
52
|
+
export declare function associatesInDatabase(params: {
|
|
53
|
+
dialect: SqlDialect;
|
|
54
|
+
fields: readonly FieldMeta[];
|
|
55
|
+
}): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* How a key member binds in the key table, on the dialect that runs it
|
|
58
|
+
* ({@link associatesInDatabase}): postgres in {@link postgresKeyTableMember},
|
|
59
|
+
* mysql below.
|
|
60
|
+
*/
|
|
61
|
+
export declare function keyTableMember(params: {
|
|
62
|
+
dialect: SqlDialect;
|
|
63
|
+
field: FieldMeta;
|
|
64
|
+
}): KeyTableMember;
|
|
65
|
+
/**
|
|
66
|
+
* The key-table ordinal a row carries, or `null` when it is not one. The
|
|
67
|
+
* ordinals are integer literals: they arrive as numbers or, under mysql's
|
|
68
|
+
* `bigNumberStrings`, as digit strings.
|
|
69
|
+
*/
|
|
70
|
+
export declare function keyOrdinalOf(params: {
|
|
71
|
+
raw: unknown;
|
|
72
|
+
}): number | null;
|
|
23
73
|
//# sourceMappingURL=relation-key.d.ts.map
|