kitcn 0.16.1 → 0.17.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.
Files changed (38) hide show
  1. package/dist/aggregate/index.d.ts +1 -1
  2. package/dist/auth/client/index.js +1 -1
  3. package/dist/auth/index.js +19 -21
  4. package/dist/auth/nextjs/index.d.ts +1 -1
  5. package/dist/auth/nextjs/index.js +4 -4
  6. package/dist/{auth-store-ssZDPa37.js → auth-store-BnGZxmnY.js} +4 -1
  7. package/dist/{backend-core-DqPydYyx.mjs → backend-core-BsKP1LVg.mjs} +204 -164
  8. package/dist/{builder-DBgto1yn.js → builder-f4F_NRvK.js} +245 -153
  9. package/dist/{caller-factory-NEfgD5E0.js → caller-factory-DHywSoGZ.js} +7 -5
  10. package/dist/cli.mjs +14 -7
  11. package/dist/crpc/index.js +1 -127
  12. package/dist/{middleware-Bg-PdtrI.js → middleware-Cgrv2jIu.js} +1 -1
  13. package/dist/orm/index.d.ts +1 -1
  14. package/dist/orm/index.js +335 -106
  15. package/dist/plugins/index.js +1 -1
  16. package/dist/{procedure-caller-9m6NBxQu.js → procedure-caller-Rj6z3ai7.js} +1 -1
  17. package/dist/{procedure-name-Cy1AxayA.d.ts → procedure-name-Bo5KMcqc.d.ts} +15 -3
  18. package/dist/{query-context-B47_3n97.js → query-context-C90vNlc9.js} +105 -22
  19. package/dist/query-options-C_eBSIXG.js +247 -0
  20. package/dist/ratelimit/index.d.ts +26 -6
  21. package/dist/ratelimit/index.js +427 -100
  22. package/dist/ratelimit/react/index.d.ts +14 -0
  23. package/dist/ratelimit/react/index.js +149 -16
  24. package/dist/react/index.d.ts +3 -1
  25. package/dist/react/index.js +48 -15
  26. package/dist/rsc/index.js +22 -33
  27. package/dist/server/index.d.ts +1 -1
  28. package/dist/server/index.js +3 -3
  29. package/dist/solid/index.js +19 -5
  30. package/dist/watcher.mjs +2 -2
  31. package/dist/{where-clause-compiler-C8UKgzTO.d.ts → where-clause-compiler-BRhLW1dp.d.ts} +24 -0
  32. package/package.json +1 -1
  33. package/skills/kitcn/SKILL.md +1 -0
  34. package/skills/kitcn/references/features/create-plugins.md +1 -1
  35. package/skills/kitcn/references/features/orm.md +11 -1
  36. package/skills/kitcn/references/features/ratelimit.md +105 -0
  37. package/skills/kitcn/references/setup/server.md +1 -1
  38. package/dist/query-options-C96zLANM.js +0 -121
package/dist/orm/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { A as vectorIndex, B as ConvexColumnBuilder, C as RlsPolicy, D as rankIndex, E as index, F as arrayOf, I as custom, L as json, M as createSystemFields, N as integer, O as searchIndex, P as id, R as objectOf, S as TablePolymorphic, T as aggregateIndex, V as entityKind, _ as OrmSchemaRelations, a as deletion, b as TableDeleteConfig, c as Columns, d as OrmSchemaDefinition, f as OrmSchemaExtensionRelations, g as OrmSchemaOptions, h as OrmSchemaExtensions, i as convexTable, j as text, k as uniqueIndex, l as EnableRLS, m as OrmSchemaExtensionTriggers, o as discriminator, p as OrmSchemaExtensionTables, s as Brand, t as DirectAggregate, u as OrmContext, v as OrmSchemaTriggers, w as rlsPolicy, x as TableName, y as RlsPolicies, z as unionOf } from "../runtime-DmVSOe24.js";
2
2
  import { a as pretendRequired, i as pretend, n as deprecated } from "../validators-C7LelqTN.js";
3
- import { A as ne, C as inArray, D as like, E as isNull, F as notLike, I as or, L as startsWith, M as notBetween, N as notIlike, O as lt, P as notInArray, S as ilike, T as isNotNull, _ as endsWith, a as mergedStream, b as gt, c as isUnsetToken, d as arrayContained, f as arrayContains, g as contains, h as column, i as getIndexFields, j as not, k as lte, l as unsetToken, m as between, n as EmptyStream, o as stream, p as arrayOverlaps, r as QueryStream, s as streamIndexRange, t as getByIdWithOrmQueryFallback, u as and, v as eq, w as isFieldReference, x as gte, y as fieldRef } from "../query-context-B47_3n97.js";
4
- import { v } from "convex/values";
3
+ import { A as like, B as or, C as gt, D as isFieldReference, E as inArray, F as not, I as notBetween, L as notIlike, M as lte, N as matchLikePattern, O as isNotNull, P as ne, R as notInArray, S as filterValuesEqual, T as ilike, V as startsWith, _ as contains, a as indexKeyWithinBounds, b as fieldRef, c as streamIndexRange, d as and, f as arrayContained, g as column, h as between, i as getIndexFields, j as lt, k as isNull, l as isUnsetToken, m as arrayOverlaps, n as EmptyStream, o as mergedStream, p as arrayContains, r as QueryStream, s as stream, t as getByIdWithOrmQueryFallback, u as unsetToken, v as endsWith, w as gte, x as filterValueInList, y as eq, z as notLike } from "../query-context-C90vNlc9.js";
4
+ import { compareValues, v } from "convex/values";
5
5
  import { defineSchema as defineSchema$1, internalActionGeneric, internalMutationGeneric } from "convex/server";
6
6
 
7
7
  //#region src/orm/builders/bigint.ts
@@ -1188,23 +1188,7 @@ function splitReturningSelection(fields) {
1188
1188
  countSelection
1189
1189
  };
1190
1190
  }
