kitcn 0.32.2 → 0.33.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.
@@ -1,3 +1,3 @@
1
- import { C as MigrationStep, D as defineMigration, E as buildMigrationPlan, O as defineMigrationSet, S as MigrationStateMap, T as MigrationWriteMode, _ as MigrationManifestEntry, a as MAX_STATUS_RUN_LIMIT, b as MigrationRunStatus, c as MigrationRunChunkArgs, d as MigrationAppliedState, f as MigrationDefinition, g as MigrationDriftIssue, h as MigrationDocContext, k as detectMigrationDrift, l as MigrationStatusArgs, m as MigrationDoc, o as MigrationCancelArgs, p as MigrationDirection, s as MigrationRunArgs, u as createMigrationHandlers, v as MigrationMigrateOne, w as MigrationTableName, x as MigrationSet, y as MigrationPlan } from "../../capabilities-BJm_VSDT.js";
2
- import { Ct as migrationCapability, dt as MIGRATION_STORAGE_TABLE_NAMES, ft as injectMigrationStorageTables, lt as MIGRATION_RUN_TABLE, mt as migrationStorageTables, pt as migrationExtension, ut as MIGRATION_STATE_TABLE } from "../../where-clause-compiler-Dgf4lrO-.js";
1
+ import { C as MigrationStep, D as defineMigration, E as buildMigrationPlan, O as defineMigrationSet, S as MigrationStateMap, T as MigrationWriteMode, _ as MigrationManifestEntry, a as MAX_STATUS_RUN_LIMIT, b as MigrationRunStatus, c as MigrationRunChunkArgs, d as MigrationAppliedState, f as MigrationDefinition, g as MigrationDriftIssue, h as MigrationDocContext, k as detectMigrationDrift, l as MigrationStatusArgs, m as MigrationDoc, o as MigrationCancelArgs, p as MigrationDirection, s as MigrationRunArgs, u as createMigrationHandlers, v as MigrationMigrateOne, w as MigrationTableName, x as MigrationSet, y as MigrationPlan } from "../../capabilities-Bxzoofl4.js";
2
+ import { Ct as migrationCapability, dt as MIGRATION_STORAGE_TABLE_NAMES, ft as injectMigrationStorageTables, lt as MIGRATION_RUN_TABLE, mt as migrationStorageTables, pt as migrationExtension, ut as MIGRATION_STATE_TABLE } from "../../where-clause-compiler-DrV6lQg0.js";
3
3
  export { MAX_STATUS_RUN_LIMIT, MIGRATION_RUN_TABLE, MIGRATION_STATE_TABLE, MIGRATION_STORAGE_TABLE_NAMES, type MigrationAppliedState, type MigrationCancelArgs, type MigrationDefinition, type MigrationDirection, type MigrationDoc, type MigrationDocContext, type MigrationDriftIssue, type MigrationManifestEntry, type MigrationMigrateOne, type MigrationPlan, type MigrationRunArgs, type MigrationRunChunkArgs, type MigrationRunStatus, type MigrationSet, type MigrationStateMap, type MigrationStatusArgs, type MigrationStep, type MigrationTableName, type MigrationWriteMode, buildMigrationPlan, createMigrationHandlers, defineMigration, defineMigrationSet, detectMigrationDrift, injectMigrationStorageTables, migrationCapability, migrationExtension, migrationStorageTables };
@@ -671,6 +671,21 @@ function mergedStream(streams, orderByIndexFields) {
671
671
  return new MergedStream(streams, orderByIndexFields);
672
672
  }
