@vibeorm/runtime 1.3.0 → 2.0.0-alpha.2

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.
Files changed (44) hide show
  1. package/README.md +50 -107
  2. package/dist/adapter.d.ts +124 -0
  3. package/dist/adapter.d.ts.map +1 -0
  4. package/dist/client.d.ts +152 -0
  5. package/dist/client.d.ts.map +1 -0
  6. package/dist/codecs.d.ts +170 -0
  7. package/dist/codecs.d.ts.map +1 -0
  8. package/dist/computed.d.ts +43 -0
  9. package/dist/computed.d.ts.map +1 -0
  10. package/dist/extensions.d.ts +102 -0
  11. package/dist/extensions.d.ts.map +1 -0
  12. package/dist/index.d.ts +29 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +6625 -0
  15. package/dist/index.js.map +21 -0
  16. package/dist/model-meta.d.ts +156 -0
  17. package/dist/model-meta.d.ts.map +1 -0
  18. package/dist/nested-writes.d.ts +100 -0
  19. package/dist/nested-writes.d.ts.map +1 -0
  20. package/dist/query-builder.d.ts +250 -0
  21. package/dist/query-builder.d.ts.map +1 -0
  22. package/dist/relation-loader.d.ts +75 -0
  23. package/dist/relation-loader.d.ts.map +1 -0
  24. package/dist/relation-plan.d.ts +103 -0
  25. package/dist/relation-plan.d.ts.map +1 -0
  26. package/dist/render-cache.d.ts +48 -0
  27. package/dist/render-cache.d.ts.map +1 -0
  28. package/dist/views.d.ts +97 -0
  29. package/dist/views.d.ts.map +1 -0
  30. package/package.json +31 -26
  31. package/src/adapter.ts +0 -146
  32. package/src/client.ts +0 -2172
  33. package/src/coerce.ts +0 -184
  34. package/src/count-loader.ts +0 -152
  35. package/src/errors.ts +0 -492
  36. package/src/id-generators.ts +0 -151
  37. package/src/index.ts +0 -55
  38. package/src/lateral-join-builder.ts +0 -1053
  39. package/src/query-builder.ts +0 -1832
  40. package/src/relation-loader.ts +0 -534
  41. package/src/retry.ts +0 -183
  42. package/src/types.ts +0 -317
  43. package/src/view.ts +0 -629
  44. package/src/where-builder.ts +0 -772
