turbine-orm 0.74.0 → 0.75.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.
@@ -54,6 +54,7 @@
54
54
  */
55
55
  import { type PgCompatPool, type PgCompatPoolClient, TurbineClient, type TurbineConfig } from './client.js';
56
56
  import { type Dialect } from './dialect.js';
57
+ import type { EngineClientConfig } from './engine-config.js';
57
58
  import type { PowdbExec } from './powdb-introspect.js';
58
59
  import { type ColumnMetadata, type SchemaMetadata, type TableMetadata } from './schema.js';
59
60
  /**
@@ -932,8 +933,22 @@ export declare class PowdbEmbeddedPool implements PgCompatPool {
932
933
  }
933
934
  export { introspectPowdbDatabase, type PowdbExec, type PowdbIntrospectOptions, } from './powdb-introspect.js';
934
935
  export { PowqlInterface } from './powql.js';
935
- /** Options for {@link turbinePowDB}. */
936
- export interface TurbinePowdbOptions extends Pick<TurbineConfig, 'logging' | 'defaultLimit' | 'warnOnUnlimited' | 'relationLoadStrategy'> {
936
+ /**
937
+ * Options for {@link turbinePowDB}.
938
+ *
939
+ * Extends {@link EngineClientConfig}, the same inverted default the SQL engine
940
+ * factories use: every `TurbineConfig` option reaches the client unless it is one
941
+ * of the transport keys declared below, which {@link turbinePowDB} destructures
942
+ * out before spreading. This used to be a four-key `Pick`, so `globalFilters`,
943
+ * `stableRelationOrder`, `errorMessages` and everything else added after PowDB
944
+ * landed were accepted by the type and then dropped on the floor. A tenant filter
945
+ * that silently does not apply is the worst shape that class of bug can take.
946
+ *
947
+ * Options that PowDB cannot honour by CAPABILITY (rather than by plumbing) still
948
+ * throw a typed `UnsupportedFeatureError` (E017) at the point of use, which is a
949
+ * better answer than an option the type system claims does not exist.
950
+ */
951
+ export interface TurbinePowdbOptions extends EngineClientConfig {
937
952
  /**
938
953
  * Client-level default `with`-relation load strategy. On PowDB the default is
939
954
  * the batched N+1 loaders; setting `'join'` opts INTO native PowQL server-side
package/dist/cjs/powdb.js CHANGED
@@ -2408,14 +2408,16 @@ async function turbinePowDB(target, schema, options = {}) {
2408
2408
  // The PowQL generator is loaded here to keep client.ts free of any PowDB import.
2409
2409
  const { PowqlInterface } = await Promise.resolve().then(() => __importStar(require('./powql.js')));
2410
2410
  const queryInterfaceFactory = (p, table, sch, middlewares, opts) => new PowqlInterface(p, table, sch, middlewares, opts);
2411
+ // Forward every client-level option EXCEPT the ones this factory owns (they
2412
+ // configure the pool / transport above, and TurbineClient warns on a config
2413
+ // key it does not recognize). Destructured rather than hand-listed on the way
2414
+ // IN: an allowlist is how `globalFilters` came to be accepted and ignored.
2415
+ const { connectionLimit: _connectionLimit, transactionQueueTimeoutMs: _transactionQueueTimeoutMs, retryStaleReads: _retryStaleReads, assumeEngineVersion: _assumeEngineVersion, readonly: _readonly, powdbClientModule: _powdbClientModule, powdbEmbeddedModule: _powdbEmbeddedModule, ...clientConfig } = options;
2411
2416
  const client = new client_js_1.TurbineClient({
2417
+ ...clientConfig,
2412
2418
  pool,
2413
2419
  preparedStatements: false,
2414
2420
  dialect: exports.powdbDialect,
2415
- logging: options.logging,
2416
- defaultLimit: options.defaultLimit,
2417
- warnOnUnlimited: options.warnOnUnlimited,
2418
- relationLoadStrategy: options.relationLoadStrategy,
2419
2421
  queryInterfaceFactory,
2420
2422
  }, schema);
2421
2423
  if (owns) {
package/dist/cjs/powql.js CHANGED
@@ -3113,17 +3113,56 @@ class PowqlInterface {
3113
3113
  const aggOrderExprs = new Map();
3114
3114
  const aggInner = new Map();
3115
3115
  let aggN = 0;
3116
- // Parity with the SQL builder (query/builder.ts): `_count` is selected by
3117
- // DEFAULT unless the caller explicitly opts out with `_count: false`, so
3118
- // every groupBy row carries `_count` and `orderBy: { _count }` works
3119
- // without requesting it (the alias is seeded into `aggOrderExprs`).
3120
- const countSelected = args._count === true || args._count === undefined;
3121
- if (countSelected) {
3116
+ // `_count` has the same two shapes as the SQL groupBy (query/aggregates.ts),
3117
+ // and the record form is why this is not a one-liner:
3118
+ // - `true` / omitted scalar `_count` (row count). Selected by DEFAULT
3119
+ // unless the caller opts out with `_count: false`, so every groupBy row
3120
+ // carries `_count` and `orderBy: { _count }` works without requesting it.
3121
+ // - record form → `_all` is the row count and each field is that column's
3122
+ // NON-NULL count, result `_count: { _all, field }`.
3123
+ // The record form used to fall through both branches here, so PowDB answered
3124
+ // `groupBy({ _count: { id: true } })` with the group keys and NO `_count` key
3125
+ // at all while every SQL engine returned the counts. Accepted and ignored is
3126
+ // the one outcome an aggregate must never have: the caller reads
3127
+ // `g._count.id` off a row that never carried it.
3128
+ const countArg = args._count;
3129
+ const scalarCount = countArg === true || countArg === undefined;
3130
+ const countIsRecord = !scalarCount && countArg !== false && typeof countArg === 'object';
3131
+ if (scalarCount) {
3122
3132
  const alias = `agg_${aggN++}`;
3123
3133
  proj.push(`${alias}: count(*)`);
3124
3134
  aggReaders.push({ alias, outKey: '_count', numeric: true });
3125
3135
  aggOrderExprs.set('_count', `.${alias}`);
3126
3136
  }
3137
+ else if (countIsRecord) {
3138
+ for (const [field, enabled] of Object.entries(countArg)) {
3139
+ if (!enabled)
3140
+ continue;
3141
+ const alias = `agg_${aggN++}`;
3142
+ if (field === '_all') {
3143
+ // Double underscore in the output key, collision-proof against a
3144
+ // real column named `all`, exactly as the SQL builder aliases it.
3145
+ claim('_count__all', '_count `_all`');
3146
+ proj.push(`${alias}: count(*)`);
3147
+ aggReaders.push({ alias, outKey: '_count:_all', numeric: true });
3148
+ // COUNT(*) is orderable whenever it is selected: the scalar form
3149
+ // above, or the record form containing `_all`. Same rule as SQL.
3150
+ aggOrderExprs.set('_count', `.${alias}`);
3151
+ }
3152
+ else {
3153
+ // Per-field count is the NON-NULL count, which the engine only
3154
+ // honours from the version `projectedCountSupported` gates on; below
3155
+ // it the projection was ignored and the ROW count came back. Same
3156
+ // per-column gate `aggregate()` applies, for the same reason.
3157
+ const col = this.column(field);
3158
+ this.assertProjectedCountSupported(field);
3159
+ claim(`_count_${col.name}`, `_count of column "${col.name}"`);
3160
+ const inner = this.colRefName(col.name);
3161
+ proj.push(`${alias}: count(${inner})`);
3162
+ aggReaders.push({ alias, outKey: `_count:${col.field}`, numeric: true });
3163
+ }
3164
+ }
3165
+ }
3127
3166
  for (const fn of ['_sum', '_avg', '_min', '_max']) {
3128
3167
  const spec = args[fn];
3129
3168
  if (!spec)
@@ -82,11 +82,23 @@ import { type PgCompatPool, TurbineClient, type TurbineConfig } from './client.j
82
82
  import type { SchemaMetadata } from './schema.js';
83
83
  export type { PgCompatPool, PgCompatPoolClient, PgCompatQueryResult } from './client.js';
84
84
  /**
85
- * Options for `turbineHttp()`. Mirrors the fields of `TurbineConfig`
86
- * that are relevant for externally-managed pools.
85
+ * Options for `turbineHttp()`: everything a `TurbineClient` takes except the
86
+ * pool, which is this function's first argument.
87
+ *
88
+ * This used to be a three-key `Pick` (`logging` / `defaultLimit` /
89
+ * `warnOnUnlimited`), which made the accepted set an ALLOWLIST that stopped
90
+ * being maintained the day it was written. The runtime already spread the whole
91
+ * object through, so the effect was purely at the type level: a serverless
92
+ * caller passing `globalFilters` (or `errorMessages`, or `relationLoadStrategy`)
93
+ * got an excess-property error on an option that would have worked. That is a
94
+ * kinder failure than PowDB's silent drop, but the same allowlist, and it lands
95
+ * on the edge deployments where a tenant filter matters most.
96
+ *
97
+ * Deliberately `Omit<TurbineConfig, 'pool'>` rather than `EngineClientConfig`:
98
+ * unlike the SQL engine factories, this one binds no dialect and pins no
99
+ * prepared-statement mode, so it has no business narrowing those away.
87
100
  */
88
- export interface TurbineHttpOptions extends Pick<TurbineConfig, 'logging' | 'defaultLimit' | 'warnOnUnlimited'> {
89
- }
101
+ export type TurbineHttpOptions = Omit<TurbineConfig, 'pool'>;
90
102
  /**
91
103
  * Create a TurbineClient bound to an external pg-compatible pool.
92
104
  *
package/dist/powdb.d.ts CHANGED
@@ -54,6 +54,7 @@
54
54
  */
55
55
  import { type PgCompatPool, type PgCompatPoolClient, TurbineClient, type TurbineConfig } from './client.js';
56
56
  import { type Dialect } from './dialect.js';
57
+ import type { EngineClientConfig } from './engine-config.js';
57
58
  import type { PowdbExec } from './powdb-introspect.js';
58
59
  import { type ColumnMetadata, type SchemaMetadata, type TableMetadata } from './schema.js';
59
60
  /**
@@ -932,8 +933,22 @@ export declare class PowdbEmbeddedPool implements PgCompatPool {
932
933
  }
933
934
  export { introspectPowdbDatabase, type PowdbExec, type PowdbIntrospectOptions, } from './powdb-introspect.js';
934
935
  export { PowqlInterface } from './powql.js';
935
- /** Options for {@link turbinePowDB}. */
936
- export interface TurbinePowdbOptions extends Pick<TurbineConfig, 'logging' | 'defaultLimit' | 'warnOnUnlimited' | 'relationLoadStrategy'> {
936
+ /**
937
+ * Options for {@link turbinePowDB}.
938
+ *
939
+ * Extends {@link EngineClientConfig}, the same inverted default the SQL engine
940
+ * factories use: every `TurbineConfig` option reaches the client unless it is one
941
+ * of the transport keys declared below, which {@link turbinePowDB} destructures
942
+ * out before spreading. This used to be a four-key `Pick`, so `globalFilters`,
943
+ * `stableRelationOrder`, `errorMessages` and everything else added after PowDB
944
+ * landed were accepted by the type and then dropped on the floor. A tenant filter
945
+ * that silently does not apply is the worst shape that class of bug can take.
946
+ *
947
+ * Options that PowDB cannot honour by CAPABILITY (rather than by plumbing) still
948
+ * throw a typed `UnsupportedFeatureError` (E017) at the point of use, which is a
949
+ * better answer than an option the type system claims does not exist.
950
+ */
951
+ export interface TurbinePowdbOptions extends EngineClientConfig {
937
952
  /**
938
953
  * Client-level default `with`-relation load strategy. On PowDB the default is
939
954
  * the batched N+1 loaders; setting `'join'` opts INTO native PowQL server-side
package/dist/powdb.js CHANGED
@@ -2342,14 +2342,16 @@ export async function turbinePowDB(target, schema, options = {}) {
2342
2342
  // The PowQL generator is loaded here to keep client.ts free of any PowDB import.
2343
2343
  const { PowqlInterface } = await import('./powql.js');
2344
2344
  const queryInterfaceFactory = (p, table, sch, middlewares, opts) => new PowqlInterface(p, table, sch, middlewares, opts);
2345
+ // Forward every client-level option EXCEPT the ones this factory owns (they
2346
+ // configure the pool / transport above, and TurbineClient warns on a config
2347
+ // key it does not recognize). Destructured rather than hand-listed on the way
2348
+ // IN: an allowlist is how `globalFilters` came to be accepted and ignored.
2349
+ const { connectionLimit: _connectionLimit, transactionQueueTimeoutMs: _transactionQueueTimeoutMs, retryStaleReads: _retryStaleReads, assumeEngineVersion: _assumeEngineVersion, readonly: _readonly, powdbClientModule: _powdbClientModule, powdbEmbeddedModule: _powdbEmbeddedModule, ...clientConfig } = options;
2345
2350
  const client = new TurbineClient({
2351
+ ...clientConfig,
2346
2352
  pool,
2347
2353
  preparedStatements: false,
2348
2354
  dialect: powdbDialect,
2349
- logging: options.logging,
2350
- defaultLimit: options.defaultLimit,
2351
- warnOnUnlimited: options.warnOnUnlimited,
2352
- relationLoadStrategy: options.relationLoadStrategy,
2353
2355
  queryInterfaceFactory,
2354
2356
  }, schema);
2355
2357
  if (owns) {
package/dist/powql.js CHANGED
@@ -3077,17 +3077,56 @@ export class PowqlInterface {
3077
3077
  const aggOrderExprs = new Map();
3078
3078
  const aggInner = new Map();
3079
3079
  let aggN = 0;
3080
- // Parity with the SQL builder (query/builder.ts): `_count` is selected by
3081
- // DEFAULT unless the caller explicitly opts out with `_count: false`, so
3082
- // every groupBy row carries `_count` and `orderBy: { _count }` works
3083
- // without requesting it (the alias is seeded into `aggOrderExprs`).
3084
- const countSelected = args._count === true || args._count === undefined;
3085
- if (countSelected) {
3080
+ // `_count` has the same two shapes as the SQL groupBy (query/aggregates.ts),
3081
+ // and the record form is why this is not a one-liner:
3082
+ // - `true` / omitted scalar `_count` (row count). Selected by DEFAULT
3083
+ // unless the caller opts out with `_count: false`, so every groupBy row
3084
+ // carries `_count` and `orderBy: { _count }` works without requesting it.
3085
+ // - record form → `_all` is the row count and each field is that column's
3086
+ // NON-NULL count, result `_count: { _all, field }`.
3087
+ // The record form used to fall through both branches here, so PowDB answered
3088
+ // `groupBy({ _count: { id: true } })` with the group keys and NO `_count` key
3089
+ // at all while every SQL engine returned the counts. Accepted and ignored is
3090
+ // the one outcome an aggregate must never have: the caller reads
3091
+ // `g._count.id` off a row that never carried it.
3092
+ const countArg = args._count;
3093
+ const scalarCount = countArg === true || countArg === undefined;
3094
+ const countIsRecord = !scalarCount && countArg !== false && typeof countArg === 'object';
3095
+ if (scalarCount) {
3086
3096
  const alias = `agg_${aggN++}`;
3087
3097
  proj.push(`${alias}: count(*)`);
3088
3098
  aggReaders.push({ alias, outKey: '_count', numeric: true });
3089
3099
  aggOrderExprs.set('_count', `.${alias}`);
3090
3100
  }
3101
+ else if (countIsRecord) {
3102
+ for (const [field, enabled] of Object.entries(countArg)) {
3103
+ if (!enabled)
3104
+ continue;
3105
+ const alias = `agg_${aggN++}`;
3106
+ if (field === '_all') {
3107
+ // Double underscore in the output key, collision-proof against a
3108
+ // real column named `all`, exactly as the SQL builder aliases it.
3109
+ claim('_count__all', '_count `_all`');
3110
+ proj.push(`${alias}: count(*)`);
3111
+ aggReaders.push({ alias, outKey: '_count:_all', numeric: true });
3112
+ // COUNT(*) is orderable whenever it is selected: the scalar form
3113
+ // above, or the record form containing `_all`. Same rule as SQL.
3114
+ aggOrderExprs.set('_count', `.${alias}`);
3115
+ }
3116
+ else {
3117
+ // Per-field count is the NON-NULL count, which the engine only
3118
+ // honours from the version `projectedCountSupported` gates on; below
3119
+ // it the projection was ignored and the ROW count came back. Same
3120
+ // per-column gate `aggregate()` applies, for the same reason.
3121
+ const col = this.column(field);
3122
+ this.assertProjectedCountSupported(field);
3123
+ claim(`_count_${col.name}`, `_count of column "${col.name}"`);
3124
+ const inner = this.colRefName(col.name);
3125
+ proj.push(`${alias}: count(${inner})`);
3126
+ aggReaders.push({ alias, outKey: `_count:${col.field}`, numeric: true });
3127
+ }
3128
+ }
3129
+ }
3091
3130
  for (const fn of ['_sum', '_avg', '_min', '_max']) {
3092
3131
  const spec = args[fn];
3093
3132
  if (!spec)
@@ -82,11 +82,23 @@ import { type PgCompatPool, TurbineClient, type TurbineConfig } from './client.j
82
82
  import type { SchemaMetadata } from './schema.js';
83
83
  export type { PgCompatPool, PgCompatPoolClient, PgCompatQueryResult } from './client.js';
84
84
  /**
85
- * Options for `turbineHttp()`. Mirrors the fields of `TurbineConfig`
86
- * that are relevant for externally-managed pools.
85
+ * Options for `turbineHttp()`: everything a `TurbineClient` takes except the
86
+ * pool, which is this function's first argument.
87
+ *
88
+ * This used to be a three-key `Pick` (`logging` / `defaultLimit` /
89
+ * `warnOnUnlimited`), which made the accepted set an ALLOWLIST that stopped
90
+ * being maintained the day it was written. The runtime already spread the whole
91
+ * object through, so the effect was purely at the type level: a serverless
92
+ * caller passing `globalFilters` (or `errorMessages`, or `relationLoadStrategy`)
93
+ * got an excess-property error on an option that would have worked. That is a
94
+ * kinder failure than PowDB's silent drop, but the same allowlist, and it lands
95
+ * on the edge deployments where a tenant filter matters most.
96
+ *
97
+ * Deliberately `Omit<TurbineConfig, 'pool'>` rather than `EngineClientConfig`:
98
+ * unlike the SQL engine factories, this one binds no dialect and pins no
99
+ * prepared-statement mode, so it has no business narrowing those away.
87
100
  */
88
- export interface TurbineHttpOptions extends Pick<TurbineConfig, 'logging' | 'defaultLimit' | 'warnOnUnlimited'> {
89
- }
101
+ export type TurbineHttpOptions = Omit<TurbineConfig, 'pool'>;
90
102
  /**
91
103
  * Create a TurbineClient bound to an external pg-compatible pool.
92
104
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turbine-orm",
3
- "version": "0.74.0",
3
+ "version": "0.75.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).",