673
673
  /**
674
+ * Read streams over disjoint index ranges one after another, as one stream.
675
+ *
676
+ * Produces the same sequence a `mergedStream` over the whole index key would,
677
+ * for sources that cannot interleave — but a source is only opened once the one
678
+ * before it runs dry, so the fan-out a merge pays up front, one live query per
679
+ * source, never happens. That is what makes a very wide union affordable.
680
+ *
681
+ * The caller owes disjointness and index order. Both are checked as rows come
682
+ * out: a key that goes backwards throws rather than silently returning an
683
+ * unordered stream.
684
+ */
685
+ function concatStreams(streams) {
686
+ return new ConcatStreams(...streams);
687
+ }
688
+ /**
674
689
  * Marks sources that are already `OrderByStream`s, so `narrow` reuses them
675
690
  * instead of adding a redundant delegation layer per call.
676
691
  *
@@ -1134,6 +1149,24 @@ function* getOrderingIndexFields(stream) {
1134
1149
  const streamIndexFields = stream.getIndexFields();
1135
1150
  for (let i = 0; i <= streamEqualityIndexLength; i++) yield streamIndexFields.slice(i);
1136
1151
  }
1152
+ /**
1153
+ * Whether `mergedStream(..., orderByIndexFields)` would accept this stream.
1154
+ *
1155
+ * `MergedStream` wraps every source in an `OrderByStream`, which throws unless
1156
+ * the ordering fields are a suffix of the source's index fields past its
1157
+ * equality-pinned prefix. A caller that is *choosing* which index a source
1158
+ * walks has to know that before it commits to one, so it can fall back instead
1159
+ * of turning a working merge into a runtime error.
1160
+ *
1161
+ * Asks the stream itself rather than re-deriving the rule from an index name,
1162
+ * so the answer cannot drift from what `OrderByStream` actually enforces.
1163
+ */
1164
+ function streamCanOrderBy(stream, orderByIndexFields) {
1165
+ const normalized = orderByIndexFields.slice();
1166
+ normalizeIndexFields(normalized);
1167
+ for (const orderingIndexFields of getOrderingIndexFields(stream)) if (equalIndexFields(orderingIndexFields, normalized)) return true;
1168
+ return false;
1169
+ }
1137
1170
  var OrderByStream = class OrderByStream extends QueryStream {
1138
1171
  #staticFilter;
1139
1172
  #stream;
@@ -1351,4 +1384,4 @@ async function getByIdWithOrmQueryFallback(ctx, tableName, id) {
1351
1384
  }
1352
1385
 
1353
1386
  //#endregion
1354
- export { indexKeyWithinBounds as a, streamIndexRange as c, getIndexFields as i, isUnsetToken as l, EmptyStream as n, mergedStream as o, QueryStream as r, stream as s, getByIdWithOrmQueryFallback as t, unsetToken as u };
1387
+ export { getIndexFields as a, stream as c, isUnsetToken as d, unsetToken as f, concatStreams as i, streamCanOrderBy as l, EmptyStream as n, indexKeyWithinBounds as o, QueryStream as r, mergedStream as s, getByIdWithOrmQueryFallback as t, streamIndexRange as u };
@@ -1479,11 +1479,12 @@ function createCRPCContext(options) {
1479
1479
  const tokenReady = token === null || decodeJwtExp(token) !== null;
1480
1480
  const identity = resolveAuthIdentity(token);
1481
1481
  previousAuthRef.current = {
1482
- isAuthenticated,
1483
- identity
1482
+ identity,
1483
+ isAuthenticated
1484
1484
  };
1485
1485
  if (!previous) return;
1486
- if (tokenReady && (previous.identity !== identity || previous.isAuthenticated !== isAuthenticated)) convexQueryClient.resetAuthQueries();
1486
+ const tokenRejected = previous.isAuthenticated && !isAuthenticated;
1487
+ if (tokenReady && previous.identity !== identity || tokenRejected) convexQueryClient.resetAuthQueries();
1487
1488
  }, [
1488
1489
  convexQueryClient,
1489
1490
  isAuthenticated,
@@ -1313,6 +1313,327 @@ const ensureAggregateAllowedForRls = (tableConfig, rlsMode, methodName) => {
1313
1313
  assertAggregateAllowedForRls(tableConfig, rlsMode, AGGREGATE_ERROR.RLS_UNSUPPORTED, methodName);
1314
1314
  };
1315
1315
 
1316
+ //#endregion
1317
+ //#region src/orm/transaction-cache.ts
1318
+ /**
1319
+ * Per-transaction memo storage for the ORM.
1320
+ *
1321
+ * The ORM already has isolate-, execution-, statement- and row-scoped memos.
1322
+ * A hook needs transaction lifetime: the write barrier is built inside
1323
+ * `createOrmDbLifecycle`, which `createOrm` runs at module scope,
1324
+ * so a flag in that closure lives as long as the isolate and would leak an
1325
+ * answer from one transaction into the next.
1326
+ *
1327
+ * Deliberately dependency-free, for the same reason as `write-fanout`:
1328
+ * `aggregate-index/runtime` is contractually unreachable from `orm/index`
1329
+ * (`import-graph.test.ts`), so importing `lifecycle` here to read one symbol
1330
+ * would drag the trigger runtime into the aggregate entry's bundle.
1331
+ * `Symbol.for` is registry-based, so re-declaring the key resolves to the same
1332
+ * symbol `lifecycle` installs.
1333
+ */
1334
+ const ORMLIFECYCLE_INNER_DB = Symbol.for("kitcn:OrmLifecycleInnerDB");
1335
+ const ORM_TRANSACTION_ANCHOR = Symbol.for("kitcn:OrmTransactionAnchor");
1336
+ /**
1337
+ * Pins `anchor`'s transaction onto `target`, for a db built from a writer that
1338
+ * cannot be resolved back to it.
1339
+ *
1340
+ * `withoutTriggers` re-roots the ORM on the raw `ctx.db`, which is the one
1341
+ * object in the chain that carries no inner-db symbol — so a db derived from it
1342
+ * would otherwise stand for a transaction of its own and miss every memo and
1343
+ * queued write filed under the real one.
1344
+ */
1345
+ const markOrmTransactionAnchor = (target, db) => {
1346
+ const anchor = resolveOrmTransactionAnchor(db);
1347
+ if (!anchor || Object.hasOwn(target, ORM_TRANSACTION_ANCHOR)) return target;
1348
+ Object.defineProperty(target, ORM_TRANSACTION_ANCHOR, {
1349
+ configurable: false,
1350
+ enumerable: false,
1351
+ value: anchor,
1352
+ writable: false
1353
+ });
1354
+ return target;
1355
+ };
1356
+ /**
1357
+ * The object whose identity stands in for "this transaction".
1358
+ *
1359
+ * Convex builds `ctx.db` fresh on every UDF invocation, so it can never be
1360
+ * shared by two transactions. `getOrmLifecycleInnerDb` cannot be used on its
1361
+ * own: the lifecycle refuses to wrap readers and returns a no-op wrapper for
1362
+ * schemas with no triggers and no aggregate indexes, so the inner-db symbol is
1363
+ * absent for every query and for most mutations.
1364
+ *
1365
+ * Following the symbol to a fixed point, rather than one hop, is what makes the
1366
+ * answer canonical: `orm.with(hookCtx)` wraps a writer that is already a hook
1367
+ * wrapper, so one hop lands on the intermediate wrapper instead of the raw
1368
+ * writer the outer scope filed its work under. A pinned anchor wins outright,
1369
+ * because it is the only way a db rooted on the raw writer can name the
1370
+ * transaction at all.
1371
+ *
1372
+ * A nested `ctx.runMutation` shares the transaction but gets its own writer
1373
+ * and JS context. It cannot see caller-local memos or queued writes, nor can it
1374
+ * invalidate caller snapshots. Callers must flush deferred writes and expire
1375
+ * row snapshots before handing control to arbitrary user code.
1376
+ */
1377
+ const resolveOrmTransactionAnchor = (db) => {
1378
+ if (typeof db !== "object" || db === null) return;
1379
+ let current = db;
1380
+ const seen = new Set([current]);
1381
+ for (;;) {
1382
+ const pinned = current[ORM_TRANSACTION_ANCHOR];
1383
+ if (typeof pinned === "object" && pinned !== null) return pinned;
1384
+ const inner = current[ORMLIFECYCLE_INNER_DB];
1385
+ if (typeof inner !== "object" || inner === null || seen.has(inner)) return current;
1386
+ seen.add(inner);
1387
+ current = inner;
1388
+ }
1389
+ };
1390
+ /**
1391
+ * One memo namespace with transaction lifetime.
1392
+ *
1393
+ * The store is a `WeakMap` keyed on the anchor rather than a slot on the db,
1394
+ * because `createDatabase` promises not to mutate the `ctx.db` it was handed.
1395
+ * Entries die with the transaction's db object.
1396
+ *
1397
+ * Callers own staleness: only memoize a fact that nothing inside the
1398
+ * transaction can invalidate.
1399
+ */
1400
+ const createOrmTransactionMemo = () => {
1401
+ const byTransaction = /* @__PURE__ */ new WeakMap();
1402
+ return {
1403
+ get(db, key) {
1404
+ const anchor = resolveOrmTransactionAnchor(db);
1405
+ return anchor ? byTransaction.get(anchor)?.get(key) : void 0;
1406
+ },
1407
+ set(db, key, value) {
1408
+ const anchor = resolveOrmTransactionAnchor(db);
1409
+ if (!anchor) return;
1410
+ const existing = byTransaction.get(anchor);
1411
+ if (existing) {
1412
+ existing.set(key, value);
1413
+ return;
1414
+ }
1415
+ byTransaction.set(anchor, new Map([[key, value]]));
1416
+ }
1417
+ };
1418
+ };
1419
+
1420
+ //#endregion
1421
+ //#region src/orm/write-cache.ts
1422
+ const scopes = createOrmTransactionMemo();
1423
+ const activeScopes = /* @__PURE__ */ new Set();
1424
+ let suspended$1 = 0;
1425
+ /** Snapshots may survive ORM-owned work, never a return to arbitrary code. */
1426
+ const runInOrmWriteScope = async (db, fn) => {
1427
+ let scope = scopes.get(db, "write");
1428
+ if (!scope) {
1429
+ scope = {
1430
+ depth: 0,
1431
+ entries: /* @__PURE__ */ new Map()
1432
+ };
1433
+ scopes.set(db, "write", scope);
1434
+ }
1435
+ scope.entries.clear();
1436
+ scope.depth += 1;
1437
+ activeScopes.add(scope);
1438
+ try {
1439
+ return await fn();
1440
+ } finally {
1441
+ scope.entries.clear();
1442
+ scope.depth -= 1;
1443
+ if (scope.depth === 0) activeScopes.delete(scope);
1444
+ }
1445
+ };
1446
+ /**
1447
+ * User code can call a nested UDF whose isolated JS context cannot invalidate
1448
+ * our snapshots. Suspend every active scope, including reentrant statements,
1449
+ * until the callback settles; never restore pre-callback entries.
1450
+ */
1451
+ const withoutOrmWriteCache = async (fn) => {
1452
+ for (const scope of activeScopes) scope.entries.clear();
1453
+ suspended$1 += 1;
1454
+ try {
1455
+ return await fn();
1456
+ } finally {
1457
+ suspended$1 -= 1;
1458
+ }
1459
+ };
1460
+ const createOrmWriteMemo = () => {
1461
+ const namespace = Symbol("orm-write-memo");
1462
+ return {
1463
+ get(db, key) {
1464
+ const scope = scopes.get(db, "write");
1465
+ return !suspended$1 && scope?.depth ? scope.entries.get(namespace)?.get(key) : void 0;
1466
+ },
1467
+ set(db, key, value) {
1468
+ const scope = scopes.get(db, "write");
1469
+ if (suspended$1 || !scope?.depth) return;
1470
+ let entries = scope.entries.get(namespace);
1471
+ if (!entries) {
1472
+ entries = /* @__PURE__ */ new Map();
1473
+ scope.entries.set(namespace, entries);
1474
+ }
1475
+ entries.set(key, value);
1476
+ }
1477
+ };
1478
+ };
1479
+
1480
+ //#endregion
1481
+ //#region src/orm/write-batch.ts
1482
+ /**
1483
+ * Statement-scoped deferral for the ORM write path.
1484
+ *
1485
+ * The lifecycle fires its hooks once per written document, so a subsystem that
1486
+ * maintains derived storage — aggregate index buckets, today — reconciles one
1487
+ * document at a time and rewrites the same storage document once per row. The
1488
+ * fold that would collapse those writes exists; what it lacks is a boundary
1489
+ * wide enough to fold across. A mutation statement is that boundary: one
1490
+ * `update()` / `delete()` / `insert()` is one unit of work whose derived writes
1491
+ * nobody can observe half-applied unless they read them, and a reader can be
1492
+ * made to drain first.
1493
+ *
1494
+ * Depends only on the anchor and row-cache lifetime, for the same reason as
1495
+ * `write-fanout`: the mutation builders are reachable from `orm/index`, the
1496
+ * aggregate runtime contractually is not (`import-graph.test.ts`), and both
1497
+ * ends have to name this module.
1498
+ *
1499
+ * Three invariants make it safe:
1500
+ *
1501
+ * 1. Every queued write is applied by `flushOrmWriteBatch` and nowhere else.
1502
+ * Deltas are commutative but the writes that apply them are absolute
1503
+ * read-modify-writes, so two of them interleaving is a lost update. Callers
1504
+ * that have no batch open still enqueue and flush immediately rather than
1505
+ * writing around the queue, which makes the drain the single writer and
1506
+ * therefore the serialization point — the role `lifecycle`'s write lock
1507
+ * plays for the documents it wraps.
1508
+ * 2. Aggregate readers and user callbacks drain first. Callback execution
1509
+ * suspends deferral because nested UDFs cannot access the caller's queue.
1510
+ * 3. A flush callback must never itself drain. It runs inside the drain, and a
1511
+ * drain in progress is awaited rather than skipped, so re-entering it would
1512
+ * deadlock instead of returning stale rows. Keep the read barrier on the
1513
+ * query-path entry points, which no flush callback calls.
1514
+ */
1515
+ const batches = /* @__PURE__ */ new WeakMap();
1516
+ const activeBatches = /* @__PURE__ */ new Set();
1517
+ let suspended = 0;
1518
+ const getState = (db) => {
1519
+ const anchor = resolveOrmTransactionAnchor(db);
1520
+ if (!anchor) return;
1521
+ const existing = batches.get(anchor);
1522
+ if (existing) return existing;
1523
+ const created = {
1524
+ depth: 0,
1525
+ flushers: /* @__PURE__ */ new Set(),
1526
+ draining: null
1527
+ };
1528
+ batches.set(anchor, created);
1529
+ return created;
1530
+ };
1531
+ /**
1532
+ * Runs every queued flush callback until none is left.
1533
+ *
1534
+ * A callback is removed before it runs, so work enqueued while the drain is in
1535
+ * flight is picked up by this same loop instead of being dropped or applied
1536
+ * twice. `draining` is published before the first callback and cleared in the
1537
+ * same synchronous step as the emptiness check that ends the loop, so a
1538
+ * caller either sees a drain it can wait for or starts its own — never a
1539
+ * window where its work is queued against a drain that has already left.
1540
+ */
1541
+ const drain = (state) => {
1542
+ if (state.draining) return state.draining;
1543
+ let settled = false;
1544
+ const running = (async () => {
1545
+ try {
1546
+ for (;;) {
1547
+ const next = state.flushers.values().next();
1548
+ if (next.done) return;
1549
+ state.flushers.delete(next.value);
1550
+ await next.value();
1551
+ }
1552
+ } finally {
1553
+ state.draining = null;
1554
+ settled = true;
1555
+ }
1556
+ })();
1557
+ if (!settled) state.draining = running;
1558
+ return running;
1559
+ };
1560
+ /**
1561
+ * Opens a write batch for the duration of `fn`.
1562
+ *
1563
+ * Nesting is reference-counted: a cascade that runs another statement inside
1564
+ * this one folds into the same batch rather than flushing the outer statement's
1565
+ * pending work early. The flush runs whether or not `fn` threw, so rows that
1566
+ * landed before a mid-statement throw still get their derived writes.
1567
+ *
1568
+ * When both throw, `fn`'s error wins and the flush failure rides along as its
1569
+ * `cause`. A plain `finally { await drain() }` would report the consequence and
1570
+ * swallow the reason — the statement's own failure is what a caller can act on.
1571
+ */
1572
+ const runInOrmWriteBatch = async (db, fn) => {
1573
+ const state = getState(db);
1574
+ if (!state) return await fn();
1575
+ state.depth += 1;
1576
+ activeBatches.add(state);
1577
+ let failure;
1578
+ let result;
1579
+ try {
1580
+ result = await fn();
1581
+ } catch (error) {
1582
+ failure = { error };
1583
+ }
1584
+ state.depth -= 1;
1585
+ if (state.depth > 0) {
1586
+ if (failure) throw failure.error;
1587
+ return result;
1588
+ }
1589
+ try {
1590
+ await drain(state);
1591
+ } catch (flushError) {
1592
+ if (!failure) throw flushError;
1593
+ if (failure.error instanceof Error && failure.error.cause === void 0) failure.error.cause = flushError;
1594
+ } finally {
1595
+ if (state.depth === 0) activeBatches.delete(state);
1596
+ }
1597
+ if (failure) throw failure.error;
1598
+ return result;
1599
+ };
1600
+ /**
1601
+ * Queues `flush` for the next drain.
1602
+ *
1603
+ * Returns `false` only when `db` has no resolvable transaction, which is the
1604
+ * caller's signal that this module cannot help and the work has to be done
1605
+ * inline.
1606
+ */
1607
+ const enqueueOrmWriteBatch = (db, flush) => {
1608
+ const state = getState(db);
1609
+ if (!state) return false;
1610
+ state.flushers.add(flush);
1611
+ return true;
1612
+ };
1613
+ /** True while a statement scope is holding the queue back. */
1614
+ const isOrmWriteBatchOpen = (db) => suspended === 0 && (getState(db)?.depth ?? 0) > 0;
1615
+ /** Nested UDFs cannot read or drain queues in the caller's JS context. */
1616
+ const runInOrmUserCallback = async (fn) => await withoutOrmWriteCache(async () => {
1617
+ suspended += 1;
1618
+ try {
1619
+ for (const state of activeBatches) await drain(state);
1620
+ return await fn();
1621
+ } finally {
1622
+ suspended -= 1;
1623
+ }
1624
+ });
1625
+ /**
1626
+ * Applies everything queued. Both the read barrier and the only writer.
1627
+ *
1628
+ * Callers must be outside any flush callback; see this module's header.
1629
+ */
1630
+ const flushOrmWriteBatch = async (db) => {
1631
+ const anchor = resolveOrmTransactionAnchor(db);
1632
+ const state = anchor ? batches.get(anchor) : void 0;
1633
+ if (!state || !state.draining && state.flushers.size === 0) return;
1634
+ await drain(state);
1635
+ };
1636
+
1316
1637
  //#endregion
1317
1638
  //#region src/orm/aggregate-index/definitions.ts
1318
1639
  const getAggregateIndexDefinitions = (tableConfig) => {
@@ -1426,4 +1747,4 @@ function aggregateExtension() {
1426
1747
  }
1427
1748
 
1428
1749
  //#endregion
1429
- export { patchReferencingRows as $, enforcePolymorphicWrite as A, getForeignKeys as B, collectPrimaryIdLookupRows as C, encodeUndefinedDeep as D, deserializeFilterExpression as E, evaluateCheckConstraintTriState as F, getTableColumns as G, getMutationCollectionLimits as H, evaluateFilter as I, getUniqueIndexes as J, getTableDeleteConfig as K, extractPrimaryIdLookup as L, ensureDefaultColumns as M, ensureNonNullValues as N, enforceCheckConstraints as O, ensureNullableColumns as P, normalizeTemporalComparableValue as Q, getChecks as R, collectMutationRowsBounded as S, decodeUndefinedDeep as T, getMutationExecutionMode as U, getMutationAsyncDelayMs as V, getOrmContext as W, hydrateDateFieldsForRead as X, hardDeleteRow as Y, normalizeDateFieldsForWrite as Z, applyDefaults as _, AGGREGATE_STATE_TABLE as a, splitReturningSelection as at, buildForeignKeyGraph as b, getAggregateIndexDefinitions as c, toConvexFilter as ct, COUNT_ERROR as d, markLifecycleHookedTables as dt, resolveOrmRuntimeDefaults as et, createAggregateError as f, compileConvexFilter as ft, ensureCountAllowedForRls as g, ensureAggregateAllowedForRls as h, mapWithConcurrency as ht, AGGREGATE_RANK_TREE_TABLE as i, softDeleteRow as it, enforceUniqueIndexes as j, enforceForeignKeys as k, getRankIndexDefinitions as l, unsetFieldsOf as lt, createError as m, isConvexEnforceableFilter as mt, AGGREGATE_EXTREMA_TABLE as n, selectReturningRowWithHydration as nt, aggregateExtension as o, stripUnsetFields as ot, createCountError as p, convexAnd as pt, getTableName as q, AGGREGATE_MEMBER_TABLE as r, serializeFilterExpression as rt, rankAggregateName as s, takeRowsWithinByteBudget as st, AGGREGATE_BUCKET_TABLE as t, returningSelectionReadsCreationTime as tt, AGGREGATE_ERROR as u, hasLifecycleHooks as ut, applyIncomingForeignKeyActionsOnDelete as v, createForeignKeyProbeMemo as w, canUsePrimaryIdLookupCursor as x, applyIncomingForeignKeyActionsOnUpdate as y, getColumnName as z };
1750
+ export { getOrmContext as $, canUsePrimaryIdLookupCursor as A, enforceUniqueIndexes as B, createError as C, isConvexEnforceableFilter as Ct, applyIncomingForeignKeyActionsOnDelete as D, applyDefaults as E, deserializeFilterExpression as F, evaluateFilter as G, ensureNonNullValues as H, encodeUndefinedDeep as I, getColumnName as J, extractPrimaryIdLookup as K, enforceCheckConstraints as L, collectPrimaryIdLookupRows as M, createForeignKeyProbeMemo as N, applyIncomingForeignKeyActionsOnUpdate as O, decodeUndefinedDeep as P, getMutationExecutionMode as Q, enforceForeignKeys as R, createCountError as S, convexAnd as St, ensureCountAllowedForRls as T, ensureNullableColumns as U, ensureDefaultColumns as V, evaluateCheckConstraintTriState as W, getMutationAsyncDelayMs as X, getForeignKeys as Y, getMutationCollectionLimits as Z, createOrmTransactionMemo as _, toConvexFilter as _t, AGGREGATE_STATE_TABLE as a, hydrateDateFieldsForRead as at, COUNT_ERROR as b, markLifecycleHookedTables as bt, getAggregateIndexDefinitions as c, patchReferencingRows as ct, flushOrmWriteBatch as d, selectReturningRowWithHydration as dt, getTableColumns as et, isOrmWriteBatchOpen as f, serializeFilterExpression as ft, runInOrmWriteScope as g, takeRowsWithinByteBudget as gt, createOrmWriteMemo as h, stripUnsetFields as ht, AGGREGATE_RANK_TREE_TABLE as i, hardDeleteRow as it, collectMutationRowsBounded as j, buildForeignKeyGraph as k, getRankIndexDefinitions as l, resolveOrmRuntimeDefaults as lt, runInOrmWriteBatch as m, splitReturningSelection as mt, AGGREGATE_EXTREMA_TABLE as n, getTableName as nt, aggregateExtension as o, normalizeDateFieldsForWrite as ot, runInOrmUserCallback as p, softDeleteRow as pt, getChecks as q, AGGREGATE_MEMBER_TABLE as r, getUniqueIndexes as rt, rankAggregateName as s, normalizeTemporalComparableValue as st, AGGREGATE_BUCKET_TABLE as t, getTableDeleteConfig as tt, enqueueOrmWriteBatch as u, returningSelectionReadsCreationTime as ut, markOrmTransactionAnchor as v, unsetFieldsOf as vt, ensureAggregateAllowedForRls as w, mapWithConcurrency as wt, createAggregateError as x, compileConvexFilter as xt, AGGREGATE_ERROR as y, hasLifecycleHooks as yt, enforcePolymorphicWrite as z };
@@ -1,4 +1,4 @@
1
- import { $t as OrmLifecycleChange, A as CreateDatabaseOptions, Ar as $Type, B as getUniqueIndexes, Ct as OrderByClause, F as CascadeMode, Ft as ExtractTablesFromSchema, Gn as Columns, Gt as TablesRelationalConfig, Ht as RelationsBuilderConfigValue, I as DeleteMode, Kn as OrmRuntimeDefaults, L as SerializedFilterExpression, Mr as ColumnBuilder, Mt as VectorSearchProvider, N as OrmReader, Nr as ColumnBuilderBaseConfig, Or as SystemFieldAliases, P as OrmWriter, Pt as AnyRelationsBuilderConfig, Qn as OrmSchemaTriggers, R as getChecks, U as EdgeMetadata, Un as ConvexColumnBuilder, Ur as entityKind, Ut as RelationsConfigWithSchema, Vn as ConvexTextBuilderInitial, Wt as TableRelationalConfig, Xt as ConvexTable, Yn as OrmSchemaExtensionTriggers, Zn as OrmSchemaRelations, Zt as ConvexTableWithColumns, an as RlsPolicy, dt as InferInsertModel, kr as SystemFields, pt as InferSelectModel, qn as OrmSchemaExtensionRelations, r as OrmCapability, rr as FilterExpression$1, x as MigrationSet, z as getForeignKeys, zr as HasDefault, zt as RelationsBuilder } from "./capabilities-BJm_VSDT.js";
1
+ import { $t as OrmLifecycleChange, A as CreateDatabaseOptions, Ar as $Type, B as getUniqueIndexes, Ct as OrderByClause, F as CascadeMode, Ft as ExtractTablesFromSchema, Gn as Columns, Gt as TablesRelationalConfig, Ht as RelationsBuilderConfigValue, I as DeleteMode, Kn as OrmRuntimeDefaults, L as SerializedFilterExpression, Mr as ColumnBuilder, Mt as VectorSearchProvider, N as OrmReader, Nr as ColumnBuilderBaseConfig, Or as SystemFieldAliases, P as OrmWriter, Pt as AnyRelationsBuilderConfig, Qn as OrmSchemaTriggers, R as getChecks, U as EdgeMetadata, Un as ConvexColumnBuilder, Ur as entityKind, Ut as RelationsConfigWithSchema, Vn as ConvexTextBuilderInitial, Wt as TableRelationalConfig, Xt as ConvexTable, Yn as OrmSchemaExtensionTriggers, Zn as OrmSchemaRelations, Zt as ConvexTableWithColumns, an as RlsPolicy, dt as InferInsertModel, kr as SystemFields, pt as InferSelectModel, qn as OrmSchemaExtensionRelations, r as OrmCapability, rr as FilterExpression$1, x as MigrationSet, z as getForeignKeys, zr as HasDefault, zt as RelationsBuilder } from "./capabilities-Bxzoofl4.js";
2
2
  import * as convex_values0 from "convex/values";
3
3
  import { GenericId, Validator, Value } from "convex/values";
4
4
  import { DefineSchemaOptions, GenericDatabaseReader, GenericDatabaseWriter, GenericSchema, SchedulableFunctionReference, Scheduler, SchemaDefinition, internalActionGeneric, internalMutationGeneric, internalQueryGeneric } from "convex/server";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kitcn",
3
- "version": "0.32.2",
3
+ "version": "0.33.0",
4
4
  "description": "kitcn - React Query integration and CLI tools for Convex",
5
5
  "keywords": [
6
6
  "convex",
@@ -58,6 +58,19 @@ const orders = convexTable(
58
58
 
59
59
  After deploying, CLI runs `aggregateBackfill` automatically. Wait for `aggregateBackfillStatus` (an internal query) to report `READY`.
60
60
 
61
+ ### Write costs
62
+
63
+ Bulk ORM statements reuse aggregate bucket/member reads within uninterrupted
64
+ statements: one bucket read per distinct tuple and one membership read per
65
+ document. User hooks, policy callbacks and statement exit end reuse, preserving
66
+ nested-mutation writes. Raw writer calls do not share cached rows.
67
+
68
+ Shared bucket/extrema writes fold within statements. Aggregate reads (including
69
+ returning relation counts) and user hook/policy entry flush pending writes;
70
+ callbacks suspend batching until they settle so nested functions see stored
71
+ writes. Reads/callbacks can increase flush count. Membership rows remain one
72
+ write per changed document.
73
+
61
74
  ### `count()` — O(1) No-Scan Counts
62
75
 
63
76
  ```ts
@@ -598,7 +598,9 @@ Per source:
598
598
 
599
599
  - `index: { name, range }` anchors that source on its own range. It overrides
600
600
  the chain-level `.withIndex(...)`; sources that omit it use the chain index.
601
- - `where` filters that source's rows after the read.
601
+ - `where` is compiled against the table's indexes like a `findMany` where. It
602
+ bounds the read when it can be lowered onto the index that source walks;
603
+ whatever no index covers is filtered after the read.
602
604
 
603
605
  `interleaveBy` fields must be the trailing fields each source is already ordered
604
606
  by, so every field before them has to be pinned with `eq` in that source's
@@ -607,8 +609,18 @@ fields — e.g. `by_author_likes` (authorId, numLikes) with `eq("authorId", ...)
607
609
  merges with `numLikesAndType` (type, numLikes) with `eq("type", ...)` under
608
610
  `interleaveBy(["numLikes"])`.
609
611
 
610
- Anchor every source. A shared `.withIndex(...)` plus per-source `where` makes
611
- each source walk the same range and discard the misses after reading them.
612
+ Whether a source `where` bounds the read depends on the index that source ends
613
+ up walking. With no index pinned it picks one, so `{ where: { status: "active" } }`
614
+ reads the `by_status` range. With a pinned index it may only narrow that same
615
+ index: `.withIndex("by_status")` plus `{ where: { status: "active" } }` narrows,
616
+ while `.withIndex("by_name")` plus the same `where` walks `by_name` and discards
617
+ the misses.
618
+
619
+ A pinned `range` is the source's scope, never a hint: a `where` alongside one
620
+ always filters after the read rather than widening it. Lowering also has to
621
+ preserve the merge, so a `where` is only lowered when the resulting order can
622
+ still supply `interleaveBy`. Anchor a source with `index: { name, range }`
623
+ whenever you need a specific range read.
612
624
 
613
625
  ### Pre-pagination transforms
614
626
 
@@ -630,6 +642,14 @@ const page = await ctx.orm.query.users
630
642
  .paginate({ cursor: null, limit: 20 });
631
643
  ```
632
644
 
645
+ Children are read through an index that leads with the relation's foreign key.
646
+ A stage `where` rides that index when one extends it — `where: { numLikes: { gt: 10 } }`
647
+ on `posts` reads the `by_author_likes` (authorId, numLikes) range instead of
648
+ every post by the author. Children then arrive in that index's order, so a
649
+ lowered range field orders them ahead of creation time. Declare an index whose
650
+ fields are `(...foreign key, ...filtered fields)` for the stage filters you
651
+ page on.
652
+
633
653
  See [Select Composition Limitations](#select-composition-limitations) in API Reference.
634
654
 
635
655
  ## Pagination Modes
@@ -675,7 +695,7 @@ const page = await ctx.orm.query.users.withIndex("by_status").findMany({
675
695
  });
676
696
  ```
677
697
 
678
- Falls back to a bounded scan (needs `maxScan`) when the probed index cannot supply the requested `orderBy`, or when the union is wider than 64 ranges.
698
+ Falls back to a bounded scan (needs `maxScan`) when the probed index cannot supply the requested `orderBy`, or when a union wider than 64 ranges is asked for an `orderBy` that sorts across values, such as `createdAt`. Up to 64 ranges are read as one merged stream; a wider union is read one range after another, which keeps it index-bounded at any list length but can only produce the index's own order.
679
699
 
680
700
  Without an `orderBy`, an index-union page is in the order of the index it walks — grouped by the probed value — not in creation order.
681
701