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.
@@ -2104,17 +2104,17 @@ function makeRawSurface(exec, ph) {
2104
2104
  return {
2105
2105
  $queryRaw: async (strings, ...values) => {
2106
2106
  const { text, params } = flattenTemplate(strings, values, ph);
2107
- return (await exec(text, params)).rows;
2107
+ return (await exec(text, params, '$queryRaw')).rows;
2108
2108
  },
2109
2109
  $queryRawUnsafe: async (sql, ...params) => {
2110
- return (await exec(sql, params)).rows;
2110
+ return (await exec(sql, params, '$queryRawUnsafe')).rows;
2111
2111
  },
2112
2112
  $executeRaw: async (strings, ...values) => {
2113
2113
  const { text, params } = flattenTemplate(strings, values, ph);
2114
- return (await exec(text, params)).rowCount ?? 0;
2114
+ return (await exec(text, params, '$executeRaw')).rowCount ?? 0;
2115
2115
  },
2116
2116
  $executeRawUnsafe: async (sql, ...params) => {
2117
- return (await exec(sql, params)).rowCount ?? 0;
2117
+ return (await exec(sql, params, '$executeRawUnsafe')).rowCount ?? 0;
2118
2118
  },
2119
2119
  };
2120
2120
  }
@@ -2371,8 +2371,12 @@ function createPrismaCompatClient(client, map, options = {}) {
2371
2371
  baseDelegates.set(prismaModel, makeDelegate(ctx, mm, () => db.table(mm.table), (fn) => db.$transaction((tx) => fn((n) => tx.table(n)))));
2372
2372
  }
2373
2373
  const ph = placeholderOf(db);
