@vibeorm/runtime 1.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/README.md +74 -0
- package/package.json +47 -0
- package/src/adapter.ts +114 -0
- package/src/client.ts +2055 -0
- package/src/errors.ts +39 -0
- package/src/id-generators.ts +151 -0
- package/src/index.ts +36 -0
- package/src/lateral-join-builder.ts +759 -0
- package/src/query-builder.ts +1417 -0
- package/src/relation-loader.ts +489 -0
- package/src/types.ts +290 -0
- package/src/where-builder.ts +737 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime types used by the generated client and the query builder.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { DatabaseAdapter } from "./adapter.ts";
|
|
6
|
+
|
|
7
|
+
export type VibeClientOptions = {
|
|
8
|
+
/** Database adapter instance (required). */
|
|
9
|
+
adapter: DatabaseAdapter;
|
|
10
|
+
/** Log SQL queries for debugging */
|
|
11
|
+
log?: boolean | "query" | "info" | "warn" | "error";
|
|
12
|
+
/**
|
|
13
|
+
* Default relation loading strategy for all queries.
|
|
14
|
+
* - "query" (default): Separate batched WHERE IN queries per relation
|
|
15
|
+
* - "join": Single query with PostgreSQL LATERAL JOINs
|
|
16
|
+
*/
|
|
17
|
+
relationStrategy?: "query" | "join";
|
|
18
|
+
/**
|
|
19
|
+
* Strategy for building COUNT queries.
|
|
20
|
+
* - "direct" (default): `SELECT COUNT(*) FROM "Table" WHERE ...`
|
|
21
|
+
* Uses PostgreSQL parallel workers when available. Best for standard
|
|
22
|
+
* PostgreSQL deployments (self-hosted, RDS, Cloud SQL, Docker).
|
|
23
|
+
*
|
|
24
|
+
* - "subquery": `SELECT COUNT(*) FROM (SELECT "id" FROM "Table" WHERE ... OFFSET 0) AS "sub"`
|
|
25
|
+
* Wraps the count in a subquery with OFFSET 0, which can produce better
|
|
26
|
+
* query plans on some serverless/proxy PostgreSQL providers (e.g. Neon,
|
|
27
|
+
* PlanetScale, Supabase pooler) but disables parallel workers on standard
|
|
28
|
+
* PostgreSQL. Use this if count queries are consistently slow on your
|
|
29
|
+
* remote database.
|
|
30
|
+
*/
|
|
31
|
+
countStrategy?: "direct" | "subquery";
|
|
32
|
+
/**
|
|
33
|
+
* Eagerly establish database connections on client creation.
|
|
34
|
+
* When true, the connection pool is warmed up immediately instead of
|
|
35
|
+
* waiting for the first query, eliminating cold-start latency (~300ms).
|
|
36
|
+
* Defaults to false (lazy connection).
|
|
37
|
+
*/
|
|
38
|
+
eager?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Enable Zod validation of inputs and/or outputs using the generated schemas.
|
|
41
|
+
* - false (default): no validation — zero runtime overhead
|
|
42
|
+
* - true / "all": validate both mutation inputs and query outputs
|
|
43
|
+
* - "input": validate mutation inputs only (create/update data)
|
|
44
|
+
* - "output": validate query result rows only
|
|
45
|
+
*
|
|
46
|
+
* Requires the generated schemas to be wired into the client (automatic
|
|
47
|
+
* when using the generated VibeClient() factory).
|
|
48
|
+
*/
|
|
49
|
+
validate?: boolean | "input" | "output" | "all";
|
|
50
|
+
/**
|
|
51
|
+
* Custom ID generator function. Called when a field with @default(uuid/cuid/nanoid/ulid)
|
|
52
|
+
* is not provided in create data. Overrides the built-in generators.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* VibeClient({
|
|
57
|
+
* adapter: bunAdapter({ url: "postgres://..." }),
|
|
58
|
+
* idGenerator: ({ model, field, defaultKind }) => {
|
|
59
|
+
* return `${model.toLowerCase().slice(0, 3)}_${crypto.randomUUID()}`;
|
|
60
|
+
* }
|
|
61
|
+
* })
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
idGenerator?: (params: { model: string; field: string; defaultKind: string }) => string;
|
|
65
|
+
/**
|
|
66
|
+
* Enable per-query profiling with detailed timing breakdowns.
|
|
67
|
+
* - false (default): no profiling — zero runtime overhead
|
|
68
|
+
* - true: logs a structured timing breakdown to console after each query
|
|
69
|
+
* - A callback function: receives a QueryProfile object for custom handling
|
|
70
|
+
*
|
|
71
|
+
* Shows where time is spent: query building, SQL execution, relation loading,
|
|
72
|
+
* with per-relation breakdown including SQL text, exec time, and row counts.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* // Log to console
|
|
77
|
+
* VibeClient({ adapter, debug: true })
|
|
78
|
+
*
|
|
79
|
+
* // Collect profiles programmatically
|
|
80
|
+
* const profiles: QueryProfile[] = [];
|
|
81
|
+
* VibeClient({ adapter, debug: (p) => profiles.push(p) })
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
debug?: boolean | ((params: { profile: QueryProfile }) => void);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// ─── Query Profiling Types ─────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Per-relation timing breakdown emitted during profiled queries.
|
|
91
|
+
* Shows how long each relation's SQL execution took and how many rows were returned.
|
|
92
|
+
*/
|
|
93
|
+
export type RelationProfile = {
|
|
94
|
+
readonly relation: string;
|
|
95
|
+
readonly sqlExecMs: number;
|
|
96
|
+
readonly rowCount: number;
|
|
97
|
+
readonly sql: string;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Detailed per-query timing profile emitted when `debug` is enabled.
|
|
102
|
+
* Breaks down total wall-clock time into query building, SQL execution,
|
|
103
|
+
* relation loading (with per-relation details), and type coercion phases.
|
|
104
|
+
*/
|
|
105
|
+
export type QueryProfile = {
|
|
106
|
+
readonly model: string;
|
|
107
|
+
readonly operation: string;
|
|
108
|
+
readonly totalMs: number;
|
|
109
|
+
readonly queryBuildMs: number;
|
|
110
|
+
readonly sqlExecMs: number;
|
|
111
|
+
readonly rowCount: number;
|
|
112
|
+
readonly relationLoadMs: number;
|
|
113
|
+
readonly relationProfiles: readonly RelationProfile[];
|
|
114
|
+
readonly sql: string;
|
|
115
|
+
/** Time spent mapping raw DB rows to JS objects (scalar copy + JSON parse). */
|
|
116
|
+
readonly resultMapMs: number;
|
|
117
|
+
/** Approximate size of the result set in bytes (JSON-encoded). */
|
|
118
|
+
readonly resultSizeBytes: number;
|
|
119
|
+
/** SQL parameter values (for raw driver comparison). */
|
|
120
|
+
readonly sqlValues?: readonly unknown[];
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Mutable profiling context threaded through relation loading.
|
|
125
|
+
* Accumulates per-relation timings as each relation query executes.
|
|
126
|
+
* @internal — not part of the public API.
|
|
127
|
+
*/
|
|
128
|
+
export type ProfilingContext = {
|
|
129
|
+
relationProfiles: RelationProfile[];
|
|
130
|
+
/** Populated by executeLateralJoinQuery with per-phase timings. */
|
|
131
|
+
queryBuildMs?: number;
|
|
132
|
+
sqlExecMs?: number;
|
|
133
|
+
resultMapMs?: number;
|
|
134
|
+
sql?: string;
|
|
135
|
+
sqlValues?: unknown[];
|
|
136
|
+
rowCount?: number;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// ─── Validation Schema Types ──────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Duck-typed schema interface — works with any Zod schema without
|
|
143
|
+
* importing Zod directly. The runtime only calls safeParse() on these.
|
|
144
|
+
*/
|
|
145
|
+
export type ValidationSchema = {
|
|
146
|
+
safeParse(data: unknown): { success: true; data: unknown } | { success: false; error: unknown };
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Validation schemas for a single model, keyed by operation type.
|
|
151
|
+
* Generated by the schemas.ts file and optionally wired into the client.
|
|
152
|
+
*/
|
|
153
|
+
export type ModelSchemas = {
|
|
154
|
+
model?: ValidationSchema;
|
|
155
|
+
createInput?: ValidationSchema;
|
|
156
|
+
updateInput?: ValidationSchema;
|
|
157
|
+
whereInput?: ValidationSchema;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// ─── Model Metadata ───────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
export type ScalarFieldMeta = {
|
|
163
|
+
readonly name: string;
|
|
164
|
+
readonly dbName: string;
|
|
165
|
+
readonly kind: "scalar" | "enum";
|
|
166
|
+
readonly isUpdatedAt?: boolean;
|
|
167
|
+
/** Prisma type for runtime coercion (e.g., BigInt values from the DB driver) */
|
|
168
|
+
readonly type?: string;
|
|
169
|
+
/**
|
|
170
|
+
* Default value kind for application-level generation.
|
|
171
|
+
* - "uuid" | "cuid" | "nanoid" | "ulid": runtime generates the value if not provided
|
|
172
|
+
* - true: DB handles the default (autoincrement, now, literal)
|
|
173
|
+
*/
|
|
174
|
+
readonly hasDefault?: "uuid" | "cuid" | "nanoid" | "ulid" | true;
|
|
175
|
+
/** ID prefix to prepend to generated values (from /// @vibeorm.idPrefix("...")) */
|
|
176
|
+
readonly idPrefix?: string;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export type RelationFieldMeta = {
|
|
180
|
+
readonly name: string;
|
|
181
|
+
readonly kind: "relation";
|
|
182
|
+
readonly relatedModel: string;
|
|
183
|
+
readonly type: "oneToOne" | "oneToMany" | "manyToOne" | "manyToMany";
|
|
184
|
+
readonly isList: boolean;
|
|
185
|
+
readonly isForeignKey: boolean;
|
|
186
|
+
readonly fields: readonly string[];
|
|
187
|
+
readonly references: readonly string[];
|
|
188
|
+
readonly relationName?: string;
|
|
189
|
+
readonly joinTable?: string;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export type ModelMeta = {
|
|
193
|
+
readonly name: string;
|
|
194
|
+
readonly dbName: string;
|
|
195
|
+
readonly primaryKey: readonly string[];
|
|
196
|
+
readonly uniqueFields: readonly string[];
|
|
197
|
+
readonly scalarFields: readonly ScalarFieldMeta[];
|
|
198
|
+
readonly relationFields: readonly RelationFieldMeta[];
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export type ModelMetaMap = Record<string, ModelMeta>;
|
|
202
|
+
|
|
203
|
+
// ─── Query Operation Descriptor ───────────────────────────────────
|
|
204
|
+
|
|
205
|
+
export type Operation =
|
|
206
|
+
| "findMany"
|
|
207
|
+
| "findFirst"
|
|
208
|
+
| "findUnique"
|
|
209
|
+
| "findUniqueOrThrow"
|
|
210
|
+
| "findFirstOrThrow"
|
|
211
|
+
| "create"
|
|
212
|
+
| "createMany"
|
|
213
|
+
| "createManyAndReturn"
|
|
214
|
+
| "update"
|
|
215
|
+
| "upsert"
|
|
216
|
+
| "delete"
|
|
217
|
+
| "deleteMany"
|
|
218
|
+
| "updateMany"
|
|
219
|
+
| "count"
|
|
220
|
+
| "aggregate"
|
|
221
|
+
| "groupBy";
|
|
222
|
+
|
|
223
|
+
export type QueryDescriptor = {
|
|
224
|
+
model: string;
|
|
225
|
+
operation: Operation;
|
|
226
|
+
args: Record<string, unknown>;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// ─── SQL Builder Output ───────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
export type SqlQuery = {
|
|
232
|
+
text: string;
|
|
233
|
+
values: unknown[];
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// ─── Runtime Helpers (O(1) field/model lookups) ───────────────────
|
|
237
|
+
|
|
238
|
+
const _sfMapCache = new WeakMap<
|
|
239
|
+
readonly ScalarFieldMeta[],
|
|
240
|
+
Map<string, ScalarFieldMeta>
|
|
241
|
+
>();
|
|
242
|
+
const _modelNameCache = new WeakMap<object, Map<string, ModelMeta>>();
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get or build a Map<fieldName, ScalarFieldMeta> for O(1) lookups.
|
|
246
|
+
* Lazily builds and caches the Map per scalarFields array reference.
|
|
247
|
+
*/
|
|
248
|
+
export function getScalarFieldMap(params: {
|
|
249
|
+
scalarFields: readonly ScalarFieldMeta[];
|
|
250
|
+
}): ReadonlyMap<string, ScalarFieldMeta> {
|
|
251
|
+
const { scalarFields } = params;
|
|
252
|
+
let map = _sfMapCache.get(scalarFields);
|
|
253
|
+
if (!map) {
|
|
254
|
+
map = new Map(scalarFields.map((f) => [f.name, f]));
|
|
255
|
+
_sfMapCache.set(scalarFields, map);
|
|
256
|
+
}
|
|
257
|
+
return map;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get or build a Map<modelName, ModelMeta> for O(1) lookups by PascalCase model name.
|
|
262
|
+
* Lazily builds and caches the Map per allModelsMeta reference.
|
|
263
|
+
*/
|
|
264
|
+
export function getModelByNameMap(params: {
|
|
265
|
+
allModelsMeta: ModelMetaMap;
|
|
266
|
+
}): ReadonlyMap<string, ModelMeta> {
|
|
267
|
+
const { allModelsMeta } = params;
|
|
268
|
+
let map = _modelNameCache.get(allModelsMeta);
|
|
269
|
+
if (!map) {
|
|
270
|
+
map = new Map(
|
|
271
|
+
Object.values(allModelsMeta).map((m) => [m.name, m])
|
|
272
|
+
);
|
|
273
|
+
_modelNameCache.set(allModelsMeta, map);
|
|
274
|
+
}
|
|
275
|
+
return map;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ─── PostgreSQL Array Parameter Wrapper ───────────────────────────
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Wrapper to tag a JS array as a PostgreSQL array parameter for = ANY($N).
|
|
282
|
+
* Distinguishes from regular JS arrays (JSON values, scalar list ops)
|
|
283
|
+
* so that the adapter can convert only these to the appropriate format.
|
|
284
|
+
*
|
|
285
|
+
* - bun:sql adapter: converts to PG array literal string `{val1,val2,...}`
|
|
286
|
+
* - pg adapter: returns the raw JS array (pg handles native array serialization)
|
|
287
|
+
*/
|
|
288
|
+
export class PgArray {
|
|
289
|
+
constructor(public readonly values: unknown[]) {}
|
|
290
|
+
}
|