package/src/coerce.ts DELETED
@@ -1,184 +0,0 @@
1
- /**
2
- * Scalar value type coercion.
3
- *
4
- * Coerce raw DB driver output to the JS types declared in the Prisma schema.
5
- * Currently handles `BigInt` (pg / bun:sql return PG `bigint` as a string,
6
- * but the application expects native `BigInt`).
7
- *
8
- * Lives in its own module so all relation loaders (query strategy, lateral
9
- * join strategy, post-write refresh, raw post-processing) can call it without
10
- * creating circular imports between `client.ts` and the relation loaders.
11
- *
12
- * Mutates the records in place — callers rely on this for performance.
13
- */
14
-
15
- import type { ModelMeta, ScalarFieldMeta } from "./types.ts";
16
-
17
- /**
18
- * Per-model cache of names of fields that need BigInt coercion.
19
- * Empty arrays are cached too, so the `length === 0` fast path costs one
20
- * `WeakMap.get` after the first call per model.
21
- *
22
- * Keyed by the `scalarFields` array reference (the same hot-path cache key
23
- * used by `getScalarFieldMap`), which is stable for the process lifetime
24
- * because model metadata is generated once at startup.
25
- */
26
- const _bigintFieldsCache = new WeakMap<
27
- readonly ScalarFieldMeta[],
28
- readonly string[]
29
- >();
30
-
31
- function getBigIntFieldNames(modelMeta: ModelMeta): readonly string[] {
32
- const sf = modelMeta.scalarFields;
33
- let names = _bigintFieldsCache.get(sf);
34
- if (names) return names;
35
- const arr: string[] = [];
36
- for (const f of sf) {
37
- if ((f as { type?: string }).type === "BigInt") arr.push(f.name);
38
- }
39
- names = arr;
40
- _bigintFieldsCache.set(sf, names);
41
- return names;
42
- }
43
-
44
- /**
45
- * Per-model cache of enum-array field names. Same WeakMap shape and
46
- * lifetime guarantees as `_bigintFieldsCache`.
47
- *
48
- * Enum-array columns need post-processing because neither `bun:sql` nor
49
- * `node-postgres` knows the user-defined enum's array type OID, so the
50
- * driver returns the raw PG array literal as a string (e.g. `"{ADMIN,USER}"`)
51
- * instead of a JS array. Built-in scalar arrays (`text[]`, `int4[]`, …) ARE
52
- * parsed by both drivers because their OIDs are well-known.
53
- */
54
- const _enumListFieldsCache = new WeakMap<
55
- readonly ScalarFieldMeta[],
56
- readonly string[]
57
- >();
58
-
59
- function getEnumListFieldNames(modelMeta: ModelMeta): readonly string[] {
60
- const sf = modelMeta.scalarFields;
61
- let names = _enumListFieldsCache.get(sf);
62
- if (names) return names;
63
- const arr: string[] = [];
64
- for (const f of sf) {
65
- if (f.kind === "enum" && f.isList === true) arr.push(f.name);
66
- }
67
- names = arr;
68
- _enumListFieldsCache.set(sf, names);
69
- return names;
70
- }
71
-
72
- /**
73
- * Parse a PostgreSQL array literal string into a JS array of strings.
74
- *
75
- * Format: `{val1,val2,"quoted,val",NULL,...}` with `""` quoting only when an
76
- * element contains commas, double-quotes, backslashes, or is the literal
77
- * `NULL`. PG uses backslash-escaping inside quoted elements.
78
- *
79
- * Returns `[]` for `{}`. Returns the input unchanged if it doesn't look like
80
- * an array literal (defensive — should never happen for an enum-array column).
81
- */
82
- function parsePgArrayLiteral(literal: string): string[] | string {
83
- if (literal.length < 2 || literal.charCodeAt(0) !== 123 /* { */) return literal;
84
- if (literal === "{}") return [];
85
-
86
- const out: string[] = [];
87
- const inner = literal.slice(1, -1);
88
- let i = 0;
89
- const len = inner.length;
90
- while (i < len) {
91
- if (inner.charCodeAt(i) === 34 /* " */) {
92
- // Quoted element — read until matching close-quote, honouring \\ and \"
93
- let s = "";
94
- i++;
95
- while (i < len) {
96
- const ch = inner.charCodeAt(i);
97
- if (ch === 92 /* \ */) {
98
- s += inner[i + 1] ?? "";
99
- i += 2;
100
- } else if (ch === 34) {
101
- i++;
102
- break;
103
- } else {
104
- s += inner[i];
105
- i++;
106
- }
107
- }
108
- out.push(s);
109
- } else {
110
- // Unquoted element — read until next comma or end
111
- let s = "";
112
- while (i < len && inner.charCodeAt(i) !== 44 /* , */) {
113
- s += inner[i];
114
- i++;
115
- }
116
- out.push(s);
117
- }
118
- if (i < len && inner.charCodeAt(i) === 44 /* , */) i++;
119
- }
120
- return out;
121
- }
122
-
123
- /**
124
- * Coerce scalar field values on the given records to their JS-native types.
125
- *
126
- * Today only `BigInt` fields are affected. Driver behaviour:
127
- * - `bun:sql` returns PG `bigint` as a JS `string`.
128
- * - `node-postgres` returns PG `bigint` as a JS `string` by default too.
129
- * - In both cases the Prisma type is `BigInt`, so we coerce.
130
- *
131
- * No-op when the model has no `BigInt` fields, or when `records` is empty.
132
- */
133
- export function coerceFieldTypes(params: {
134
- records: Record<string, unknown>[];
135
- modelMeta: ModelMeta;
136
- }): void {
137
- const { records, modelMeta } = params;
138
- if (records.length === 0) return;
139
-
140
- const bigintNames = getBigIntFieldNames(modelMeta);
141
- const enumListNames = getEnumListFieldNames(modelMeta);
142
- if (bigintNames.length === 0 && enumListNames.length === 0) return;
143
-
144
- for (const record of records) {
145
- for (const name of bigintNames) {
146
- const val = record[name];
147
- if (typeof val === "string") {
148
- record[name] = BigInt(val);
149
- } else if (typeof val === "number") {
150
- record[name] = BigInt(val);
151
- }
152
- }
153
- // Enum-array fields arrive as raw PG array literal strings from both
154
- // bun:sql and node-postgres (the driver doesn't know the user-defined
155
- // enum's array OID). Parse them into JS string arrays. Bug 2 + Bug 3.
156
- for (const name of enumListNames) {
157
- const val = record[name];
158
- if (typeof val === "string") {
159
- const parsed = parsePgArrayLiteral(val);
160
- if (Array.isArray(parsed)) record[name] = parsed;
161
- }
162
- }
163
- }
164
- }
165
-
166
- /**
167
- * True iff the model has any `BigInt` fields that require coercion.
168
- * Cheap O(1) lookup after first call per model. Hot-path callers (lateral-join
169
- * builder, relation loaders) use this to skip the per-row coercion loop
170
- * entirely for relations whose related model has no `BigInt` columns.
171
- */
172
- export function modelHasBigInt(modelMeta: ModelMeta): boolean {
173
- return getBigIntFieldNames(modelMeta).length > 0;
174
- }
175
-
176
- /**
177
- * True iff the model has any fields that need post-driver coercion
178
- * (BigInt OR enum-array). Use this in place of `modelHasBigInt` whenever
179
- * the fast-path skip would otherwise miss enum-array fields (Bug 2).
180
- */
181
- export function modelNeedsCoercion(modelMeta: ModelMeta): boolean {
182
- return getBigIntFieldNames(modelMeta).length > 0
183
- || getEnumListFieldNames(modelMeta).length > 0;
184
- }
@@ -1,152 +0,0 @@
1
- /**
2
- * Shared `_count` resolution + loader, used by both the "query" and "join"
3
- * relation strategies.
4
- *
5
- * `resolveCountSpec` looks at `include._count` / `select._count` and returns
6
- * the list of list-relation names to count (or `["__all__"]` for `_count: true`,
7
- * or `null` when not requested).
8
- *
9
- * `loadRelationCounts` issues a single grouped `COUNT(*) GROUP BY <fk>` per
10
- * relation (parallelised across relations) and attaches the result as a
11
- * `_count` object on each parent record.
12
- *
13
- * Lives in its own module so the lateral-join path can call it without
14
- * pulling in `client.ts` (which would create a circular import).
15
- */
16
-
17
- import type { ModelMeta, ModelMetaMap } from "./types.ts";
18
- import { getScalarFieldMap, getModelByNameMap, PgArray } from "./types.ts";
19
-
20
- type SqlExecutor = (params: {
21
- text: string;
22
- values: unknown[];
23
- }) => Promise<Record<string, unknown>[]>;
24
-
25
- /**
26
- * Resolve `_count` specification from `include` or `select` args.
27
- * Returns the list of relation names to count, `["__all__"]` to count every
28
- * list relation, or `null` if `_count` was not requested.
29
- */
30
- export function resolveCountSpec(params: { args: Record<string, unknown> }): string[] | null {
31
- const { args } = params;
32
- const include = args.include as Record<string, unknown> | undefined;
33
- const select = args.select as Record<string, unknown> | undefined;
34
-
35
- const countArg = include?._count ?? select?._count;
36
- if (!countArg) return null;
37
-
38
- if (countArg === true) {
39
- // Count all list relations — will be resolved by loadRelationCounts
40
- return ["__all__"];
41
- }
42
-
43
- if (typeof countArg === "object" && countArg !== null) {
44
- const countObj = countArg as Record<string, unknown>;
45
- const selectObj = countObj.select as Record<string, boolean> | undefined;
46
- if (selectObj) {
47
- return Object.entries(selectObj)
48
- .filter(([_, enabled]) => enabled)
49
- .map(([name]) => name);
50
- }
51
- }
52
-
53
- return null;
54
- }
55
-
56
- /**
57
- * Load relation counts and attach a `_count` object to each parent record.
58
- * Uses one grouped `COUNT(*)` query per relation, executed in parallel.
59
- *
60
- * Mutates the parent records in place.
61
- */
62
- export async function loadRelationCounts(params: {
63
- records: Record<string, unknown>[];
64
- modelMeta: ModelMeta;
65
- allModelsMeta: ModelMetaMap;
66
- countSpec: string[];
67
- executor: SqlExecutor;
68
- }): Promise<void> {
69
- const { records, modelMeta, allModelsMeta, countSpec, executor } = params;
70
- const modelMap = getModelByNameMap({ allModelsMeta });
71
- const parentPk = modelMeta.primaryKey[0];
72
- if (!parentPk) return;
73
-
74
- const parentIds = records.map((r) => r[parentPk]).filter((id) => id != null);
75
- if (parentIds.length === 0) return;
76
-
77
- // Resolve which relations to count
78
- const listRelations = modelMeta.relationFields.filter((r) => r.isList);
79
- const relationsToCount = countSpec.includes("__all__")
80
- ? listRelations
81
- : listRelations.filter((r) => countSpec.includes(r.name));
82
-
83
- // Initialize _count on all records
84
- for (const record of records) {
85
- const countObj: Record<string, number> = {};
86
- for (const rel of relationsToCount) {
87
- countObj[rel.name] = 0;
88
- }
89
- record._count = countObj;
90
- }
91
-
92
- // Run all relation COUNT queries in parallel — each hits a different table
93
- // so there are no data races on the parent records.
94
- await Promise.all(
95
- relationsToCount.map(async (rel) => {
96
- const relatedModelMeta = modelMap.get(rel.relatedModel);
97
- if (!relatedModelMeta) return;
98
-
99
- // M:N relation: count via join table
100
- if (rel.type === "manyToMany" && (rel as { joinTable?: string }).joinTable) {
101
- const joinTableName = (rel as { joinTable?: string }).joinTable!;
102
- const sorted = [modelMeta.name, relatedModelMeta.name].sort();
103
- const parentIsA = modelMeta.name === sorted[0];
104
- const parentCol = parentIsA ? "A" : "B";
105
-
106
- const text = `SELECT "${joinTableName}"."${parentCol}" AS "__fk", COUNT(*) AS "__count" FROM "${joinTableName}" WHERE "${joinTableName}"."${parentCol}" = ANY($1) GROUP BY "${joinTableName}"."${parentCol}"`;
107
- const result = await executor({ text, values: [new PgArray(parentIds)] });
108
-
109
- const countMap = new Map<unknown, number>();
110
- for (const row of result) {
111
- countMap.set(row.__fk, Number(row.__count ?? 0));
112
- }
113
-
114
- for (const record of records) {
115
- const pkValue = record[parentPk];
116
- const cnt = countMap.get(pkValue) ?? 0;
117
- (record._count as Record<string, number>)[rel.name] = cnt;
118
- }
119
- return;
120
- }
121
-
122
- // Find the FK column on the related model (with relationName disambiguation)
123
- const reverseRel = relatedModelMeta.relationFields.find(
124
- (r) => r.relatedModel === modelMeta.name && r.isForeignKey && r.fields.length > 0 &&
125
- (!rel.relationName || r.relationName === rel.relationName)
126
- );
127
- if (!reverseRel) return;
128
-
129
- const fkField = reverseRel.fields[0]!;
130
- const relatedSfMap = getScalarFieldMap({ scalarFields: relatedModelMeta.scalarFields });
131
- const fkScalar = relatedSfMap.get(fkField);
132
- const fkDbName = fkScalar?.dbName ?? fkField;
133
- const relatedTable = `"${relatedModelMeta.dbName}"`;
134
-
135
- // SELECT "fk" AS "__fk", COUNT(*) AS "__count" FROM "related" WHERE "fk" = ANY($1) GROUP BY "fk"
136
- const text = `SELECT ${relatedTable}."${fkDbName}" AS "__fk", COUNT(*) AS "__count" FROM ${relatedTable} WHERE ${relatedTable}."${fkDbName}" = ANY($1) GROUP BY ${relatedTable}."${fkDbName}"`;
137
- const result = await executor({ text, values: [new PgArray(parentIds)] });
138
-
139
- // Map counts back to parent records
140
- const countMap = new Map<unknown, number>();
141
- for (const row of result) {
142
- countMap.set(row.__fk, Number(row.__count ?? 0));
143
- }
144
-
145
- for (const record of records) {
146
- const pkValue = record[parentPk];
147
- const cnt = countMap.get(pkValue) ?? 0;
148
- (record._count as Record<string, number>)[rel.name] = cnt;
149
- }
150
- })
151
- );
152
- }