turbine-orm 0.78.0 → 0.79.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/dist/cjs/client.d.ts +47 -2
- package/dist/cjs/client.js +122 -41
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +5 -1
- package/dist/cjs/powql.d.ts +32 -3
- package/dist/cjs/powql.js +96 -20
- package/dist/cjs/prisma-compat.d.ts +1 -1
- package/dist/cjs/prisma-compat.js +11 -7
- package/dist/cjs/query/deferred.d.ts +15 -0
- package/dist/cjs/query-events.d.ts +82 -0
- package/dist/cjs/query-events.js +137 -0
- package/dist/cjs/typed-sql.d.ts +14 -1
- package/dist/cjs/typed-sql.js +12 -8
- package/dist/client.d.ts +47 -2
- package/dist/client.js +123 -42
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/powql.d.ts +32 -3
- package/dist/powql.js +96 -20
- package/dist/prisma-compat.d.ts +1 -1
- package/dist/prisma-compat.js +11 -7
- package/dist/query/deferred.d.ts +15 -0
- package/dist/query-events.d.ts +82 -0
- package/dist/query-events.js +127 -0
- package/dist/typed-sql.d.ts +14 -1
- package/dist/typed-sql.js +12 -8
- package/package.json +1 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm, `$on('query')` event metadata: the raw-statement model name
|
|
3
|
+
* and per-scope query tags for attribution.
|
|
4
|
+
*
|
|
5
|
+
* A tag is a caller-chosen label (`'checkout'`, `'nightly-report'`) that rides
|
|
6
|
+
* on every {@link QueryEvent} emitted while a `db.$tag(label, fn)` callback is
|
|
7
|
+
* running, including queries issued from inside awaited helpers, transactions
|
|
8
|
+
* and pipelines. It exists so telemetry can answer "which feature issued this
|
|
9
|
+
* query", which model + action alone cannot: `orders.findMany` is called from
|
|
10
|
+
* a dozen places.
|
|
11
|
+
*
|
|
12
|
+
* The tag is scoped by async context rather than threaded through every query
|
|
13
|
+
* argument type. A per-query option would have to be added to every args type,
|
|
14
|
+
* the runtime option surface, and the prisma-compat unknown-option check, and
|
|
15
|
+
* it would still miss raw SQL and pipelines. A scope reaches every emitter with
|
|
16
|
+
* one read at the single emit seam in client.ts, and it costs nothing when no
|
|
17
|
+
* `$on('query')` listener is registered, because the store is only read on the
|
|
18
|
+
* emit path.
|
|
19
|
+
*
|
|
20
|
+
* ONE scope per realm. The store hangs off `globalThis` under a
|
|
21
|
+
* `Symbol.for(...)` key for the same reason the warn-once registry does: the
|
|
22
|
+
* package ships ESM and CJS, and a mixed graph loading both copies must still
|
|
23
|
+
* see the tag a caller set through either copy.
|
|
24
|
+
*
|
|
25
|
+
* The tag never reaches SQL. It is metadata on the in-process event only; what
|
|
26
|
+
* a listener does with it (log it, forward it to a collector) is the
|
|
27
|
+
* listener's business.
|
|
28
|
+
*/
|
|
29
|
+
import type { QueryEvent } from './query/deferred.js';
|
|
30
|
+
/**
|
|
31
|
+
* The `model` reported for statements that are not tied to one table: `raw`,
|
|
32
|
+
* `sql`, the transaction-scoped `raw` / `rawQuery`, and prisma-compat's
|
|
33
|
+
* `$queryRaw` / `$executeRaw` family. `action` names the entry point that ran
|
|
34
|
+
* it. The `$` prefix cannot collide with a generated table accessor.
|
|
35
|
+
*/
|
|
36
|
+
export declare const RAW_QUERY_MODEL = "$raw";
|
|
37
|
+
/** Longest tag accepted. Keeps a tag a label, not a payload. */
|
|
38
|
+
export declare const MAX_QUERY_TAG_LENGTH = 128;
|
|
39
|
+
/** The tag in force for the code currently running, or `undefined`. */
|
|
40
|
+
export declare function currentQueryTag(): string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Run `fn` with `tag` attached to every query event it causes. An inner scope
|
|
43
|
+
* replaces an outer one for its own duration. Throws {@link ValidationError}
|
|
44
|
+
* (E003) for a tag that is not a non-empty string of at most
|
|
45
|
+
* {@link MAX_QUERY_TAG_LENGTH} characters, before `fn` runs.
|
|
46
|
+
*/
|
|
47
|
+
export declare function runWithQueryTag<R>(tag: string, fn: () => R): R;
|
|
48
|
+
/** A client's event sink, as QueryInterfaceOptions carries it. */
|
|
49
|
+
type QueryEventSink = ((event: QueryEvent) => void) | undefined;
|
|
50
|
+
/** Rows a driver result reports: the affected count, else the returned rows. */
|
|
51
|
+
export declare function resultRowCount(result: {
|
|
52
|
+
rowCount?: number | null;
|
|
53
|
+
rows?: unknown[];
|
|
54
|
+
} | undefined): number;
|
|
55
|
+
/**
|
|
56
|
+
* Emit one query event for a statement that ran outside a QueryInterface (raw
|
|
57
|
+
* SQL, a pipeline slot, a transaction-batch slot). Never throws: a listener
|
|
58
|
+
* problem must not turn a successful statement into a failed one.
|
|
59
|
+
*/
|
|
60
|
+
export declare function emitStatementEvent(sink: QueryEventSink, event: Omit<QueryEvent, 'timestamp'>): void;
|
|
61
|
+
/**
|
|
62
|
+
* Split a DeferredQuery tag (`'<table>.<action>'`) into the event's model and
|
|
63
|
+
* action. A tag without one is reported as a raw statement.
|
|
64
|
+
*/
|
|
65
|
+
export declare function deferredIdentity(tag: string): {
|
|
66
|
+
model: string;
|
|
67
|
+
action: string;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Run one statement and report it. `identity` is the event's model and action
|
|
71
|
+
* (plus `batch` for a batch slot); for raw SQL the model is
|
|
72
|
+
* {@link RAW_QUERY_MODEL} and the action names the entry point (`'raw'`,
|
|
73
|
+
* `'sql'`, `'rawQuery'`, `'$queryRaw'` ...). `wrap` turns a driver error into
|
|
74
|
+
* what the caller sees, and the event carries that same error.
|
|
75
|
+
*/
|
|
76
|
+
export declare function runReportedStatement<R extends {
|
|
77
|
+
rowCount?: number | null;
|
|
78
|
+
rows?: unknown[];
|
|
79
|
+
}>(sink: QueryEventSink, identity: Pick<QueryEvent, 'model' | 'action' | 'batch'>, sql: string, params: unknown[], run: () => Promise<R>, wrap: (err: unknown) => unknown): Promise<R>;
|
|
80
|
+
/** The identity of a raw statement run through `action`. */
|
|
81
|
+
export declare const rawIdentity: (action: string) => Pick<QueryEvent, "model" | "action">;
|
|
82
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm, `$on('query')` event metadata: the raw-statement model name
|
|
3
|
+
* and per-scope query tags for attribution.
|
|
4
|
+
*
|
|
5
|
+
* A tag is a caller-chosen label (`'checkout'`, `'nightly-report'`) that rides
|
|
6
|
+
* on every {@link QueryEvent} emitted while a `db.$tag(label, fn)` callback is
|
|
7
|
+
* running, including queries issued from inside awaited helpers, transactions
|
|
8
|
+
* and pipelines. It exists so telemetry can answer "which feature issued this
|
|
9
|
+
* query", which model + action alone cannot: `orders.findMany` is called from
|
|
10
|
+
* a dozen places.
|
|
11
|
+
*
|
|
12
|
+
* The tag is scoped by async context rather than threaded through every query
|
|
13
|
+
* argument type. A per-query option would have to be added to every args type,
|
|
14
|
+
* the runtime option surface, and the prisma-compat unknown-option check, and
|
|
15
|
+
* it would still miss raw SQL and pipelines. A scope reaches every emitter with
|
|
16
|
+
* one read at the single emit seam in client.ts, and it costs nothing when no
|
|
17
|
+
* `$on('query')` listener is registered, because the store is only read on the
|
|
18
|
+
* emit path.
|
|
19
|
+
*
|
|
20
|
+
* ONE scope per realm. The store hangs off `globalThis` under a
|
|
21
|
+
* `Symbol.for(...)` key for the same reason the warn-once registry does: the
|
|
22
|
+
* package ships ESM and CJS, and a mixed graph loading both copies must still
|
|
23
|
+
* see the tag a caller set through either copy.
|
|
24
|
+
*
|
|
25
|
+
* The tag never reaches SQL. It is metadata on the in-process event only; what
|
|
26
|
+
* a listener does with it (log it, forward it to a collector) is the
|
|
27
|
+
* listener's business.
|
|
28
|
+
*/
|
|
29
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
30
|
+
import { ValidationError } from './errors.js';
|
|
31
|
+
/**
|
|
32
|
+
* The `model` reported for statements that are not tied to one table: `raw`,
|
|
33
|
+
* `sql`, the transaction-scoped `raw` / `rawQuery`, and prisma-compat's
|
|
34
|
+
* `$queryRaw` / `$executeRaw` family. `action` names the entry point that ran
|
|
35
|
+
* it. The `$` prefix cannot collide with a generated table accessor.
|
|
36
|
+
*/
|
|
37
|
+
export const RAW_QUERY_MODEL = '$raw';
|
|
38
|
+
/** Longest tag accepted. Keeps a tag a label, not a payload. */
|
|
39
|
+
export const MAX_QUERY_TAG_LENGTH = 128;
|
|
40
|
+
const SCOPE_KEY = Symbol.for('turbine.queryTag.scope');
|
|
41
|
+
function scope() {
|
|
42
|
+
const g = globalThis;
|
|
43
|
+
let store = g[SCOPE_KEY];
|
|
44
|
+
if (!store) {
|
|
45
|
+
store = new AsyncLocalStorage();
|
|
46
|
+
g[SCOPE_KEY] = store;
|
|
47
|
+
}
|
|
48
|
+
return store;
|
|
49
|
+
}
|
|
50
|
+
/** The tag in force for the code currently running, or `undefined`. */
|
|
51
|
+
export function currentQueryTag() {
|
|
52
|
+
return scope().getStore();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Run `fn` with `tag` attached to every query event it causes. An inner scope
|
|
56
|
+
* replaces an outer one for its own duration. Throws {@link ValidationError}
|
|
57
|
+
* (E003) for a tag that is not a non-empty string of at most
|
|
58
|
+
* {@link MAX_QUERY_TAG_LENGTH} characters, before `fn` runs.
|
|
59
|
+
*/
|
|
60
|
+
export function runWithQueryTag(tag, fn) {
|
|
61
|
+
if (typeof tag !== 'string' || !tag.trim() || tag.length > MAX_QUERY_TAG_LENGTH) {
|
|
62
|
+
throw new ValidationError(`$tag() needs a label of 1-${MAX_QUERY_TAG_LENGTH} characters.`);
|
|
63
|
+
}
|
|
64
|
+
return scope().run(tag, fn);
|
|
65
|
+
}
|
|
66
|
+
/** Rows a driver result reports: the affected count, else the returned rows. */
|
|
67
|
+
export function resultRowCount(result) {
|
|
68
|
+
return typeof result?.rowCount === 'number' ? result.rowCount : (result?.rows?.length ?? 0);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Emit one query event for a statement that ran outside a QueryInterface (raw
|
|
72
|
+
* SQL, a pipeline slot, a transaction-batch slot). Never throws: a listener
|
|
73
|
+
* problem must not turn a successful statement into a failed one.
|
|
74
|
+
*/
|
|
75
|
+
export function emitStatementEvent(sink, event) {
|
|
76
|
+
try {
|
|
77
|
+
sink?.({ ...event, timestamp: new Date() });
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Listener errors must never crash a query.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Split a DeferredQuery tag (`'<table>.<action>'`) into the event's model and
|
|
85
|
+
* action. A tag without one is reported as a raw statement.
|
|
86
|
+
*/
|
|
87
|
+
export function deferredIdentity(tag) {
|
|
88
|
+
const at = tag.lastIndexOf('.');
|
|
89
|
+
return at > 0 ? { model: tag.slice(0, at), action: tag.slice(at + 1) } : { model: RAW_QUERY_MODEL, action: tag };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Run one statement and report it. `identity` is the event's model and action
|
|
93
|
+
* (plus `batch` for a batch slot); for raw SQL the model is
|
|
94
|
+
* {@link RAW_QUERY_MODEL} and the action names the entry point (`'raw'`,
|
|
95
|
+
* `'sql'`, `'rawQuery'`, `'$queryRaw'` ...). `wrap` turns a driver error into
|
|
96
|
+
* what the caller sees, and the event carries that same error.
|
|
97
|
+
*/
|
|
98
|
+
export async function runReportedStatement(sink, identity, sql, params, run, wrap) {
|
|
99
|
+
const start = performance.now();
|
|
100
|
+
let result;
|
|
101
|
+
let error;
|
|
102
|
+
try {
|
|
103
|
+
result = await run();
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
error = wrap(err);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
if (sink) {
|
|
112
|
+
emitStatementEvent(sink, {
|
|
113
|
+
...identity,
|
|
114
|
+
sql,
|
|
115
|
+
params,
|
|
116
|
+
duration: performance.now() - start,
|
|
117
|
+
rows: resultRowCount(result),
|
|
118
|
+
...(error === undefined ? {} : { error: error instanceof Error ? error : new Error(String(error)) }),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** The identity of a raw statement run through `action`. */
|
|
124
|
+
export const rawIdentity = (action) => ({
|
|
125
|
+
model: RAW_QUERY_MODEL,
|
|
126
|
+
action,
|
|
127
|
+
});
|
package/dist/typed-sql.d.ts
CHANGED
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
*/
|
|
50
50
|
import type { PgCompatPool } from './client.js';
|
|
51
51
|
import { type Dialect } from './dialect.js';
|
|
52
|
+
import type { QueryEvent } from './query/deferred.js';
|
|
52
53
|
/**
|
|
53
54
|
* Build a `(sql, params)` pair from a tagged-template invocation.
|
|
54
55
|
*
|
|
@@ -93,6 +94,12 @@ export declare class TypedSqlQuery<T extends Record<string, unknown>> implements
|
|
|
93
94
|
* behaves exactly as before.
|
|
94
95
|
*/
|
|
95
96
|
private readonly runScoped;
|
|
97
|
+
/**
|
|
98
|
+
* The owning client's `$on('query')` sink. Each execution emits one event
|
|
99
|
+
* with model `'$raw'` and action `'sql'`. Absent for a `TypedSqlQuery`
|
|
100
|
+
* built outside a client, which then emits nothing, as before.
|
|
101
|
+
*/
|
|
102
|
+
private readonly onQuery?;
|
|
96
103
|
constructor(pool: PgCompatPool, sql: string, params: unknown[], logging: boolean,
|
|
97
104
|
/**
|
|
98
105
|
* Runs the execution under the owning client's `errorMessages` mode. Passed
|
|
@@ -107,7 +114,13 @@ export declare class TypedSqlQuery<T extends Record<string, unknown>> implements
|
|
|
107
114
|
* Defaults to calling through, so a `TypedSqlQuery` built outside a client
|
|
108
115
|
* behaves exactly as before.
|
|
109
116
|
*/
|
|
110
|
-
runScoped?: <R>(fn: () => R) => R
|
|
117
|
+
runScoped?: <R>(fn: () => R) => R,
|
|
118
|
+
/**
|
|
119
|
+
* The owning client's `$on('query')` sink. Each execution emits one event
|
|
120
|
+
* with model `'$raw'` and action `'sql'`. Absent for a `TypedSqlQuery`
|
|
121
|
+
* built outside a client, which then emits nothing, as before.
|
|
122
|
+
*/
|
|
123
|
+
onQuery?: ((event: QueryEvent) => void) | undefined);
|
|
111
124
|
/** Execute and return all rows. Internal; powers `then`, `one`, and `scalar`. */
|
|
112
125
|
private run;
|
|
113
126
|
/**
|
package/dist/typed-sql.js
CHANGED
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
*/
|
|
50
50
|
import { postgresDialect } from './dialect.js';
|
|
51
51
|
import { ValidationError, wrapPgError } from './errors.js';
|
|
52
|
+
import { rawIdentity, runReportedStatement } from './query-events.js';
|
|
52
53
|
/**
|
|
53
54
|
* Build a `(sql, params)` pair from a tagged-template invocation.
|
|
54
55
|
*
|
|
@@ -93,6 +94,7 @@ export class TypedSqlQuery {
|
|
|
93
94
|
params;
|
|
94
95
|
logging;
|
|
95
96
|
runScoped;
|
|
97
|
+
onQuery;
|
|
96
98
|
constructor(pool, sql, params, logging,
|
|
97
99
|
/**
|
|
98
100
|
* Runs the execution under the owning client's `errorMessages` mode. Passed
|
|
@@ -107,12 +109,19 @@ export class TypedSqlQuery {
|
|
|
107
109
|
* Defaults to calling through, so a `TypedSqlQuery` built outside a client
|
|
108
110
|
* behaves exactly as before.
|
|
109
111
|
*/
|
|
110
|
-
runScoped = (fn) => fn()
|
|
112
|
+
runScoped = (fn) => fn(),
|
|
113
|
+
/**
|
|
114
|
+
* The owning client's `$on('query')` sink. Each execution emits one event
|
|
115
|
+
* with model `'$raw'` and action `'sql'`. Absent for a `TypedSqlQuery`
|
|
116
|
+
* built outside a client, which then emits nothing, as before.
|
|
117
|
+
*/
|
|
118
|
+
onQuery) {
|
|
111
119
|
this.pool = pool;
|
|
112
120
|
this.sql = sql;
|
|
113
121
|
this.params = params;
|
|
114
122
|
this.logging = logging;
|
|
115
123
|
this.runScoped = runScoped;
|
|
124
|
+
this.onQuery = onQuery;
|
|
116
125
|
}
|
|
117
126
|
/** Execute and return all rows. Internal; powers `then`, `one`, and `scalar`. */
|
|
118
127
|
run() {
|
|
@@ -120,13 +129,8 @@ export class TypedSqlQuery {
|
|
|
120
129
|
if (this.logging) {
|
|
121
130
|
console.log(`[turbine] Typed SQL: ${this.sql.trim().substring(0, 120)}...`);
|
|
122
131
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return result.rows;
|
|
126
|
-
}
|
|
127
|
-
catch (err) {
|
|
128
|
-
throw wrapPgError(err);
|
|
129
|
-
}
|
|
132
|
+
const result = await runReportedStatement(this.onQuery, rawIdentity('sql'), this.sql, this.params, () => this.pool.query(this.sql, this.params), wrapPgError);
|
|
133
|
+
return result.rows;
|
|
130
134
|
});
|
|
131
135
|
}
|
|
132
136
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.0",
|
|
4
4
|
"description": "Postgres-native TypeScript ORM, runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"//exports": "Each subpath declares its types PER CONDITION. A single shared top-level \"types\" resolves to the ESM declarations for `require` too, which is TS1479 (\"is an ES module ... cannot be require()d\") for any CJS consumer on moduleResolution node16/nodenext. The require condition points at dist/cjs, which ships its own {\"type\":\"commonjs\"} package.json, so those declarations are CJS declarations. Gated in CI by publint + @arethetypeswrong/cli + a real .cts consumer typecheck (see the package-types job in ci.yml).",
|