1191
- function matchLike(value, pattern, caseInsensitive) {
1192
- const targetValue = caseInsensitive ? value.toLowerCase() : value;
1193
- const targetPattern = caseInsensitive ? pattern.toLowerCase() : pattern;
1194
- if (targetPattern.startsWith("%") && targetPattern.endsWith("%")) {
1195
- const substring = targetPattern.slice(1, -1);
1196
- return targetValue.includes(substring);
1197
- }
1198
- if (targetPattern.startsWith("%")) {
1199
- const suffix = targetPattern.slice(1);
1200
- return targetValue.endsWith(suffix);
1201
- }
1202
- if (targetPattern.endsWith("%")) {
1203
- const prefix = targetPattern.slice(0, -1);
1204
- return targetValue.startsWith(prefix);
1205
- }
1206
- return targetValue === targetPattern;
1207
- }
1191
+ const matchLike = matchLikePattern;
1208
1192
  function evaluateFilter(row, filter) {
1209
1193
  if (filter.type === "binary") {
1210
1194
  const [field, value] = filter.operands;
@@ -1240,14 +1224,14 @@ function evaluateFilter(row, filter) {
1240
1224
  case "contains":
1241
1225
  if (typeof fieldValue !== "string") return false;
1242
1226
  return fieldValue.includes(value);
1243
- case "eq": return fieldValue === value;
1244
- case "ne": return fieldValue !== value;
1227
+ case "eq": return filterValuesEqual(fieldValue, value);
1228
+ case "ne": return !filterValuesEqual(fieldValue, value);
1245
1229
  case "gt": return fieldValue > value;
1246
1230
  case "gte": return fieldValue >= value;
1247
1231
  case "lt": return fieldValue < value;
1248
1232
  case "lte": return fieldValue <= value;
1249
- case "inArray": return value.includes(fieldValue);
1250
- case "notInArray": return !value.includes(fieldValue);
1233
+ case "inArray": return filterValueInList(fieldValue, value);
1234
+ case "notInArray": return !filterValueInList(fieldValue, value);
1251
1235
  case "arrayContains":
1252
1236
  if (!Array.isArray(fieldValue)) return false;
1253
1237
  return value.every((item) => fieldValue.includes(item));
@@ -1335,12 +1319,14 @@ function evaluateCheckConstraintTriState(row, filter) {
1335
1319
  case "inArray": {
1336
1320
  const arr = compareValue;
1337
1321
  if (!Array.isArray(arr)) return false;
1338
- return arr.includes(fieldValue);
1322
+ if (arr.includes(fieldValue)) return true;
1323
+ return arr.some(nullish) ? "unknown" : false;
1339
1324
  }
1340
1325
  case "notInArray": {
1341
1326
  const arr = compareValue;
1342
1327
  if (!Array.isArray(arr)) return false;
1343
- return !arr.includes(fieldValue);
1328
+ if (arr.includes(fieldValue)) return false;
1329
+ return arr.some(nullish) ? "unknown" : true;
1344
1330
  }
1345
1331
  case "arrayContains": {
1346
1332
  if (!Array.isArray(fieldValue)) return false;
@@ -3629,6 +3615,17 @@ function policyApplies(policy, operation) {
3629
3615
  const target = policy.for ?? "all";
3630
3616
  return target === "all" || target === operation;
3631
3617
  }
3618
+ /**
3619
+ * SQL pseudo-roles always resolve to the calling user, so they apply to every
3620
+ * caller and never need a role resolver. Only `rlsRole()` values and bare role
3621
+ * names are matched against resolved roles.
3622
+ */
3623
+ const PSEUDO_ROLES = new Set([
3624
+ "public",
3625
+ "current_role",
3626
+ "current_user",
3627
+ "session_user"
3628
+ ]);
3632
3629
  function flattenRoles(target) {
3633
3630
  if (!target) return "public";
3634
3631
  const roles = [];
@@ -3638,7 +3635,7 @@ function flattenRoles(target) {
3638
3635
  value.forEach(visit);
3639
3636
  return;
3640
3637
  }
3641
- if (value === "public") {
3638
+ if (typeof value === "string" && PSEUDO_ROLES.has(value)) {
3642
3639
  hasPublic = true;
3643
3640
  return;
3644
3641
  }
@@ -3651,14 +3648,42 @@ function flattenRoles(target) {
3651
3648
  visit(target);
3652
3649
  return hasPublic ? "public" : roles;
3653
3650
  }
3651
+ function roleResolverRequiredError(policy, table, targetRoles) {
3652
+ const roleList = targetRoles.map((role) => `'${role}'`).join(", ");
3653
+ return /* @__PURE__ */ new Error(`RLS_ROLE_RESOLVER_REQUIRED: policy '${policy.name}' on table '${getTableName(table)}' targets role(s) ${roleList}, but no 'roleResolver' is configured. Pass 'rls.roleResolver' when creating the ORM database, or remove the policy's 'to' clause.`);
3654
+ }
3655
+ /**
3656
+ * A named-role policy cannot be evaluated without a role resolver. Validate
3657
+ * that from the table's policy set alone so the failure depends only on
3658
+ * configuration, never on whether the table currently holds matching rows.
3659
+ */
3660
+ function assertRlsRolesResolvable(options) {
3661
+ const { table, operation, rls } = options;
3662
+ if (!isRlsEnabled(table)) return;
3663
+ if (rls?.mode === "skip") return;
3664
+ if (rls?.roleResolver) return;
3665
+ for (const policy of getRlsPolicies(table)) {
3666
+ if (!policyApplies(policy, operation)) continue;
3667
+ const targetRoles = flattenRoles(policy.to);
3668
+ if (targetRoles === "public") continue;
3669
+ throw roleResolverRequiredError(policy, table, targetRoles);
3670
+ }
3671
+ }
3654
3672
  function roleMatches(policy, rls) {
3655
- const resolver = rls?.roleResolver;
3656
- if (!resolver) return true;
3657
- const roles = resolver(rls?.ctx ?? {}) ?? [];
3658
3673
  const targetRoles = flattenRoles(policy.to);
3659
3674
  if (targetRoles === "public") return true;
3675
+ const resolver = rls?.roleResolver;
3676
+ const roles = resolver ? resolver(rls?.ctx ?? {}) ?? [] : [];
3660
3677
  return targetRoles.some((role) => roles.includes(role));
3661
3678
  }
3679
+ /**
3680
+ * Policy expressions follow SQL RLS semantics: an expression that evaluates to
3681
+ * NULL/unknown is treated as false. A nullish document field or a nullish
3682
+ * context value therefore never satisfies a policy.
3683
+ */
3684
+ function policyExpressionPasses(row, expression) {
3685
+ return evaluateCheckConstraintTriState(row, expression) === true;
3686
+ }
3662
3687
  async function resolveExpression(policy, checkType, ctx, table) {
3663
3688
  const candidate = checkType === "withCheck" ? policy.withCheck ?? policy.using : policy.using;
3664
3689
  if (!candidate) return;
@@ -3668,6 +3693,11 @@ async function resolveExpression(policy, checkType, ctx, table) {
3668
3693
  async function evaluatePolicySet({ table, operation, checkType, row, rls }) {
3669
3694
  if (!isRlsEnabled(table)) return true;
3670
3695
  if (rls?.mode === "skip") return true;
3696
+ assertRlsRolesResolvable({
3697
+ table,
3698
+ operation,
3699
+ rls
3700
+ });
3671
3701
  const ctx = rls?.ctx ?? {};
3672
3702
  const policies = getRlsPolicies(table).filter((policy) => policyApplies(policy, operation) && roleMatches(policy, rls));
3673
3703
  if (policies.length === 0) return false;
@@ -3676,7 +3706,7 @@ async function evaluatePolicySet({ table, operation, checkType, row, rls }) {
3676
3706
  let permissivePasses = false;
3677
3707
  for (const policy of permissive) {
3678
3708
  const expression = await resolveExpression(policy, checkType, ctx, table);
3679
- if (!expression || evaluateFilter(row, expression)) {
3709
+ if (!expression || policyExpressionPasses(row, expression)) {
3680
3710
  permissivePasses = true;
3681
3711
  break;
3682
3712
  }
@@ -3686,7 +3716,7 @@ async function evaluatePolicySet({ table, operation, checkType, row, rls }) {
3686
3716
  for (const policy of restrictive) {
3687
3717
  const expression = await resolveExpression(policy, checkType, ctx, table);
3688
3718
  if (!expression) continue;
3689
- if (!evaluateFilter(row, expression)) return false;
3719
+ if (!policyExpressionPasses(row, expression)) return false;
3690
3720
  }
3691
3721
  return true;
3692
3722
  }
@@ -3741,6 +3771,11 @@ async function evaluateUpdateDecision(options) {
3741
3771
  async function filterSelectRows(options) {
3742
3772
  if (!isRlsEnabled(options.table)) return options.rows;
3743
3773
  if (options.rls?.mode === "skip") return options.rows;
3774
+ assertRlsRolesResolvable({
3775
+ table: options.table,
3776
+ operation: "select",
3777
+ rls: options.rls
3778
+ });
3744
3779
  const rows = [];
3745
3780
  for (const row of options.rows) if (await canSelectRow({
3746
3781
  table: options.table,
@@ -4283,37 +4318,90 @@ const RELATION_COUNT_ERROR = {
4283
4318
  NOT_INDEXED: "RELATION_COUNT_NOT_INDEXED",
4284
4319
  FILTER_UNSUPPORTED: "RELATION_COUNT_FILTER_UNSUPPORTED"
4285
4320
  };
4286
- var LimitedQueryStream = class LimitedQueryStream extends QueryStream {
4287
- constructor(inner, limit) {
4321
+ /**
4322
+ * Replays an already-read run of `[doc | null, indexKey]` entries.
4323
+ *
4324
+ * `narrow()` filters the buffer instead of re-issuing the read, so a stream
4325
+ * that had to be consumed to be sized is not consumed twice.
4326
+ */
4327
+ var BufferedQueryStream = class BufferedQueryStream extends QueryStream {
4328
+ constructor(entries, order, indexFields, equalityIndexFilter) {
4288
4329
  super();
4289
- this.inner = inner;
4290
- this.limit = limit;
4330
+ this.entries = entries;
4331
+ this.order = order;
4332
+ this.indexFields = indexFields;
4333
+ this.equalityIndexFilter = equalityIndexFilter;
4291
4334
  }
4292
4335
  iterWithKeys() {
4293
- const iterable = this.inner.iterWithKeys();
4294
- const max = this.limit;
4336
+ const entries = this.entries;
4295
4337
  return { [Symbol.asyncIterator]() {
4296
- const iterator = iterable[Symbol.asyncIterator]();
4297
- let seen = 0;
4298
- return { async next() {
4299
- if (seen >= max) return {
4338
+ let index = 0;
4339
+ return { next() {
4340
+ if (index >= entries.length) return Promise.resolve({
4300
4341
  done: true,
4301
4342
  value: void 0
4302
- };
4303
- const next = await iterator.next();
4304
- if (!next.done) seen += 1;
4305
- return next;
4343
+ });
4344
+ const value = entries[index];
4345
+ index += 1;
4346
+ return Promise.resolve({
4347
+ done: false,
4348
+ value
4349
+ });
4306
4350
  } };
4307
4351
  } };
4308
4352
  }
4309
4353
  narrow(indexBounds) {
4310
- return new LimitedQueryStream(this.inner.narrow(indexBounds), this.limit);
4354
+ return new BufferedQueryStream(this.entries.filter(([, indexKey]) => indexKeyWithinBounds(indexKey, indexBounds)), this.order, this.indexFields, this.equalityIndexFilter);
4355
+ }
4356
+ getOrder() {
4357
+ return this.order;
4358
+ }
4359
+ getIndexFields() {
4360
+ return this.indexFields;
4361
+ }
4362
+ getEqualityIndexFilter() {
4363
+ return this.equalityIndexFilter;
4364
+ }
4365
+ };
4366
+ const PIPELINE_LIMIT_ORDINAL_FIELD = "__kitcn_limit_ordinal";
4367
+ /** Cap a stream at its first `limit` matching documents without eager reads. */
4368
+ var LimitedMatchesQueryStream = class LimitedMatchesQueryStream extends QueryStream {
4369
+ constructor(inner, limit, priorMatches = 0) {
4370
+ super();
4371
+ this.inner = inner;
4372
+ this.limit = limit;
4373
+ this.priorMatches = priorMatches;
4374
+ }
4375
+ iterWithKeys() {
4376
+ const inner = this.inner;
4377
+ const limit = this.limit;
4378
+ const priorMatches = this.priorMatches;
4379
+ return { async *[Symbol.asyncIterator]() {
4380
+ let matched = priorMatches;
4381
+ if (matched >= limit) return;
4382
+ for await (const [doc, indexKey] of inner.iterWithKeys()) {
4383
+ if (doc !== null) matched += 1;
4384
+ yield [doc, [...indexKey, matched]];
4385
+ if (matched >= limit) return;
4386
+ }
4387
+ } };
4388
+ }
4389
+ narrow(indexBounds) {
4390
+ const innerFieldCount = this.inner.getIndexFields().length;
4391
+ const ordinal = (this.getOrder() === "asc" ? indexBounds.lowerBound : indexBounds.upperBound)[innerFieldCount];
4392
+ const priorMatches = typeof ordinal === "number" && Number.isInteger(ordinal) && ordinal >= 0 ? Math.max(this.priorMatches, ordinal) : this.priorMatches;
4393
+ return new LimitedMatchesQueryStream(this.inner.narrow({
4394
+ lowerBound: indexBounds.lowerBound.slice(0, innerFieldCount),
4395
+ lowerBoundInclusive: indexBounds.lowerBoundInclusive,
4396
+ upperBound: indexBounds.upperBound.slice(0, innerFieldCount),
4397
+ upperBoundInclusive: indexBounds.upperBoundInclusive
4398
+ }), this.limit, priorMatches);
4311
4399
  }
4312
4400
  getOrder() {
4313
4401
  return this.inner.getOrder();
4314
4402
  }
4315
4403
  getIndexFields() {
4316
- return this.inner.getIndexFields();
4404
+ return [...this.inner.getIndexFields(), PIPELINE_LIMIT_ORDINAL_FIELD];
4317
4405
  }
4318
4406
  getEqualityIndexFilter() {
4319
4407
  return this.inner.getEqualityIndexFilter();
@@ -4468,7 +4556,7 @@ var GelRelationalQuery = class extends QueryPromise {
4468
4556
  return first ?? null;
4469
4557
  }
4470
4558
  async _applyRlsSelectFilter(rows, tableConfig) {
4471
- if (!rows.length || !tableConfig) return rows;
4559
+ if (!tableConfig) return rows;
4472
4560
  return await filterSelectRows({
4473
4561
  table: tableConfig.table,
4474
4562
  rows,
@@ -4532,21 +4620,7 @@ var GelRelationalQuery = class extends QueryPromise {
4532
4620
  return Object.values(this.schema).find((table) => table.name === dbName);
4533
4621
  }
4534
4622
  _matchLike(value, pattern, caseInsensitive) {
4535
- const targetValue = caseInsensitive ? value.toLowerCase() : value;
4536
- const targetPattern = caseInsensitive ? pattern.toLowerCase() : pattern;
4537
- if (targetPattern.startsWith("%") && targetPattern.endsWith("%")) {
4538
- const substring = targetPattern.slice(1, -1);
4539
- return targetValue.includes(substring);
4540
- }
4541
- if (targetPattern.startsWith("%")) {
4542
- const suffix = targetPattern.slice(1);
4543
- return targetValue.endsWith(suffix);
4544
- }
4545
- if (targetPattern.endsWith("%")) {
4546
- const prefix = targetPattern.slice(0, -1);
4547
- return targetValue.startsWith(prefix);
4548
- }
4549
- return targetValue === targetPattern;
4623
+ return matchLikePattern(value, pattern, caseInsensitive);
4550
4624
  }
4551
4625
  /**
4552
4626
  * True when the expression can be fully enforced by Convex's own `.filter()`.
@@ -4612,14 +4686,14 @@ var GelRelationalQuery = class extends QueryPromise {
4612
4686
  case "contains":
4613
4687
  if (typeof fieldValue !== "string") return false;
4614
4688
  return fieldValue.includes(normalizedValue);
4615
- case "eq": return fieldValue === normalizedValue;
4616
- case "ne": return fieldValue !== normalizedValue;
4689
+ case "eq": return filterValuesEqual(fieldValue, normalizedValue);
4690
+ case "ne": return !filterValuesEqual(fieldValue, normalizedValue);
4617
4691
  case "gt": return fieldValue > comparableValue;
4618
4692
  case "gte": return fieldValue >= comparableValue;
4619
4693
  case "lt": return fieldValue < comparableValue;
4620
4694
  case "lte": return fieldValue <= comparableValue;
4621
- case "inArray": return normalizedValue.includes(fieldValue);
4622
- case "notInArray": return !normalizedValue.includes(fieldValue);
4695
+ case "inArray": return filterValueInList(fieldValue, normalizedValue);
4696
+ case "notInArray": return !filterValueInList(fieldValue, normalizedValue);
4623
4697
  case "arrayContains":
4624
4698
  if (!Array.isArray(fieldValue)) return false;
4625
4699
  return normalizedValue.every((item) => fieldValue.includes(item));
@@ -4667,8 +4741,8 @@ var GelRelationalQuery = class extends QueryPromise {
4667
4741
  if (this._isPlaceholder(filter) || this._isSQLWrapper(filter)) throw new Error("SQL placeholders are not supported in Convex filters.");
4668
4742
  if (filter instanceof Date) return fieldValue === filter.getTime();
4669
4743
  if (filter === null || typeof filter !== "object" || Array.isArray(filter)) {
4670
- if (fieldName) return fieldValue === this._normalizeComparableValue(fieldName, filter);
4671
- return fieldValue === filter;
4744
+ if (fieldName) return filterValuesEqual(fieldValue, this._normalizeComparableValue(fieldName, filter));
4745
+ return filterValuesEqual(fieldValue, filter);
4672
4746
  }
4673
4747
  const entries = Object.entries(filter);
4674
4748
  if (!entries.length) return true;
@@ -4701,7 +4775,7 @@ var GelRelationalQuery = class extends QueryPromise {
4701
4775
  continue;
4702
4776
  }
4703
4777
  const normalized = fieldName ? this._normalizeComparableValue(fieldName, value) : value;
4704
- results.push(normalized.includes(fieldValue));
4778
+ results.push(filterValueInList(fieldValue, normalized));
4705
4779
  continue;
4706
4780
  }
4707
4781
  case "notIn": {
@@ -4710,7 +4784,7 @@ var GelRelationalQuery = class extends QueryPromise {
4710
4784
  continue;
4711
4785
  }
4712
4786
  const normalized = fieldName ? this._normalizeComparableValue(fieldName, value) : value;
4713
- results.push(!normalized.includes(fieldValue));
4787
+ results.push(!filterValueInList(fieldValue, normalized));
4714
4788
  continue;
4715
4789
  }
4716
4790
  case "arrayContains":
@@ -4784,10 +4858,10 @@ var GelRelationalQuery = class extends QueryPromise {
4784
4858
  results.push(fieldValue.includes(value));
4785
4859
  continue;
4786
4860
  case "eq":
4787
- results.push(fieldValue === (fieldName ? this._normalizeComparableValue(fieldName, value) : value));
4861
+ results.push(filterValuesEqual(fieldValue, fieldName ? this._normalizeComparableValue(fieldName, value) : value));
4788
4862
  continue;
4789
4863
  case "ne":
4790
- results.push(fieldValue !== (fieldName ? this._normalizeComparableValue(fieldName, value) : value));
4864
+ results.push(!filterValuesEqual(fieldValue, fieldName ? this._normalizeComparableValue(fieldName, value) : value));
4791
4865
  continue;
4792
4866
  case "gt":
4793
4867
  results.push(fieldValue > (fieldName ? this._normalizeComparableValue(fieldName, value) : value));
@@ -5255,6 +5329,67 @@ var GelRelationalQuery = class extends QueryPromise {
5255
5329
  row[config.alias] = nested;
5256
5330
  }
5257
5331
  }
5332
+ /**
5333
+ * Validate role-scoped policies for every table this read plan touches before
5334
+ * relations are loaded. Relation loaders skip work when a parent page is
5335
+ * empty, so per-row evaluation alone would make a misconfigured table fail
5336
+ * only once it holds rows. Mirrors the `_loadRelations` depth budget so the
5337
+ * plan walked here matches the plan that would be executed.
5338
+ */
5339
+ _assertRlsSelectPlan(withConfig, tableConfig, edges, depth, maxDepth) {
5340
+ assertRlsRolesResolvable({
5341
+ table: tableConfig.table,
5342
+ operation: "select",
5343
+ rls: this.rls
5344
+ });
5345
+ if (!withConfig || depth >= maxDepth) return;
5346
+ for (const [relationName, relationConfig] of Object.entries(withConfig)) {
5347
+ if (relationName === "_count") {
5348
+ this._assertRelationCountRlsPlan(relationConfig, edges);
5349
+ continue;
5350
+ }
5351
+ const edge = edges.find((entry) => entry.edgeName === relationName);
5352
+ if (!edge) continue;
5353
+ if (edge.through) {
5354
+ const throughTableConfig = this._getTableConfigByDbName(edge.through.table);
5355
+ if (throughTableConfig) assertRlsRolesResolvable({
5356
+ table: throughTableConfig.table,
5357
+ operation: "select",
5358
+ rls: this.rls
5359
+ });
5360
+ }
5361
+ const targetTableConfig = this._getTableConfigByDbName(edge.targetTable);
5362
+ if (!targetTableConfig) continue;
5363
+ let nestedWith;
5364
+ if (relationConfig && typeof relationConfig === "object") {
5365
+ const configuredWith = relationConfig.with;
5366
+ if (this._isRecord(configuredWith)) nestedWith = { ...configuredWith };
5367
+ const relationWhere = relationConfig.where;
5368
+ if (relationWhere && typeof relationWhere !== "function") {
5369
+ const filterWith = this._buildFilterWithConfig(relationWhere, targetTableConfig);
5370
+ if (Object.keys(filterWith).length > 0) if (nestedWith) this._mergeWithConfig(nestedWith, filterWith);
5371
+ else nestedWith = filterWith;
5372
+ }
5373
+ }
5374
+ this._assertRlsSelectPlan(nestedWith, targetTableConfig, this._getTargetTableEdges(edge.targetTable), depth + 1, maxDepth);
5375
+ }
5376
+ }
5377
+ _assertRelationCountRlsPlan(relationCountConfig, edges) {
5378
+ if (!this._isRecord(relationCountConfig)) return;
5379
+ for (const [relationName, relationSelection] of Object.entries(relationCountConfig)) {
5380
+ if (relationSelection === void 0 || relationSelection === false) continue;
5381
+ const edge = edges.find((entry) => entry.edgeName === relationName);
5382
+ if (!edge) continue;
5383
+ const where = this._coerceRelationCountWhere(relationName, relationSelection);
5384
+ if (edge.through) {
5385
+ const throughTableConfig = this._getTableConfigByDbName(edge.through.table);
5386
+ if (throughTableConfig) ensureCountAllowedForRls(throughTableConfig, this.rls?.mode);
5387
+ if (this._isEmptyWhere(where) || where === void 0) continue;
5388
+ }
5389
+ const targetTableConfig = this._getTableConfigByDbName(edge.targetTable);
5390
+ if (targetTableConfig) ensureCountAllowedForRls(targetTableConfig, this.rls?.mode);
5391
+ }
5392
+ }
5258
5393
  async _finalizeRows(rows) {
5259
5394
  const polymorphicState = this._resolvePolymorphicFinalizeState();
5260
5395
  const requestedWith = this.config.with;
@@ -5263,6 +5398,7 @@ var GelRelationalQuery = class extends QueryPromise {
5263
5398
  const extrasConfig = this.config.extras;
5264
5399
  const resolvedExtras = typeof extrasConfig === "function" ? extrasConfig(tableColumns) : extrasConfig;
5265
5400
  if (polymorphicState) this._assertPolymorphicAliasCollisions(polymorphicState.configs, requestedWith, resolvedExtras);
5401
+ this._assertRlsSelectPlan(effectiveWith, this.tableConfig, this.edgeMetadata, 0, 3);
5266
5402
  let rowsWithRelations = rows;
5267
5403
  if (effectiveWith) rowsWithRelations = await this._loadRelations(rowsWithRelations, effectiveWith, 0, 3, this.edgeMetadata, this.tableConfig);
5268
5404
  if (polymorphicState) this._synthesizePolymorphicRows(rowsWithRelations, polymorphicState.configs);
@@ -5357,6 +5493,33 @@ var GelRelationalQuery = class extends QueryPromise {
5357
5493
  }
5358
5494
  throw new Error(`${context} where callback must return a filter expression or predicate(...).`);
5359
5495
  }
5496
+ /**
5497
+ * Read an id-only `where` through the primary key instead of scanning for it.
5498
+ *
5499
+ * The where-clause compiler is built from declared indexes only, and `_id` is
5500
+ * never one of them, so an `id` filter can never be index-selected: it lands
5501
+ * in the post-filters and the stream walks the creation-time index until it
5502
+ * happens on the row. `db.get()` reads exactly the rows asked for, so the ids
5503
+ * are fetched directly and replayed as a creation-time-ordered stream — the
5504
+ * order the scan would have produced, so stage order and cursors are the same.
5505
+ *
5506
+ * Returns null when something else already owns the read: a pinned index, an
5507
+ * index the compiler did select, a `where(predicate)`, or an `orderBy` that
5508
+ * walks a different index.
5509
+ */
5510
+ async _buildIdLookupStream(params) {
5511
+ const { configuredIndex, idLookup, order, queryConfig, wherePredicate } = params;
5512
+ if (!idLookup || wherePredicate || configuredIndex?.name || queryConfig.index) return null;
5513
+ const primaryOrder = queryConfig.order?.[0];
5514
+ if (primaryOrder && primaryOrder.field !== INTERNAL_CREATION_TIME_FIELD) return null;
5515
+ const ids = idLookup.kind === "in" ? Array.from(new Map(idLookup.ids.map((id) => [String(id), id])).values()) : [idLookup.id];
5516
+ const rows = (await this._mapWithConcurrency(ids, async (id) => {
5517
+ return this._getById(this.tableConfig.name, id);
5518
+ })).filter((row) => !!row);
5519
+ rows.sort((a, b) => compareValues(a[INTERNAL_CREATION_TIME_FIELD], b[INTERNAL_CREATION_TIME_FIELD]) || compareValues(a[INTERNAL_ID_FIELD], b[INTERNAL_ID_FIELD]));
5520
+ if (order === "desc") rows.reverse();
5521
+ return new BufferedQueryStream(rows.map((row) => [row, [row[INTERNAL_CREATION_TIME_FIELD], row[INTERNAL_ID_FIELD]]]), order, [INTERNAL_CREATION_TIME_FIELD, INTERNAL_ID_FIELD], []);
5522
+ }
5360
5523
  _buildBasePipelineStream(queryConfig, wherePredicate, configuredIndex) {
5361
5524
  const schemaDefinition = this._getSchemaDefinitionOrThrow();
5362
5525
  let streamQuery = stream(this.db, schemaDefinition).query(this.tableConfig.name);
@@ -5437,11 +5600,18 @@ var GelRelationalQuery = class extends QueryPromise {
5437
5600
  const targetFields = edge.cardinality === "one" ? edge.targetFields.length > 0 ? edge.targetFields : ["_id"] : edge.targetFields.length > 0 ? edge.targetFields : [edge.fieldName];
5438
5601
  const targetTableConfig = this._getTableConfigByDbName(edge.targetTable);
5439
5602
  if (!targetTableConfig) throw new Error(`Pipeline flatMap target table '${edge.targetTable}' not found.`);
5603
+ assertRlsRolesResolvable({
5604
+ table: targetTableConfig.table,
5605
+ operation: "select",
5606
+ rls: this.rls
5607
+ });
5440
5608
  const strict = this.tableConfig.strict !== false;
5441
5609
  const indexName = targetFields.length === 1 && targetFields[0] === "_id" ? "by_id" : findRelationIndex(targetTableConfig.table, targetFields, `${this.tableConfig.name}.${relationName}`, edge.targetTable, strict, this.allowFullScan);
5442
5610
  const outerOrder = sourceStream.getOrder();
5443
5611
  const schemaDefinition = this._getSchemaDefinitionOrThrow();
5444
5612
  const innerIndexFields = getIndexFields(edge.targetTable, indexName ?? "by_creation_time" ?? "by_creation_time", schemaDefinition);
5613
+ if (stage.limit !== void 0 && (!Number.isInteger(stage.limit) || stage.limit < 1)) throw new Error("pipeline.flatMap.limit must be a positive integer");
5614
+ const mappedIndexFields = stage.limit === void 0 ? innerIndexFields : [...innerIndexFields, PIPELINE_LIMIT_ORDINAL_FIELD];
5445
5615
  const stageWherePredicate = this._buildTableFilterPredicate(stage.where, targetTableConfig);
5446
5616
  this._assertWhereIndexRequirement({
5447
5617
  where: stage.where,
@@ -5451,21 +5621,21 @@ var GelRelationalQuery = class extends QueryPromise {
5451
5621
  });
5452
5622
  return sourceStream.flatMap(async (parent) => {
5453
5623
  const values = sourceFields.map((field) => parent[field]);
5454
- if (values.some((value) => value === null || value === void 0)) return new EmptyStream(outerOrder, innerIndexFields);
5624
+ if (values.some((value) => value === null || value === void 0)) return new EmptyStream(outerOrder, mappedIndexFields);
5455
5625
  let inner = stream(this.db, schemaDefinition).query(edge.targetTable);
5456
5626
  if (indexName) inner = inner.withIndex(indexName, (q) => this._applyEqBounds(q, targetFields, values));
5457
5627
  inner = inner.order(outerOrder);
5458
5628
  if (stageWherePredicate) inner = inner.filterWith(stageWherePredicate);
5459
- if (stage.limit !== void 0) {
5460
- if (!Number.isInteger(stage.limit) || stage.limit < 1) throw new Error("pipeline.flatMap.limit must be a positive integer");
5461
- inner = new LimitedQueryStream(inner, stage.limit);
5462
- }
5629
+ if (isRlsEnabled(targetTableConfig.table)) inner = inner.filterWith(async (row) => {
5630
+ return (await this._applyRlsSelectFilter([row], targetTableConfig)).length === 1;
5631
+ });
5632
+ if (stage.limit !== void 0) inner = new LimitedMatchesQueryStream(inner, stage.limit);
5463
5633
  if (stage.includeParent ?? true) inner = inner.map(async (child) => ({
5464
5634
  parent,
5465
5635
  child
5466
5636
  }));
5467
5637
  return inner;
5468
- }, innerIndexFields);
5638
+ }, mappedIndexFields);
5469
5639
  }
5470
5640
  async _applyPipelineStages(baseStream, pipeline) {
5471
5641
  let streamQuery = baseStream;
@@ -6451,8 +6621,11 @@ var GelRelationalQuery = class extends QueryPromise {
6451
6621
  if (vectorSearchConfig) throw new Error("pageByKey cannot be combined with vectorSearch.");
6452
6622
  if (pipeline) throw new Error("pageByKey cannot be combined with pipeline.");
6453
6623
  }
6624
+ const preflightWith = this._resolveWithVariantsState(config.with, this._resolvePolymorphicFinalizeState()).effectiveWith;
6625
+ this._assertRlsSelectPlan(preflightWith, this.tableConfig, this.edgeMetadata, 0, 3);
6626
+ if (whereFilter) this._assertRlsSelectPlan(this._buildFilterWithConfig(whereFilter, this.tableConfig), this.tableConfig, this.edgeMetadata, 0, 3);
6454
6627
  const idLookup = this._extractIdOnlyWhere(whereFilter);
6455
- if (idLookup && !vectorSearchConfig && !searchConfig && !wherePredicate && !isCursorPaginated && configuredIndex === void 0) {
6628
+ if (idLookup && !vectorSearchConfig && !searchConfig && !wherePredicate && !isCursorPaginated && !pipeline && !pageByKey && endCursor === void 0 && configuredIndex === void 0) {
6456
6629
  const orderSpecs = this._orderBySpecs(config.orderBy);
6457
6630
  const offset = config.offset ?? 0;
6458
6631
  if (offset !== void 0 && typeof offset !== "number") throw new Error("Only numeric offset is supported in kitcn ORM.");
@@ -6467,7 +6640,7 @@ var GelRelationalQuery = class extends QueryPromise {
6467
6640
  const selectedRows = await this._finalizeRows(rows);
6468
6641
  return this._returnSelectedRows(selectedRows);
6469
6642
  }
6470
- const queryConfig = this._toConvexQuery(whereExpressionFromCallback);
6643
+ const queryConfig = this._toConvexQuery(whereExpressionFromCallback, configuredIndex);
6471
6644
  if (!searchConfig && !vectorSearchConfig && wherePredicate !== void 0 && !configuredIndex?.name) throw new Error("This where() with predicate(...) requires .withIndex(name, range?). Add .withIndex(...) before findMany/findFirst.");
6472
6645
  if (pageByKey) {
6473
6646
  const schemaDefinition = this._getSchemaDefinitionOrThrow();
@@ -6505,7 +6678,23 @@ var GelRelationalQuery = class extends QueryPromise {
6505
6678
  if (!pipeline?.interleaveBy || pipeline.interleaveBy.length === 0) throw new Error("pipeline.interleaveBy is required when pipeline.union has multiple sources.");
6506
6679
  streamQuery = mergedStream(streams, pipeline.interleaveBy.map((field) => this._normalizePublicFieldName(field)));
6507
6680
  }
6508
- } else streamQuery = this._buildBasePipelineStream(queryConfig, wherePredicate, configuredIndex);
6681
+ } else {
6682
+ if (isCursorPaginated && maxScan !== void 0 && idLookup?.kind === "in") throw new Error("An id IN pipeline cannot use maxScan because creation-order replay requires reading the complete ID list.");
6683
+ streamQuery = await this._buildIdLookupStream({
6684
+ configuredIndex,
6685
+ idLookup,
6686
+ order: fallbackOrder,
6687
+ queryConfig,
6688
+ wherePredicate
6689
+ }) ?? this._buildBasePipelineStream(queryConfig, wherePredicate, configuredIndex);
6690
+ }
6691
+ const rootRlsEnabled = isRlsEnabled(this.tableConfig.table);
6692
+ if (rootRlsEnabled) {
6693
+ await this._applyRlsSelectFilter([], this.tableConfig);
6694
+ streamQuery = streamQuery.filterWith(async (row) => {
6695
+ return (await this._applyRlsSelectFilter([row], this.tableConfig)).length === 1;
6696
+ });
6697
+ }
6509
6698
  if (pipeline) streamQuery = await this._applyPipelineStages(streamQuery, pipeline);
6510
6699
  if (isCursorPaginated) {
6511
6700
  const paginationResult = await streamQuery.paginate({
@@ -6515,7 +6704,7 @@ var GelRelationalQuery = class extends QueryPromise {
6515
6704
  maxScan
6516
6705
  });
6517
6706
  return {
6518
- page: await this._finalizeRows(await this._applyRlsSelectFilter(paginationResult.page, this.tableConfig)),
6707
+ page: await this._finalizeRows(rootRlsEnabled ? paginationResult.page : await this._applyRlsSelectFilter(paginationResult.page, this.tableConfig)),
6519
6708
  continueCursor: paginationResult.continueCursor,
6520
6709
  isDone: paginationResult.isDone,
6521
6710
  pageStatus: paginationResult.pageStatus,
@@ -6527,7 +6716,7 @@ var GelRelationalQuery = class extends QueryPromise {
6527
6716
  const limit = this._resolveNonPaginatedLimit(config);
6528
6717
  let rows = limit === void 0 ? await streamQuery.collect() : await streamQuery.take(offset > 0 ? offset + limit : limit);
6529
6718
  if (offset > 0) rows = rows.slice(offset);
6530
- rows = await this._applyRlsSelectFilter(rows, this.tableConfig);
6719
+ if (!rootRlsEnabled) rows = await this._applyRlsSelectFilter(rows, this.tableConfig);
6531
6720
  const selectedRows = await this._finalizeRows(rows);
6532
6721
  return this._returnSelectedRows(selectedRows);
6533
6722
  }
@@ -6633,7 +6822,7 @@ var GelRelationalQuery = class extends QueryPromise {
6633
6822
  const orderField = primaryOrder.field;
6634
6823
  if (orderField === "_creationTime") query = query.order(primaryOrder.direction);
6635
6824
  else {
6636
- const orderIndex = getIndexes(this.tableConfig.table).find((idx) => idx.fields.includes(orderField)) ?? this.edgeMetadata.find((idx) => idx.indexFields.includes(orderField));
6825
+ const orderIndex = getIndexes(this.tableConfig.table).find((idx) => idx.fields[0] === orderField) ?? this.edgeMetadata.find((idx) => idx.indexFields[0] === orderField);
6637
6826
  if (orderIndex) {
6638
6827
  orderIndexName = "indexName" in orderIndex ? orderIndex.indexName : orderIndex.name;
6639
6828
  query = query.withIndex(orderIndexName, (q) => q);
@@ -6715,7 +6904,7 @@ var GelRelationalQuery = class extends QueryPromise {
6715
6904
  if (queryConfig.postFilters.length > 0) rows = rows.filter((row) => queryConfig.postFilters.every((filter) => this._evaluatePostFetchFilter(row, filter)));
6716
6905
  rows = await this._applyRlsSelectFilter(rows, this.tableConfig);
6717
6906
  if (whereFilter) rows = await this._applyRelationsFilterToRows(rows, this.tableConfig, whereFilter, this.edgeMetadata, 0, 3, this.config.with);
6718
- if (usePostFetchSort && postFetchOrders.length > 0) rows = rows.sort((a, b) => this._compareByOrderSpecs(a, b, postFetchOrders));
6907
+ if (postFetchOrders.length > 0) rows = rows.sort((a, b) => this._compareByOrderSpecs(a, b, postFetchOrders));
6719
6908
  const offset = config.offset ?? 0;
6720
6909
  if (typeof offset !== "number") throw new Error("Only numeric offset is supported in kitcn ORM.");
6721
6910
  const limit = this._resolveNonPaginatedLimit(config);
@@ -6769,6 +6958,7 @@ var GelRelationalQuery = class extends QueryPromise {
6769
6958
  const schemaDefinition = this.schema[OrmSchemaDefinition];
6770
6959
  if (!schemaDefinition) throw new Error("Pagination with maxScan requires defineSchema(). Ensure defineSchema(tables) was used with the same tables object passed to defineRelations.");
6771
6960
  let streamQuery = stream(this.db, schemaDefinition).query(this.tableConfig.name);
6961
+ if (configuredIndex?.name) streamQuery = streamQuery.withIndex(configuredIndex.name, configuredIndex.range ? configuredIndex.range : (q) => q);
6772
6962
  if (queryConfig.order && primaryOrder) {
6773
6963
  if (needsPostFetchSortForPrimary) {
6774
6964
  if (strict) throw new Error(`Pagination: Field '${primaryOrder.field}' has no index. Add an index or disable strict.`);
@@ -6903,7 +7093,7 @@ var GelRelationalQuery = class extends QueryPromise {
6903
7093
  * Convert query config to Convex query parameters
6904
7094
  * Phase 4 implementation with WhereClauseCompiler
6905
7095
  */
6906
- _toConvexQuery(whereExpressionOverride) {
7096
+ _toConvexQuery(whereExpressionOverride, configuredIndex) {
6907
7097
  const config = this.config;
6908
7098
  const tableIndexes = getIndexes(this.tableConfig.table).map((index) => ({
6909
7099
  indexName: index.name,
@@ -6912,12 +7102,20 @@ var GelRelationalQuery = class extends QueryPromise {
6912
7102
  const compiler = new WhereClauseCompiler(this.tableConfig.table.tableName, tableIndexes);
6913
7103
  let whereExpression = whereExpressionOverride;
6914
7104
  if (!whereExpression && config.where && typeof config.where !== "function") whereExpression = this._buildFilterExpression(config.where, this.tableConfig);
6915
- const compiled = compiler.compile(whereExpression);
7105
+ const planned = compiler.compile(whereExpression);
7106
+ const plannedUsesIndex = !!planned.selectedIndex && (planned.indexFilters.length > 0 || planned.probeFilters.length > 0);
7107
+ const compiled = !configuredIndex?.name || !plannedUsesIndex || planned.selectedIndex?.indexName === configuredIndex.name && !configuredIndex.range ? planned : {
7108
+ strategy: "none",
7109
+ selectedIndex: null,
7110
+ indexFilters: [],
7111
+ probeFilters: [],
7112
+ postFilters: whereExpression ? [whereExpression] : []
7113
+ };
6916
7114
  const result = {
6917
7115
  table: this.tableConfig.table.tableName,
6918
7116
  strategy: compiled.strategy,
6919
- probeFilters: compiled.probeFilters,
6920
- postFilters: compiled.postFilters
7117
+ probeFilters: [...compiled.probeFilters],
7118
+ postFilters: [...compiled.postFilters]
6921
7119
  };
6922
7120
  if (compiled.selectedIndex && (compiled.indexFilters.length > 0 || compiled.probeFilters.length > 0)) result.index = {
6923
7121
  name: compiled.selectedIndex.indexName,
@@ -7403,6 +7601,7 @@ var GelRelationalQuery = class extends QueryPromise {
7403
7601
  if (!targetTableConfig) throw new Error(`Relation '${relationName}' target table '${edge.targetTable}' not found.`);
7404
7602
  const relationDefinition = tableConfig.relations[relationName];
7405
7603
  const strict = tableConfig.strict !== false;
7604
+ const hasPostFetchTargetFilter = this.rls?.mode !== "skip" && isRlsEnabled(targetTableConfig.table) || Boolean(relationDefinition?.where) || Boolean(relationConfig && typeof relationConfig === "object" && "where" in relationConfig && relationConfig.where);
7406
7605
  let orderSpecs = [];
7407
7606
  if (relationConfig && typeof relationConfig === "object" && "orderBy" in relationConfig) {
7408
7607
  let orderByValue = relationConfig.orderBy;
@@ -7423,17 +7622,34 @@ var GelRelationalQuery = class extends QueryPromise {
7423
7622
  if (effectivePerParentLimit !== void 0) result = result.slice(0, effectivePerParentLimit);
7424
7623
  return result;
7425
7624
  };
7625
+ const applyPostFetchTargetFilters = async (candidateTargets) => {
7626
+ let filteredTargets = await this._applyRlsSelectFilter(candidateTargets, targetTableConfig);
7627
+ if (relationDefinition?.where) filteredTargets = filteredTargets.filter((target) => this._evaluateTableFilter(target, targetTableConfig, relationDefinition.where));
7628
+ if (relationConfig && typeof relationConfig === "object" && "where" in relationConfig) {
7629
+ const whereFilter = relationConfig.where;
7630
+ if (typeof whereFilter === "function") {
7631
+ const whereExpression = this._resolveWhereCallbackExpression(whereFilter, targetTableConfig, { context: "relation" });
7632
+ if (whereExpression && !this._isPredicateWhereClause(whereExpression)) filteredTargets = filteredTargets.filter((target) => this._evaluatePostFetchFilter(target, whereExpression));
7633
+ } else if (whereFilter) {
7634
+ const targetEdges = this._getTargetTableEdges(edge.targetTable);
7635
+ filteredTargets = await this._applyRelationsFilterToRows(filteredTargets, targetTableConfig, whereFilter, targetEdges, depth + 1, maxDepth, relationConfig.with);
7636
+ }
7637
+ }
7638
+ return filteredTargets;
7639
+ };
7426
7640
  let targets = [];
7427
7641
  let throughBySourceKey;
7642
+ let targetFiltersApplied = false;
7428
7643
  if (edge.through) {
7429
7644
  const throughTableConfig = this._getTableConfigByDbName(edge.through.table);
7430
7645
  if (!throughTableConfig) throw new Error(`Relation '${relationName}' through table '${edge.through.table}' not found.`);
7431
7646
  const throughIndexName = findRelationIndex(throughTableConfig.table, edge.through.sourceFields, `${tableConfig.name}.${relationName}`, edge.through.table, strict, this.allowFullScan);
7432
7647
  const entries = Array.from(sourceKeyMap.entries());
7433
7648
  const throughRowsPerSource = await this._mapWithConcurrency(entries, async ([key, values]) => {
7649
+ const query = this._queryByFields(this.db.query(edge.through.table), edge.through.sourceFields, values, throughIndexName);
7434
7650
  return {
7435
7651
  key,
7436
- rows: await this._queryByFields(this.db.query(edge.through.table), edge.through.sourceFields, values, throughIndexName).collect()
7652
+ rows: await this._applyRlsSelectFilter(await query.collect(), throughTableConfig)
7437
7653
  };
7438
7654
  });
7439
7655
  throughBySourceKey = /* @__PURE__ */ new Map();
@@ -7470,27 +7686,30 @@ var GelRelationalQuery = class extends QueryPromise {
7470
7686
  } else {
7471
7687
  const indexName = findRelationIndex(targetTableConfig.table, targetFields, `${tableConfig.name}.${relationName}`, edge.targetTable, strict, this.allowFullScan);
7472
7688
  const entries = Array.from(sourceKeyMap.entries());
7689
+ const streamPostFetchTargetFilters = orderSpecs.length === 0 && hasPostFetchTargetFilter && effectivePerParentLimit !== void 0;
7690
+ targetFiltersApplied = streamPostFetchTargetFilters;
7473
7691
  targets = (await this._mapWithConcurrency(entries, async ([, values]) => {
7474
7692
  const query = this._queryByFields(this.db.query(edge.targetTable), targetFields, values, indexName);
7475
- if (orderSpecs.length === 0 && effectivePerParentLimit !== void 0) {
7693
+ if (orderSpecs.length === 0 && !hasPostFetchTargetFilter && effectivePerParentLimit !== void 0) {
7476
7694
  const fetchLimit = (perParentOffset ?? 0) + (effectivePerParentLimit ?? 0);
7477
7695
  return await query.take(fetchLimit);
7478
7696
  }
7697
+ if (streamPostFetchTargetFilters) {
7698
+ const visibleTargets = [];
7699
+ const fetchLimit = Math.max(perParentOffset ?? 0, 0) + (effectivePerParentLimit ?? 0);
7700
+ for await (const target of query) {
7701
+ const filtered = await applyPostFetchTargetFilters([target]);
7702
+ if (filtered.length > 0) {
7703
+ visibleTargets.push(filtered[0]);
7704
+ if (visibleTargets.length >= fetchLimit) break;
7705
+ }
7706
+ }
7707
+ return visibleTargets;
7708
+ }
7479
7709
  return await query.collect();
7480
7710
  })).flat();
7481
7711
  }
7482
- targets = await this._applyRlsSelectFilter(targets, targetTableConfig);
7483
- if (relationDefinition?.where) targets = targets.filter((target) => this._evaluateTableFilter(target, targetTableConfig, relationDefinition.where));
7484
- if (relationConfig && typeof relationConfig === "object" && "where" in relationConfig) {
7485
- const whereFilter = relationConfig.where;
7486
- if (typeof whereFilter === "function") {
7487
- const whereExpression = this._resolveWhereCallbackExpression(whereFilter, targetTableConfig, { context: "relation" });
7488
- if (whereExpression && !this._isPredicateWhereClause(whereExpression)) targets = targets.filter((target) => this._evaluatePostFetchFilter(target, whereExpression));
7489
- } else if (whereFilter) {
7490
- const targetEdges = this._getTargetTableEdges(edge.targetTable);
7491
- targets = await this._applyRelationsFilterToRows(targets, targetTableConfig, whereFilter, targetEdges, depth + 1, maxDepth, relationConfig.with);
7492
- }
7493
- }
7712
+ if (!targetFiltersApplied) targets = await applyPostFetchTargetFilters(targets);
7494
7713
  if (orderSpecs.length > 0) targets.sort((a, b) => this._compareByOrderSpecs(a, b, orderSpecs));
7495
7714
  if (relationConfig && typeof relationConfig === "object" && "with" in relationConfig) {
7496
7715
  const targetTableEdges = this._getTargetTableEdges(edge.targetTable);
@@ -7742,6 +7961,12 @@ var ConvexDeleteBuilder = class extends QueryPromise {
7742
7961
  if (!allowFullScan) throw new Error("update/delete without where() requires allowFullScan: true.");
7743
7962
  if (strict) console.warn("update/delete without where() is running with allowFullScan: true.");
7744
7963
  }
7964
+ const rls = ormContext?.rls;
7965
+ assertRlsRolesResolvable({
7966
+ table: this.table,
7967
+ operation: "delete",
7968
+ rls
7969
+ });
7745
7970
  let rows;
7746
7971
  let continueCursor = null;
7747
7972
  let isDone = true;
@@ -7843,7 +8068,6 @@ var ConvexDeleteBuilder = class extends QueryPromise {
7843
8068
  if (this.whereExpression) rows = rows.filter((row) => evaluateFilter(row, this.whereExpression));
7844
8069
  const results = [];
7845
8070
  let numAffected = 0;
7846
- const rls = ormContext?.rls;
7847
8071
  const foreignKeyGraph = ormContext?.foreignKeyGraph;
7848
8072
  if (!foreignKeyGraph) throw new Error("Foreign key actions require orm.db(ctx) configured from createOrm({ schema, ... }).");
7849
8073
  const cascadeMode = this.cascadeMode ?? (deleteMode === "soft" || deleteMode === "scheduled" ? "soft" : "hard");
@@ -8873,6 +9097,12 @@ var ConvexUpdateBuilder = class extends QueryPromise {
8873
9097
  if (!allowFullScan) throw new Error("update/delete without where() requires allowFullScan: true.");
8874
9098
  if (strict) console.warn("update/delete without where() is running with allowFullScan: true.");
8875
9099
  }
9100
+ const rls = ormContext?.rls;
9101
+ assertRlsRolesResolvable({
9102
+ table: this.table,
9103
+ operation: "update",
9104
+ rls
9105
+ });
8876
9106
  const onUpdateSet = {};
8877
9107
  for (const [columnName, builder] of Object.entries(getTableColumns$2(this.table))) {
8878
9108
  if (columnName in normalizedSetValues) continue;
@@ -8984,7 +9214,6 @@ var ConvexUpdateBuilder = class extends QueryPromise {
8984
9214
  maxRows
8985
9215
  });
8986
9216
  if (this.whereExpression) rows = rows.filter((row) => evaluateFilter(row, this.whereExpression));
8987
- const rls = ormContext?.rls;
8988
9217
  const foreignKeyGraph = ormContext?.foreignKeyGraph;
8989
9218
  if (!foreignKeyGraph) throw new Error("Foreign key actions require orm.db(ctx) configured from createOrm({ schema, ... }).");
8990
9219
  const updates = await Promise.all(rows.map(async (row) => {