2374
- const runRaw = async (text, params) => {
2374
+ // Prefer the client's own raw seam, which emits a `$on('query')` event; a
2375
+ // client without one (an older core, a test stub) runs on the pool as before.
2376
+ const runRaw = async (text, params, action) => {
2375
2377
  try {
2378
+ if (typeof db.rawQuery === 'function')
2379
+ return await db.rawQuery(text, params, action);
2376
2380
  return await poolOf(db).query(text, params);
2377
2381
  }
2378
2382
  catch (err) {
@@ -2387,14 +2391,14 @@ function createPrismaCompatClient(client, map, options = {}) {
2387
2391
  * `wrapPgError` is idempotent (it returns an already-typed TurbineError
2388
2392
  * untouched), so the error shape matches the pool path exactly.
2389
2393
  */
2390
- const txRunRaw = (tx) => async (text, params) => {
2394
+ const txRunRaw = (tx) => async (text, params, action) => {
2391
2395
  if (typeof tx.rawQuery !== 'function') {
2392
2396
  throw decorate(new errors_js_1.ValidationError('prisma-compat: raw SQL inside $transaction needs a transaction client that can execute it ' +
2393
2397
  '(core TransactionClient.rawQuery). Refusing to run the statement on a pool connection, which would ' +
2394
2398
  'silently place it outside the transaction.'), ctx.options.prismaErrorCodes);
2395
2399
  }
2396
2400
  try {
2397
- return await tx.rawQuery(text, params);
2401
+ return await tx.rawQuery(text, params, action);
2398
2402
  }
2399
2403
  catch (err) {
2400
2404
  throw decorate((0, errors_js_1.wrapPgError)(err), ctx.options.prismaErrorCodes);
@@ -93,6 +93,21 @@ export interface QueryEvent {
93
93
  * observability can see exactly which queries the auto default re-planned.
94
94
  */
95
95
  strategy?: 'auto-batched';
96
+ /**
97
+ * The label of the innermost `db.$tag(label, fn)` scope the query ran in.
98
+ * Absent outside any scope. Lets a listener attribute a query to the feature
99
+ * or code path that issued it, which `model` + `action` cannot.
100
+ */
101
+ tag?: string;
102
+ /**
103
+ * Set on statements that ran as part of a batch rather than on their own:
104
+ * `'pipeline'` for `db.pipeline(...)`, `'transaction'` for the array form
105
+ * `$transaction([...])`. A pipeline sends every statement in one round trip,
106
+ * so its per-statement `duration` is the batch's wall time divided evenly
107
+ * across the statements (the sum is exact, the split is not). Transaction
108
+ * batch statements are timed individually. Absent for everything else.
109
+ */
110
+ batch?: 'pipeline' | 'transaction';
96
111
  }
97
112
  export type QueryEventListener = (event: QueryEvent) => void;
98
113
  /** Options passed from TurbineClient to QueryInterface */
@@ -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,137 @@
1
+ "use strict";
2
+ /**
3
+ * turbine-orm, `$on('query')` event metadata: the raw-statement model name
4
+ * and per-scope query tags for attribution.
5
+ *
6
+ * A tag is a caller-chosen label (`'checkout'`, `'nightly-report'`) that rides
7
+ * on every {@link QueryEvent} emitted while a `db.$tag(label, fn)` callback is
8
+ * running, including queries issued from inside awaited helpers, transactions
9
+ * and pipelines. It exists so telemetry can answer "which feature issued this
10
+ * query", which model + action alone cannot: `orders.findMany` is called from
11
+ * a dozen places.
12
+ *
13
+ * The tag is scoped by async context rather than threaded through every query
14
+ * argument type. A per-query option would have to be added to every args type,
15
+ * the runtime option surface, and the prisma-compat unknown-option check, and
16
+ * it would still miss raw SQL and pipelines. A scope reaches every emitter with
17
+ * one read at the single emit seam in client.ts, and it costs nothing when no
18
+ * `$on('query')` listener is registered, because the store is only read on the
19
+ * emit path.
20
+ *
21
+ * ONE scope per realm. The store hangs off `globalThis` under a
22
+ * `Symbol.for(...)` key for the same reason the warn-once registry does: the
23
+ * package ships ESM and CJS, and a mixed graph loading both copies must still
24
+ * see the tag a caller set through either copy.
25
+ *
26
+ * The tag never reaches SQL. It is metadata on the in-process event only; what
27
+ * a listener does with it (log it, forward it to a collector) is the
28
+ * listener's business.
29
+ */
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.rawIdentity = exports.MAX_QUERY_TAG_LENGTH = exports.RAW_QUERY_MODEL = void 0;
32
+ exports.currentQueryTag = currentQueryTag;
33
+ exports.runWithQueryTag = runWithQueryTag;
34
+ exports.resultRowCount = resultRowCount;
35
+ exports.emitStatementEvent = emitStatementEvent;
36
+ exports.deferredIdentity = deferredIdentity;
37
+ exports.runReportedStatement = runReportedStatement;
38
+ const node_async_hooks_1 = require("node:async_hooks");
39
+ const errors_js_1 = require("./errors.js");
40
+ /**
41
+ * The `model` reported for statements that are not tied to one table: `raw`,
42
+ * `sql`, the transaction-scoped `raw` / `rawQuery`, and prisma-compat's
43
+ * `$queryRaw` / `$executeRaw` family. `action` names the entry point that ran
44
+ * it. The `$` prefix cannot collide with a generated table accessor.
45
+ */
46
+ exports.RAW_QUERY_MODEL = '$raw';
47
+ /** Longest tag accepted. Keeps a tag a label, not a payload. */
48
+ exports.MAX_QUERY_TAG_LENGTH = 128;
49
+ const SCOPE_KEY = Symbol.for('turbine.queryTag.scope');
50
+ function scope() {
51
+ const g = globalThis;
52
+ let store = g[SCOPE_KEY];
53
+ if (!store) {
54
+ store = new node_async_hooks_1.AsyncLocalStorage();
55
+ g[SCOPE_KEY] = store;
56
+ }
57
+ return store;
58
+ }
59
+ /** The tag in force for the code currently running, or `undefined`. */
60
+ function currentQueryTag() {
61
+ return scope().getStore();
62
+ }
63
+ /**
64
+ * Run `fn` with `tag` attached to every query event it causes. An inner scope
65
+ * replaces an outer one for its own duration. Throws {@link ValidationError}
66
+ * (E003) for a tag that is not a non-empty string of at most
67
+ * {@link MAX_QUERY_TAG_LENGTH} characters, before `fn` runs.
68
+ */
69
+ function runWithQueryTag(tag, fn) {
70
+ if (typeof tag !== 'string' || !tag.trim() || tag.length > exports.MAX_QUERY_TAG_LENGTH) {
71
+ throw new errors_js_1.ValidationError(`$tag() needs a label of 1-${exports.MAX_QUERY_TAG_LENGTH} characters.`);
72
+ }
73
+ return scope().run(tag, fn);
74
+ }
75
+ /** Rows a driver result reports: the affected count, else the returned rows. */
76
+ function resultRowCount(result) {
77
+ return typeof result?.rowCount === 'number' ? result.rowCount : (result?.rows?.length ?? 0);
78
+ }
79
+ /**
80
+ * Emit one query event for a statement that ran outside a QueryInterface (raw
81
+ * SQL, a pipeline slot, a transaction-batch slot). Never throws: a listener
82
+ * problem must not turn a successful statement into a failed one.
83
+ */
84
+ function emitStatementEvent(sink, event) {
85
+ try {
86
+ sink?.({ ...event, timestamp: new Date() });
87
+ }
88
+ catch {
89
+ // Listener errors must never crash a query.
90
+ }
91
+ }
92
+ /**
93
+ * Split a DeferredQuery tag (`'<table>.<action>'`) into the event's model and
94
+ * action. A tag without one is reported as a raw statement.
95
+ */
96
+ function deferredIdentity(tag) {
97
+ const at = tag.lastIndexOf('.');
98
+ return at > 0 ? { model: tag.slice(0, at), action: tag.slice(at + 1) } : { model: exports.RAW_QUERY_MODEL, action: tag };
99
+ }
100
+ /**
101
+ * Run one statement and report it. `identity` is the event's model and action
102
+ * (plus `batch` for a batch slot); for raw SQL the model is
103
+ * {@link RAW_QUERY_MODEL} and the action names the entry point (`'raw'`,
104
+ * `'sql'`, `'rawQuery'`, `'$queryRaw'` ...). `wrap` turns a driver error into
105
+ * what the caller sees, and the event carries that same error.
106
+ */
107
+ async function runReportedStatement(sink, identity, sql, params, run, wrap) {
108
+ const start = performance.now();
109
+ let result;
110
+ let error;
111
+ try {
112
+ result = await run();
113
+ return result;
114
+ }
115
+ catch (err) {
116
+ error = wrap(err);
117
+ throw error;
118
+ }
119
+ finally {
120
+ if (sink) {
121
+ emitStatementEvent(sink, {
122
+ ...identity,
123
+ sql,
124
+ params,
125
+ duration: performance.now() - start,
126
+ rows: resultRowCount(result),
127
+ ...(error === undefined ? {} : { error: error instanceof Error ? error : new Error(String(error)) }),
128
+ });
129
+ }
130
+ }
131
+ }
132
+ /** The identity of a raw statement run through `action`. */
133
+ const rawIdentity = (action) => ({
134
+ model: exports.RAW_QUERY_MODEL,
135
+ action,
136
+ });
137
+ exports.rawIdentity = rawIdentity;
@@ -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
  /**
@@ -53,6 +53,7 @@ exports.TypedSqlQuery = void 0;
53
53
  exports.buildTypedSql = buildTypedSql;
54
54
  const dialect_js_1 = require("./dialect.js");
55
55
  const errors_js_1 = require("./errors.js");
56
+ const query_events_js_1 = require("./query-events.js");
56
57
  /**
57
58
  * Build a `(sql, params)` pair from a tagged-template invocation.
58
59
  *
@@ -97,6 +98,7 @@ class TypedSqlQuery {
97
98
  params;
98
99
  logging;
99
100
  runScoped;
101
+ onQuery;
100
102
  constructor(pool, sql, params, logging,
101
103
  /**
102
104
  * Runs the execution under the owning client's `errorMessages` mode. Passed
@@ -111,12 +113,19 @@ class TypedSqlQuery {
111
113
  * Defaults to calling through, so a `TypedSqlQuery` built outside a client
112
114
  * behaves exactly as before.
113
115
  */
114
- runScoped = (fn) => fn()) {
116
+ runScoped = (fn) => fn(),
117
+ /**
118
+ * The owning client's `$on('query')` sink. Each execution emits one event
119
+ * with model `'$raw'` and action `'sql'`. Absent for a `TypedSqlQuery`
120
+ * built outside a client, which then emits nothing, as before.
121
+ */
122
+ onQuery) {
115
123
  this.pool = pool;
116
124
  this.sql = sql;
117
125
  this.params = params;
118
126
  this.logging = logging;
119
127
  this.runScoped = runScoped;
128
+ this.onQuery = onQuery;
120
129
  }
121
130
  /** Execute and return all rows. Internal; powers `then`, `one`, and `scalar`. */
122
131
  run() {
@@ -124,13 +133,8 @@ class TypedSqlQuery {
124
133
  if (this.logging) {
125
134
  console.log(`[turbine] Typed SQL: ${this.sql.trim().substring(0, 120)}...`);
126
135
  }
127
- try {
128
- const result = await this.pool.query(this.sql, this.params);
129
- return result.rows;
130
- }
131
- catch (err) {
132
- throw (0, errors_js_1.wrapPgError)(err);
133
- }
136
+ const result = await (0, query_events_js_1.runReportedStatement)(this.onQuery, (0, query_events_js_1.rawIdentity)('sql'), this.sql, this.params, () => this.pool.query(this.sql, this.params), errors_js_1.wrapPgError);
137
+ return result.rows;
134
138
  });
135
139
  }
136
140
  /**
package/dist/client.d.ts CHANGED
@@ -648,9 +648,11 @@ export declare class TransactionClient {
648
648
  * inside the same BEGIN/COMMIT (and any active SAVEPOINT) as every other
649
649
  * statement in the callback. Driver errors are translated by `wrapPgError`,
650
650
  * exactly as pool-scoped and table-scoped queries are. Like {@link raw}, it
651
- * emits no `$on('query')` event and runs no middleware.
651
+ * runs no middleware, and it emits one `$on('query')` event with model
652
+ * `'$raw'` and `action` set to the optional `action` label (the adapter
653
+ * passes `'$queryRaw'` / `'$executeRaw'` ...; default `'rawQuery'`).
652
654
  */
653
- rawQuery<T extends object = Record<string, unknown>>(text: string, params?: readonly unknown[]): Promise<{
655
+ rawQuery<T extends object = Record<string, unknown>>(text: string, params?: readonly unknown[], action?: string): Promise<{
654
656
  rows: T[];
655
657
  rowCount: number | null;
656
658
  }>;
@@ -986,6 +988,16 @@ export declare class TurbineClient {
986
988
  * On HTTP-based drivers it falls back to sequential execution.
987
989
  */
988
990
  pipeline<T extends readonly DeferredQuery<unknown>[]>(...args: T | [T, PipelineOptions?]): Promise<PipelineResults<T>>;
991
+ /**
992
+ * {@link pipeline} with one `$on('query')` event per statement. Only taken
993
+ * when a listener is registered, so an unobserved pipeline runs exactly as
994
+ * before. Each statement's `transform` is wrapped to capture the driver's
995
+ * row count on the way through (the pipeline returns transformed values, not
996
+ * driver results). The statements share one round trip, so the batch's wall
997
+ * time is split evenly across them and each event carries
998
+ * `batch: 'pipeline'`.
999
+ */
1000
+ private reportedPipeline;
989
1001
  /**
990
1002
  * Check whether the underlying pool supports the real pipeline protocol.
991
1003
  * Returns `true` for standard pg.Pool TCP connections, `false` for HTTP
@@ -1005,6 +1017,39 @@ export declare class TurbineClient {
1005
1017
  * ```
1006
1018
  */
1007
1019
  raw<T extends Record<string, unknown> = Record<string, unknown>>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T[]>;
1020
+ /**
1021
+ * @internal The `turbine-orm/prisma-compat` adapter's pool-level raw seam,
1022
+ * the twin of {@link TransactionClient.rawQuery}. NOT application API: use
1023
+ * {@link raw} or {@link sql}, whose tagged templates make concatenating a
1024
+ * value into the SQL text impossible. It exists so the adapter's
1025
+ * `$queryRaw` / `$executeRaw` statements emit a `$on('query')` event (model
1026
+ * `'$raw'`, `action` the adapter's label) instead of running unobserved on
1027
+ * the pool. Errors are translated by `wrapPgError`, like {@link raw}.
1028
+ */
1029
+ rawQuery<T extends object = Record<string, unknown>>(text: string, params?: readonly unknown[], action?: string): Promise<{
1030
+ rows: T[];
1031
+ rowCount: number | null;
1032
+ }>;
1033
+ /**
1034
+ * Run `fn` with `tag` attached to every `$on('query')` event it causes, so a
1035
+ * listener can attribute queries to the feature or code path that issued
1036
+ * them. The scope follows async context: queries in awaited helpers,
1037
+ * transactions, pipelines and raw SQL inside `fn` are all tagged. An inner
1038
+ * `$tag` replaces an outer one for its own duration. The tag is event
1039
+ * metadata only and never reaches SQL.
1040
+ *
1041
+ * Throws {@link ValidationError} (E003) for an empty tag or one longer than
1042
+ * {@link MAX_QUERY_TAG_LENGTH} (128) characters.
1043
+ *
1044
+ * @example
1045
+ * ```ts
1046
+ * const order = await db.$tag('checkout', async () => {
1047
+ * const cart = await db.carts.findUnique({ where: { id } });
1048
+ * return db.orders.create({ data: { cartId: cart.id } });
1049
+ * });
1050
+ * ```
1051
+ */
1052
+ $tag<R>(tag: string, fn: () => R): R;
1008
1053
  /**
1009
1054
  * Execute a **typed** raw SQL query, Turbine's answer to Prisma's TypedSQL.
1010
1055
  *