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.
@@ -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
+ });
@@ -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
- try {
124
- const result = await this.pool.query(this.sql, this.params);
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.78.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).",