kitcn 0.16.0 → 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.
- package/dist/aggregate/index.d.ts +1 -1
- package/dist/auth/client/index.js +1 -1
- package/dist/auth/index.js +19 -21
- package/dist/auth/nextjs/index.d.ts +1 -1
- package/dist/auth/nextjs/index.js +4 -4
- package/dist/{auth-store-ssZDPa37.js → auth-store-BnGZxmnY.js} +4 -1
- package/dist/{backend-core-DqPydYyx.mjs → backend-core-BsKP1LVg.mjs} +204 -164
- package/dist/{builder-DBgto1yn.js → builder-f4F_NRvK.js} +245 -153
- package/dist/{caller-factory-NEfgD5E0.js → caller-factory-DHywSoGZ.js} +7 -5
- package/dist/cli.mjs +14 -7
- package/dist/crpc/index.js +1 -127
- package/dist/{middleware-Bg-PdtrI.js → middleware-Cgrv2jIu.js} +1 -1
- package/dist/orm/index.d.ts +1 -1
- package/dist/orm/index.js +486 -121
- package/dist/plugins/index.js +1 -1
- package/dist/{procedure-caller-9m6NBxQu.js → procedure-caller-Rj6z3ai7.js} +1 -1
- package/dist/{procedure-name-Cy1AxayA.d.ts → procedure-name-Bo5KMcqc.d.ts} +15 -3
- package/dist/{query-context-ydn9kb6P.js → query-context-C90vNlc9.js} +131 -30
- package/dist/query-options-C_eBSIXG.js +247 -0
- package/dist/ratelimit/index.d.ts +26 -6
- package/dist/ratelimit/index.js +427 -100
- package/dist/ratelimit/react/index.d.ts +14 -0
- package/dist/ratelimit/react/index.js +149 -16
- package/dist/react/index.d.ts +3 -1
- package/dist/react/index.js +48 -15
- package/dist/rsc/index.js +22 -33
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +3 -3
- package/dist/solid/index.js +19 -5
- package/dist/watcher.mjs +2 -2
- package/dist/{where-clause-compiler-WF9UcrAB.d.ts → where-clause-compiler-BRhLW1dp.d.ts} +51 -0
- package/package.json +1 -1
- package/skills/kitcn/SKILL.md +1 -0
- package/skills/kitcn/references/features/create-plugins.md +1 -1
- package/skills/kitcn/references/features/orm.md +11 -1
- package/skills/kitcn/references/features/ratelimit.md +105 -0
- package/skills/kitcn/references/setup/server.md +1 -1
- 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
|
|
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
|
-
|
|
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
|
|
1244
|
-
case "ne": return fieldValue
|
|
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
|
|
1250
|
-
case "notInArray": return !
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
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 ||
|
|
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 (!
|
|
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,
|
|
@@ -3849,11 +3884,11 @@ var WhereClauseCompiler = class {
|
|
|
3849
3884
|
const selectedIndex = this.findLeadingIndex(operand.fieldName);
|
|
3850
3885
|
if (!selectedIndex) return null;
|
|
3851
3886
|
return {
|
|
3852
|
-
strategy: "
|
|
3887
|
+
strategy: "rangeIndex",
|
|
3853
3888
|
selectedIndex,
|
|
3854
|
-
indexFilters: [
|
|
3889
|
+
indexFilters: [lte(fieldRef(operand.fieldName), null)],
|
|
3855
3890
|
probeFilters: [],
|
|
3856
|
-
postFilters: []
|
|
3891
|
+
postFilters: [expression]
|
|
3857
3892
|
};
|
|
3858
3893
|
}
|
|
3859
3894
|
tryCompileIsNotNull(expression) {
|
|
@@ -4255,6 +4290,24 @@ var WhereClauseCompiler = class {
|
|
|
4255
4290
|
|
|
4256
4291
|
//#endregion
|
|
4257
4292
|
//#region src/orm/query.ts
|
|
4293
|
+
/**
|
|
4294
|
+
* Operators that Convex's `.filter()` cannot express, so `_toConvexExpression`
|
|
4295
|
+
* compiles them to a `() => true` placeholder and `_evaluatePostFetchFilter`
|
|
4296
|
+
* applies the real predicate in JavaScript after the rows are read.
|
|
4297
|
+
* Keep in sync with the string/array cases in `_toConvexExpression`.
|
|
4298
|
+
*/
|
|
4299
|
+
const POST_FETCH_ONLY_OPERATORS = new Set([
|
|
4300
|
+
"like",
|
|
4301
|
+
"ilike",
|
|
4302
|
+
"notLike",
|
|
4303
|
+
"notIlike",
|
|
4304
|
+
"startsWith",
|
|
4305
|
+
"endsWith",
|
|
4306
|
+
"contains",
|
|
4307
|
+
"arrayContains",
|
|
4308
|
+
"arrayContained",
|
|
4309
|
+
"arrayOverlaps"
|
|
4310
|
+
]);
|
|
4258
4311
|
const DEFAULT_RELATION_FAN_OUT_MAX_KEYS = 1e3;
|
|
4259
4312
|
const DEFAULT_AGGREGATE_CARTESIAN_MAX_KEYS = 4096;
|
|
4260
4313
|
const DEFAULT_AGGREGATE_WORK_BUDGET = 16384;
|
|
@@ -4265,37 +4318,90 @@ const RELATION_COUNT_ERROR = {
|
|
|
4265
4318
|
NOT_INDEXED: "RELATION_COUNT_NOT_INDEXED",
|
|
4266
4319
|
FILTER_UNSUPPORTED: "RELATION_COUNT_FILTER_UNSUPPORTED"
|
|
4267
4320
|
};
|
|
4268
|
-
|
|
4269
|
-
|
|
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) {
|
|
4270
4329
|
super();
|
|
4271
|
-
this.
|
|
4272
|
-
this.
|
|
4330
|
+
this.entries = entries;
|
|
4331
|
+
this.order = order;
|
|
4332
|
+
this.indexFields = indexFields;
|
|
4333
|
+
this.equalityIndexFilter = equalityIndexFilter;
|
|
4273
4334
|
}
|
|
4274
4335
|
iterWithKeys() {
|
|
4275
|
-
const
|
|
4276
|
-
const max = this.limit;
|
|
4336
|
+
const entries = this.entries;
|
|
4277
4337
|
return { [Symbol.asyncIterator]() {
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
if (seen >= max) return {
|
|
4338
|
+
let index = 0;
|
|
4339
|
+
return { next() {
|
|
4340
|
+
if (index >= entries.length) return Promise.resolve({
|
|
4282
4341
|
done: true,
|
|
4283
4342
|
value: void 0
|
|
4284
|
-
};
|
|
4285
|
-
const
|
|
4286
|
-
|
|
4287
|
-
return
|
|
4343
|
+
});
|
|
4344
|
+
const value = entries[index];
|
|
4345
|
+
index += 1;
|
|
4346
|
+
return Promise.resolve({
|
|
4347
|
+
done: false,
|
|
4348
|
+
value
|
|
4349
|
+
});
|
|
4288
4350
|
} };
|
|
4289
4351
|
} };
|
|
4290
4352
|
}
|
|
4291
4353
|
narrow(indexBounds) {
|
|
4292
|
-
return new
|
|
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);
|
|
4293
4399
|
}
|
|
4294
4400
|
getOrder() {
|
|
4295
4401
|
return this.inner.getOrder();
|
|
4296
4402
|
}
|
|
4297
4403
|
getIndexFields() {
|
|
4298
|
-
return this.inner.getIndexFields();
|
|
4404
|
+
return [...this.inner.getIndexFields(), PIPELINE_LIMIT_ORDINAL_FIELD];
|
|
4299
4405
|
}
|
|
4300
4406
|
getEqualityIndexFilter() {
|
|
4301
4407
|
return this.inner.getEqualityIndexFilter();
|
|
@@ -4450,7 +4556,7 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
4450
4556
|
return first ?? null;
|
|
4451
4557
|
}
|
|
4452
4558
|
async _applyRlsSelectFilter(rows, tableConfig) {
|
|
4453
|
-
if (!
|
|
4559
|
+
if (!tableConfig) return rows;
|
|
4454
4560
|
return await filterSelectRows({
|
|
4455
4561
|
table: tableConfig.table,
|
|
4456
4562
|
rows,
|
|
@@ -4514,21 +4620,29 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
4514
4620
|
return Object.values(this.schema).find((table) => table.name === dbName);
|
|
4515
4621
|
}
|
|
4516
4622
|
_matchLike(value, pattern, caseInsensitive) {
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4623
|
+
return matchLikePattern(value, pattern, caseInsensitive);
|
|
4624
|
+
}
|
|
4625
|
+
/**
|
|
4626
|
+
* True when the expression can be fully enforced by Convex's own `.filter()`.
|
|
4627
|
+
*
|
|
4628
|
+
* `_toConvexExpression` compiles the string/array operators to `() => true`
|
|
4629
|
+
* because Convex filters cannot run JavaScript string methods; those are
|
|
4630
|
+
* evaluated later by `_evaluatePostFetchFilter`. Pushing such an expression
|
|
4631
|
+
* into `.filter()` is not merely useless, it is wrong in two ways: a `take()`
|
|
4632
|
+
* downstream spends its budget on rows that have not been filtered yet, and a
|
|
4633
|
+
* surrounding `NOT` turns the `true` placeholder into `q.not(true)`, which
|
|
4634
|
+
* matches nothing. So the caller must know whether Convex can carry the whole
|
|
4635
|
+
* expression before relying on it.
|
|
4636
|
+
*/
|
|
4637
|
+
_isConvexEnforceableFilter(filter) {
|
|
4638
|
+
if (filter.type === "binary") return !POST_FETCH_ONLY_OPERATORS.has(filter.operator);
|
|
4639
|
+
if (filter.type === "unary") {
|
|
4640
|
+
const [operand] = filter.operands;
|
|
4641
|
+
if (isFieldReference(operand)) return true;
|
|
4642
|
+
return this._isConvexEnforceableFilter(operand);
|
|
4530
4643
|
}
|
|
4531
|
-
|
|
4644
|
+
if (filter.type === "logical") return filter.operands.every((operand) => this._isConvexEnforceableFilter(operand));
|
|
4645
|
+
return true;
|
|
4532
4646
|
}
|
|
4533
4647
|
/**
|
|
4534
4648
|
* Evaluate a filter expression against a fetched row
|
|
@@ -4572,14 +4686,14 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
4572
4686
|
case "contains":
|
|
4573
4687
|
if (typeof fieldValue !== "string") return false;
|
|
4574
4688
|
return fieldValue.includes(normalizedValue);
|
|
4575
|
-
case "eq": return fieldValue
|
|
4576
|
-
case "ne": return fieldValue
|
|
4689
|
+
case "eq": return filterValuesEqual(fieldValue, normalizedValue);
|
|
4690
|
+
case "ne": return !filterValuesEqual(fieldValue, normalizedValue);
|
|
4577
4691
|
case "gt": return fieldValue > comparableValue;
|
|
4578
4692
|
case "gte": return fieldValue >= comparableValue;
|
|
4579
4693
|
case "lt": return fieldValue < comparableValue;
|
|
4580
4694
|
case "lte": return fieldValue <= comparableValue;
|
|
4581
|
-
case "inArray": return
|
|
4582
|
-
case "notInArray": return !
|
|
4695
|
+
case "inArray": return filterValueInList(fieldValue, normalizedValue);
|
|
4696
|
+
case "notInArray": return !filterValueInList(fieldValue, normalizedValue);
|
|
4583
4697
|
case "arrayContains":
|
|
4584
4698
|
if (!Array.isArray(fieldValue)) return false;
|
|
4585
4699
|
return normalizedValue.every((item) => fieldValue.includes(item));
|
|
@@ -4627,8 +4741,8 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
4627
4741
|
if (this._isPlaceholder(filter) || this._isSQLWrapper(filter)) throw new Error("SQL placeholders are not supported in Convex filters.");
|
|
4628
4742
|
if (filter instanceof Date) return fieldValue === filter.getTime();
|
|
4629
4743
|
if (filter === null || typeof filter !== "object" || Array.isArray(filter)) {
|
|
4630
|
-
if (fieldName) return fieldValue
|
|
4631
|
-
return fieldValue
|
|
4744
|
+
if (fieldName) return filterValuesEqual(fieldValue, this._normalizeComparableValue(fieldName, filter));
|
|
4745
|
+
return filterValuesEqual(fieldValue, filter);
|
|
4632
4746
|
}
|
|
4633
4747
|
const entries = Object.entries(filter);
|
|
4634
4748
|
if (!entries.length) return true;
|
|
@@ -4661,7 +4775,7 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
4661
4775
|
continue;
|
|
4662
4776
|
}
|
|
4663
4777
|
const normalized = fieldName ? this._normalizeComparableValue(fieldName, value) : value;
|
|
4664
|
-
results.push(
|
|
4778
|
+
results.push(filterValueInList(fieldValue, normalized));
|
|
4665
4779
|
continue;
|
|
4666
4780
|
}
|
|
4667
4781
|
case "notIn": {
|
|
@@ -4670,7 +4784,7 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
4670
4784
|
continue;
|
|
4671
4785
|
}
|
|
4672
4786
|
const normalized = fieldName ? this._normalizeComparableValue(fieldName, value) : value;
|
|
4673
|
-
results.push(!
|
|
4787
|
+
results.push(!filterValueInList(fieldValue, normalized));
|
|
4674
4788
|
continue;
|
|
4675
4789
|
}
|
|
4676
4790
|
case "arrayContains":
|
|
@@ -4744,10 +4858,10 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
4744
4858
|
results.push(fieldValue.includes(value));
|
|
4745
4859
|
continue;
|
|
4746
4860
|
case "eq":
|
|
4747
|
-
results.push(fieldValue
|
|
4861
|
+
results.push(filterValuesEqual(fieldValue, fieldName ? this._normalizeComparableValue(fieldName, value) : value));
|
|
4748
4862
|
continue;
|
|
4749
4863
|
case "ne":
|
|
4750
|
-
results.push(fieldValue
|
|
4864
|
+
results.push(!filterValuesEqual(fieldValue, fieldName ? this._normalizeComparableValue(fieldName, value) : value));
|
|
4751
4865
|
continue;
|
|
4752
4866
|
case "gt":
|
|
4753
4867
|
results.push(fieldValue > (fieldName ? this._normalizeComparableValue(fieldName, value) : value));
|
|
@@ -5215,6 +5329,67 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
5215
5329
|
row[config.alias] = nested;
|
|
5216
5330
|
}
|
|
5217
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
|
+
}
|
|
5218
5393
|
async _finalizeRows(rows) {
|
|
5219
5394
|
const polymorphicState = this._resolvePolymorphicFinalizeState();
|
|
5220
5395
|
const requestedWith = this.config.with;
|
|
@@ -5223,6 +5398,7 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
5223
5398
|
const extrasConfig = this.config.extras;
|
|
5224
5399
|
const resolvedExtras = typeof extrasConfig === "function" ? extrasConfig(tableColumns) : extrasConfig;
|
|
5225
5400
|
if (polymorphicState) this._assertPolymorphicAliasCollisions(polymorphicState.configs, requestedWith, resolvedExtras);
|
|
5401
|
+
this._assertRlsSelectPlan(effectiveWith, this.tableConfig, this.edgeMetadata, 0, 3);
|
|
5226
5402
|
let rowsWithRelations = rows;
|
|
5227
5403
|
if (effectiveWith) rowsWithRelations = await this._loadRelations(rowsWithRelations, effectiveWith, 0, 3, this.edgeMetadata, this.tableConfig);
|
|
5228
5404
|
if (polymorphicState) this._synthesizePolymorphicRows(rowsWithRelations, polymorphicState.configs);
|
|
@@ -5317,6 +5493,33 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
5317
5493
|
}
|
|
5318
5494
|
throw new Error(`${context} where callback must return a filter expression or predicate(...).`);
|
|
5319
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
|
+
}
|
|
5320
5523
|
_buildBasePipelineStream(queryConfig, wherePredicate, configuredIndex) {
|
|
5321
5524
|
const schemaDefinition = this._getSchemaDefinitionOrThrow();
|
|
5322
5525
|
let streamQuery = stream(this.db, schemaDefinition).query(this.tableConfig.name);
|
|
@@ -5340,6 +5543,38 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
5340
5543
|
});
|
|
5341
5544
|
return streamQuery;
|
|
5342
5545
|
}
|
|
5546
|
+
/**
|
|
5547
|
+
* Stream equivalent of the `db.query(...)` chain, used when a post-fetch
|
|
5548
|
+
* filter has to run in JavaScript before `limit` or a page boundary can be
|
|
5549
|
+
* applied. `filterWith` evaluates the predicate as rows are pulled, so
|
|
5550
|
+
* `take`/`paginate` size by matches instead of by scanned rows.
|
|
5551
|
+
*
|
|
5552
|
+
* It deliberately mirrors the index and order decisions the caller already
|
|
5553
|
+
* made for the plain query rather than re-deriving them, so switching to the
|
|
5554
|
+
* stream cannot change which index is scanned or in what direction.
|
|
5555
|
+
*
|
|
5556
|
+
* Returns null when the schema definition needed by `stream()` is missing;
|
|
5557
|
+
* the caller then falls back to its plain-query path.
|
|
5558
|
+
*/
|
|
5559
|
+
_buildResidualFilterStream(params) {
|
|
5560
|
+
const schemaDefinition = this.schema[OrmSchemaDefinition];
|
|
5561
|
+
if (!schemaDefinition) return null;
|
|
5562
|
+
const { queryConfig, configuredIndex, membershipFilter, orderIndexName, primaryOrder, fallbackOrder } = params;
|
|
5563
|
+
let streamQuery = stream(this.db, schemaDefinition).query(this.tableConfig.name);
|
|
5564
|
+
if (queryConfig.index) streamQuery = streamQuery.withIndex(queryConfig.index.name, (q) => {
|
|
5565
|
+
let indexQuery = q;
|
|
5566
|
+
for (const filter of queryConfig.index.filters) indexQuery = this._applyFilterToQuery(indexQuery, filter);
|
|
5567
|
+
return indexQuery;
|
|
5568
|
+
});
|
|
5569
|
+
else if (configuredIndex?.name) streamQuery = streamQuery.withIndex(configuredIndex.name, configuredIndex.range ? configuredIndex.range : (q) => q);
|
|
5570
|
+
else if (orderIndexName) streamQuery = streamQuery.withIndex(orderIndexName, (q) => q);
|
|
5571
|
+
streamQuery = streamQuery.order(primaryOrder ? primaryOrder.direction : fallbackOrder);
|
|
5572
|
+
streamQuery = streamQuery.filterWith(async (row) => {
|
|
5573
|
+
for (const filter of queryConfig.postFilters) if (!this._evaluatePostFetchFilter(row, filter)) return false;
|
|
5574
|
+
return membershipFilter ? await membershipFilter(row) : true;
|
|
5575
|
+
});
|
|
5576
|
+
return streamQuery;
|
|
5577
|
+
}
|
|
5343
5578
|
_buildUnionSourceStream(source, fallbackOrder) {
|
|
5344
5579
|
const configuredIndex = this.configuredIndex;
|
|
5345
5580
|
this._assertWhereIndexRequirement({
|
|
@@ -5365,11 +5600,18 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
5365
5600
|
const targetFields = edge.cardinality === "one" ? edge.targetFields.length > 0 ? edge.targetFields : ["_id"] : edge.targetFields.length > 0 ? edge.targetFields : [edge.fieldName];
|
|
5366
5601
|
const targetTableConfig = this._getTableConfigByDbName(edge.targetTable);
|
|
5367
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
|
+
});
|
|
5368
5608
|
const strict = this.tableConfig.strict !== false;
|
|
5369
5609
|
const indexName = targetFields.length === 1 && targetFields[0] === "_id" ? "by_id" : findRelationIndex(targetTableConfig.table, targetFields, `${this.tableConfig.name}.${relationName}`, edge.targetTable, strict, this.allowFullScan);
|
|
5370
5610
|
const outerOrder = sourceStream.getOrder();
|
|
5371
5611
|
const schemaDefinition = this._getSchemaDefinitionOrThrow();
|
|
5372
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];
|
|
5373
5615
|
const stageWherePredicate = this._buildTableFilterPredicate(stage.where, targetTableConfig);
|
|
5374
5616
|
this._assertWhereIndexRequirement({
|
|
5375
5617
|
where: stage.where,
|
|
@@ -5379,21 +5621,21 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
5379
5621
|
});
|
|
5380
5622
|
return sourceStream.flatMap(async (parent) => {
|
|
5381
5623
|
const values = sourceFields.map((field) => parent[field]);
|
|
5382
|
-
if (values.some((value) => value === null || value === void 0)) return new EmptyStream(outerOrder,
|
|
5624
|
+
if (values.some((value) => value === null || value === void 0)) return new EmptyStream(outerOrder, mappedIndexFields);
|
|
5383
5625
|
let inner = stream(this.db, schemaDefinition).query(edge.targetTable);
|
|
5384
5626
|
if (indexName) inner = inner.withIndex(indexName, (q) => this._applyEqBounds(q, targetFields, values));
|
|
5385
5627
|
inner = inner.order(outerOrder);
|
|
5386
5628
|
if (stageWherePredicate) inner = inner.filterWith(stageWherePredicate);
|
|
5387
|
-
if (
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
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);
|
|
5391
5633
|
if (stage.includeParent ?? true) inner = inner.map(async (child) => ({
|
|
5392
5634
|
parent,
|
|
5393
5635
|
child
|
|
5394
5636
|
}));
|
|
5395
5637
|
return inner;
|
|
5396
|
-
},
|
|
5638
|
+
}, mappedIndexFields);
|
|
5397
5639
|
}
|
|
5398
5640
|
async _applyPipelineStages(baseStream, pipeline) {
|
|
5399
5641
|
let streamQuery = baseStream;
|
|
@@ -6379,8 +6621,11 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
6379
6621
|
if (vectorSearchConfig) throw new Error("pageByKey cannot be combined with vectorSearch.");
|
|
6380
6622
|
if (pipeline) throw new Error("pageByKey cannot be combined with pipeline.");
|
|
6381
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);
|
|
6382
6627
|
const idLookup = this._extractIdOnlyWhere(whereFilter);
|
|
6383
|
-
if (idLookup && !vectorSearchConfig && !searchConfig && !wherePredicate && !isCursorPaginated && configuredIndex === void 0) {
|
|
6628
|
+
if (idLookup && !vectorSearchConfig && !searchConfig && !wherePredicate && !isCursorPaginated && !pipeline && !pageByKey && endCursor === void 0 && configuredIndex === void 0) {
|
|
6384
6629
|
const orderSpecs = this._orderBySpecs(config.orderBy);
|
|
6385
6630
|
const offset = config.offset ?? 0;
|
|
6386
6631
|
if (offset !== void 0 && typeof offset !== "number") throw new Error("Only numeric offset is supported in kitcn ORM.");
|
|
@@ -6395,7 +6640,7 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
6395
6640
|
const selectedRows = await this._finalizeRows(rows);
|
|
6396
6641
|
return this._returnSelectedRows(selectedRows);
|
|
6397
6642
|
}
|
|
6398
|
-
const queryConfig = this._toConvexQuery(whereExpressionFromCallback);
|
|
6643
|
+
const queryConfig = this._toConvexQuery(whereExpressionFromCallback, configuredIndex);
|
|
6399
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.");
|
|
6400
6645
|
if (pageByKey) {
|
|
6401
6646
|
const schemaDefinition = this._getSchemaDefinitionOrThrow();
|
|
@@ -6433,7 +6678,23 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
6433
6678
|
if (!pipeline?.interleaveBy || pipeline.interleaveBy.length === 0) throw new Error("pipeline.interleaveBy is required when pipeline.union has multiple sources.");
|
|
6434
6679
|
streamQuery = mergedStream(streams, pipeline.interleaveBy.map((field) => this._normalizePublicFieldName(field)));
|
|
6435
6680
|
}
|
|
6436
|
-
} else
|
|
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
|
+
}
|
|
6437
6698
|
if (pipeline) streamQuery = await this._applyPipelineStages(streamQuery, pipeline);
|
|
6438
6699
|
if (isCursorPaginated) {
|
|
6439
6700
|
const paginationResult = await streamQuery.paginate({
|
|
@@ -6443,7 +6704,7 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
6443
6704
|
maxScan
|
|
6444
6705
|
});
|
|
6445
6706
|
return {
|
|
6446
|
-
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)),
|
|
6447
6708
|
continueCursor: paginationResult.continueCursor,
|
|
6448
6709
|
isDone: paginationResult.isDone,
|
|
6449
6710
|
pageStatus: paginationResult.pageStatus,
|
|
@@ -6455,7 +6716,7 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
6455
6716
|
const limit = this._resolveNonPaginatedLimit(config);
|
|
6456
6717
|
let rows = limit === void 0 ? await streamQuery.collect() : await streamQuery.take(offset > 0 ? offset + limit : limit);
|
|
6457
6718
|
if (offset > 0) rows = rows.slice(offset);
|
|
6458
|
-
rows = await this._applyRlsSelectFilter(rows, this.tableConfig);
|
|
6719
|
+
if (!rootRlsEnabled) rows = await this._applyRlsSelectFilter(rows, this.tableConfig);
|
|
6459
6720
|
const selectedRows = await this._finalizeRows(rows);
|
|
6460
6721
|
return this._returnSelectedRows(selectedRows);
|
|
6461
6722
|
}
|
|
@@ -6561,7 +6822,7 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
6561
6822
|
const orderField = primaryOrder.field;
|
|
6562
6823
|
if (orderField === "_creationTime") query = query.order(primaryOrder.direction);
|
|
6563
6824
|
else {
|
|
6564
|
-
const orderIndex = getIndexes(this.tableConfig.table).find((idx) => idx.fields
|
|
6825
|
+
const orderIndex = getIndexes(this.tableConfig.table).find((idx) => idx.fields[0] === orderField) ?? this.edgeMetadata.find((idx) => idx.indexFields[0] === orderField);
|
|
6565
6826
|
if (orderIndex) {
|
|
6566
6827
|
orderIndexName = "indexName" in orderIndex ? orderIndex.indexName : orderIndex.name;
|
|
6567
6828
|
query = query.withIndex(orderIndexName, (q) => q);
|
|
@@ -6643,7 +6904,7 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
6643
6904
|
if (queryConfig.postFilters.length > 0) rows = rows.filter((row) => queryConfig.postFilters.every((filter) => this._evaluatePostFetchFilter(row, filter)));
|
|
6644
6905
|
rows = await this._applyRlsSelectFilter(rows, this.tableConfig);
|
|
6645
6906
|
if (whereFilter) rows = await this._applyRelationsFilterToRows(rows, this.tableConfig, whereFilter, this.edgeMetadata, 0, 3, this.config.with);
|
|
6646
|
-
if (
|
|
6907
|
+
if (postFetchOrders.length > 0) rows = rows.sort((a, b) => this._compareByOrderSpecs(a, b, postFetchOrders));
|
|
6647
6908
|
const offset = config.offset ?? 0;
|
|
6648
6909
|
if (typeof offset !== "number") throw new Error("Only numeric offset is supported in kitcn ORM.");
|
|
6649
6910
|
const limit = this._resolveNonPaginatedLimit(config);
|
|
@@ -6652,6 +6913,12 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
6652
6913
|
const selectedRows = await this._finalizeRows(rows);
|
|
6653
6914
|
return this._returnSelectedRows(selectedRows);
|
|
6654
6915
|
}
|
|
6916
|
+
const matchesPostFetchMembership = async (row) => {
|
|
6917
|
+
const visibleRows = await this._applyRlsSelectFilter([row], this.tableConfig);
|
|
6918
|
+
if (visibleRows.length === 0) return false;
|
|
6919
|
+
if (!whereFilter) return true;
|
|
6920
|
+
return (await this._applyRelationsFilterToRows(visibleRows, this.tableConfig, whereFilter, this.edgeMetadata, 0, 3, this.config.with)).length > 0;
|
|
6921
|
+
};
|
|
6655
6922
|
if (isCursorPaginated) {
|
|
6656
6923
|
if (queryConfig.strategy === "multiProbe") if (maxScan === void 0) {
|
|
6657
6924
|
if (strict) throw new Error("Pagination with multi-probe index-union filters requires maxScan when strict=true. Add maxScan or make the query indexable.");
|
|
@@ -6691,6 +6958,7 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
6691
6958
|
const schemaDefinition = this.schema[OrmSchemaDefinition];
|
|
6692
6959
|
if (!schemaDefinition) throw new Error("Pagination with maxScan requires defineSchema(). Ensure defineSchema(tables) was used with the same tables object passed to defineRelations.");
|
|
6693
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);
|
|
6694
6962
|
if (queryConfig.order && primaryOrder) {
|
|
6695
6963
|
if (needsPostFetchSortForPrimary) {
|
|
6696
6964
|
if (strict) throw new Error(`Pagination: Field '${primaryOrder.field}' has no index. Add an index or disable strict.`);
|
|
@@ -6716,9 +6984,40 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
6716
6984
|
splitCursor: paginationResult.splitCursor
|
|
6717
6985
|
};
|
|
6718
6986
|
}
|
|
6719
|
-
|
|
6987
|
+
const convexPostFilters = queryConfig.postFilters.filter((filter) => this._isConvexEnforceableFilter(filter));
|
|
6988
|
+
const hasResidualPostFilter = convexPostFilters.length !== queryConfig.postFilters.length;
|
|
6989
|
+
const residualPageStream = hasResidualPostFilter ? this._buildResidualFilterStream({
|
|
6990
|
+
queryConfig,
|
|
6991
|
+
configuredIndex,
|
|
6992
|
+
membershipFilter: matchesPostFetchMembership,
|
|
6993
|
+
orderIndexName,
|
|
6994
|
+
primaryOrder,
|
|
6995
|
+
fallbackOrder: "desc"
|
|
6996
|
+
}) : null;
|
|
6997
|
+
if (hasResidualPostFilter && !residualPageStream) this._getSchemaDefinitionOrThrow();
|
|
6998
|
+
if (residualPageStream) {
|
|
6999
|
+
if (queryConfig.order && primaryOrder) {
|
|
7000
|
+
if (needsPostFetchSortForPrimary && strict) throw new Error(`Pagination: Field '${primaryOrder.field}' has no index. Add an index or disable strict.`);
|
|
7001
|
+
if (needsPostFetchSortForPrimary) console.warn(`Pagination: Field '${primaryOrder.field}' has no index. Falling back to _creationTime ordering.`);
|
|
7002
|
+
if (hasSecondaryOrders) console.warn("Pagination: Only the first orderBy field is used for cursor ordering. Secondary orderBy fields are applied per page and may be unstable across pages.");
|
|
7003
|
+
}
|
|
7004
|
+
const paginationResult = await residualPageStream.paginate({
|
|
7005
|
+
cursor: cursor ?? null,
|
|
7006
|
+
endCursor: endCursor ?? void 0,
|
|
7007
|
+
limit: config.limit,
|
|
7008
|
+
maxScan
|
|
7009
|
+
});
|
|
7010
|
+
return {
|
|
7011
|
+
page: await this._finalizeRows(paginationResult.page),
|
|
7012
|
+
continueCursor: paginationResult.continueCursor,
|
|
7013
|
+
isDone: paginationResult.isDone,
|
|
7014
|
+
pageStatus: paginationResult.pageStatus,
|
|
7015
|
+
splitCursor: paginationResult.splitCursor
|
|
7016
|
+
};
|
|
7017
|
+
}
|
|
7018
|
+
if (convexPostFilters.length > 0) query = query.filter((q) => {
|
|
6720
7019
|
let result = null;
|
|
6721
|
-
for (const filter of
|
|
7020
|
+
for (const filter of convexPostFilters) {
|
|
6722
7021
|
const expr = this._toConvexExpression(filter)(q);
|
|
6723
7022
|
result = result ? q.and(result, expr) : expr;
|
|
6724
7023
|
}
|
|
@@ -6745,9 +7044,11 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
6745
7044
|
isDone: paginationResult.isDone
|
|
6746
7045
|
};
|
|
6747
7046
|
}
|
|
6748
|
-
|
|
7047
|
+
const convexPostFilters = queryConfig.postFilters.filter((filter) => this._isConvexEnforceableFilter(filter));
|
|
7048
|
+
const hasResidualPostFilter = convexPostFilters.length !== queryConfig.postFilters.length;
|
|
7049
|
+
if (convexPostFilters.length > 0) query = query.filter((q) => {
|
|
6749
7050
|
let result = null;
|
|
6750
|
-
for (const filter of
|
|
7051
|
+
for (const filter of convexPostFilters) {
|
|
6751
7052
|
const expr = this._toConvexExpression(filter)(q);
|
|
6752
7053
|
result = result ? q.and(result, expr) : expr;
|
|
6753
7054
|
}
|
|
@@ -6757,11 +7058,29 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
6757
7058
|
if (typeof offset !== "number") throw new Error("Only numeric offset is supported in kitcn ORM.");
|
|
6758
7059
|
const limit = this._resolveNonPaginatedLimit(config);
|
|
6759
7060
|
const paginateAfterPostFetchSort = usePostFetchSort && postFetchOrders.length > 0;
|
|
6760
|
-
|
|
6761
|
-
|
|
7061
|
+
const sizeAfterPostFilter = hasResidualPostFilter && !paginateAfterPostFetchSort;
|
|
7062
|
+
const residualLimitStream = sizeAfterPostFilter && limit !== void 0 ? this._buildResidualFilterStream({
|
|
7063
|
+
queryConfig,
|
|
7064
|
+
configuredIndex,
|
|
7065
|
+
membershipFilter: matchesPostFetchMembership,
|
|
7066
|
+
orderIndexName,
|
|
7067
|
+
primaryOrder,
|
|
7068
|
+
fallbackOrder: "asc"
|
|
7069
|
+
}) : null;
|
|
7070
|
+
let rows;
|
|
7071
|
+
if (residualLimitStream) rows = await residualLimitStream.take(offset > 0 ? offset + limit : limit);
|
|
7072
|
+
else if (limit === void 0 || paginateAfterPostFetchSort || hasResidualPostFilter) rows = await query.collect();
|
|
7073
|
+
else rows = await query.take(offset > 0 ? offset + limit : limit);
|
|
7074
|
+
if (!(paginateAfterPostFetchSort || sizeAfterPostFilter) && offset > 0) rows = rows.slice(offset);
|
|
6762
7075
|
if (queryConfig.postFilters.length > 0) rows = rows.filter((row) => queryConfig.postFilters.every((filter) => this._evaluatePostFetchFilter(row, filter)));
|
|
6763
|
-
|
|
6764
|
-
|
|
7076
|
+
if (!residualLimitStream) {
|
|
7077
|
+
rows = await this._applyRlsSelectFilter(rows, this.tableConfig);
|
|
7078
|
+
if (whereFilter) rows = await this._applyRelationsFilterToRows(rows, this.tableConfig, whereFilter, this.edgeMetadata, 0, 3, this.config.with);
|
|
7079
|
+
}
|
|
7080
|
+
if (sizeAfterPostFilter) {
|
|
7081
|
+
if (offset > 0) rows = rows.slice(offset);
|
|
7082
|
+
if (limit !== void 0) rows = rows.slice(0, limit);
|
|
7083
|
+
}
|
|
6765
7084
|
if (usePostFetchSort && postFetchOrders.length > 0) rows = rows.sort((a, b) => this._compareByOrderSpecs(a, b, postFetchOrders));
|
|
6766
7085
|
if (paginateAfterPostFetchSort) {
|
|
6767
7086
|
if (offset > 0) rows = rows.slice(offset);
|
|
@@ -6774,7 +7093,7 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
6774
7093
|
* Convert query config to Convex query parameters
|
|
6775
7094
|
* Phase 4 implementation with WhereClauseCompiler
|
|
6776
7095
|
*/
|
|
6777
|
-
_toConvexQuery(whereExpressionOverride) {
|
|
7096
|
+
_toConvexQuery(whereExpressionOverride, configuredIndex) {
|
|
6778
7097
|
const config = this.config;
|
|
6779
7098
|
const tableIndexes = getIndexes(this.tableConfig.table).map((index) => ({
|
|
6780
7099
|
indexName: index.name,
|
|
@@ -6783,12 +7102,20 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
6783
7102
|
const compiler = new WhereClauseCompiler(this.tableConfig.table.tableName, tableIndexes);
|
|
6784
7103
|
let whereExpression = whereExpressionOverride;
|
|
6785
7104
|
if (!whereExpression && config.where && typeof config.where !== "function") whereExpression = this._buildFilterExpression(config.where, this.tableConfig);
|
|
6786
|
-
const
|
|
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
|
+
};
|
|
6787
7114
|
const result = {
|
|
6788
7115
|
table: this.tableConfig.table.tableName,
|
|
6789
7116
|
strategy: compiled.strategy,
|
|
6790
|
-
probeFilters: compiled.probeFilters,
|
|
6791
|
-
postFilters: compiled.postFilters
|
|
7117
|
+
probeFilters: [...compiled.probeFilters],
|
|
7118
|
+
postFilters: [...compiled.postFilters]
|
|
6792
7119
|
};
|
|
6793
7120
|
if (compiled.selectedIndex && (compiled.indexFilters.length > 0 || compiled.probeFilters.length > 0)) result.index = {
|
|
6794
7121
|
name: compiled.selectedIndex.indexName,
|
|
@@ -7274,6 +7601,7 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
7274
7601
|
if (!targetTableConfig) throw new Error(`Relation '${relationName}' target table '${edge.targetTable}' not found.`);
|
|
7275
7602
|
const relationDefinition = tableConfig.relations[relationName];
|
|
7276
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);
|
|
7277
7605
|
let orderSpecs = [];
|
|
7278
7606
|
if (relationConfig && typeof relationConfig === "object" && "orderBy" in relationConfig) {
|
|
7279
7607
|
let orderByValue = relationConfig.orderBy;
|
|
@@ -7294,17 +7622,34 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
7294
7622
|
if (effectivePerParentLimit !== void 0) result = result.slice(0, effectivePerParentLimit);
|
|
7295
7623
|
return result;
|
|
7296
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
|
+
};
|
|
7297
7640
|
let targets = [];
|
|
7298
7641
|
let throughBySourceKey;
|
|
7642
|
+
let targetFiltersApplied = false;
|
|
7299
7643
|
if (edge.through) {
|
|
7300
7644
|
const throughTableConfig = this._getTableConfigByDbName(edge.through.table);
|
|
7301
7645
|
if (!throughTableConfig) throw new Error(`Relation '${relationName}' through table '${edge.through.table}' not found.`);
|
|
7302
7646
|
const throughIndexName = findRelationIndex(throughTableConfig.table, edge.through.sourceFields, `${tableConfig.name}.${relationName}`, edge.through.table, strict, this.allowFullScan);
|
|
7303
7647
|
const entries = Array.from(sourceKeyMap.entries());
|
|
7304
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);
|
|
7305
7650
|
return {
|
|
7306
7651
|
key,
|
|
7307
|
-
rows: await this.
|
|
7652
|
+
rows: await this._applyRlsSelectFilter(await query.collect(), throughTableConfig)
|
|
7308
7653
|
};
|
|
7309
7654
|
});
|
|
7310
7655
|
throughBySourceKey = /* @__PURE__ */ new Map();
|
|
@@ -7341,27 +7686,30 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
7341
7686
|
} else {
|
|
7342
7687
|
const indexName = findRelationIndex(targetTableConfig.table, targetFields, `${tableConfig.name}.${relationName}`, edge.targetTable, strict, this.allowFullScan);
|
|
7343
7688
|
const entries = Array.from(sourceKeyMap.entries());
|
|
7689
|
+
const streamPostFetchTargetFilters = orderSpecs.length === 0 && hasPostFetchTargetFilter && effectivePerParentLimit !== void 0;
|
|
7690
|
+
targetFiltersApplied = streamPostFetchTargetFilters;
|
|
7344
7691
|
targets = (await this._mapWithConcurrency(entries, async ([, values]) => {
|
|
7345
7692
|
const query = this._queryByFields(this.db.query(edge.targetTable), targetFields, values, indexName);
|
|
7346
|
-
if (orderSpecs.length === 0 && effectivePerParentLimit !== void 0) {
|
|
7693
|
+
if (orderSpecs.length === 0 && !hasPostFetchTargetFilter && effectivePerParentLimit !== void 0) {
|
|
7347
7694
|
const fetchLimit = (perParentOffset ?? 0) + (effectivePerParentLimit ?? 0);
|
|
7348
7695
|
return await query.take(fetchLimit);
|
|
7349
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
|
+
}
|
|
7350
7709
|
return await query.collect();
|
|
7351
7710
|
})).flat();
|
|
7352
7711
|
}
|
|
7353
|
-
targets = await
|
|
7354
|
-
if (relationDefinition?.where) targets = targets.filter((target) => this._evaluateTableFilter(target, targetTableConfig, relationDefinition.where));
|
|
7355
|
-
if (relationConfig && typeof relationConfig === "object" && "where" in relationConfig) {
|
|
7356
|
-
const whereFilter = relationConfig.where;
|
|
7357
|
-
if (typeof whereFilter === "function") {
|
|
7358
|
-
const whereExpression = this._resolveWhereCallbackExpression(whereFilter, targetTableConfig, { context: "relation" });
|
|
7359
|
-
if (whereExpression && !this._isPredicateWhereClause(whereExpression)) targets = targets.filter((target) => this._evaluatePostFetchFilter(target, whereExpression));
|
|
7360
|
-
} else if (whereFilter) {
|
|
7361
|
-
const targetEdges = this._getTargetTableEdges(edge.targetTable);
|
|
7362
|
-
targets = await this._applyRelationsFilterToRows(targets, targetTableConfig, whereFilter, targetEdges, depth + 1, maxDepth, relationConfig.with);
|
|
7363
|
-
}
|
|
7364
|
-
}
|
|
7712
|
+
if (!targetFiltersApplied) targets = await applyPostFetchTargetFilters(targets);
|
|
7365
7713
|
if (orderSpecs.length > 0) targets.sort((a, b) => this._compareByOrderSpecs(a, b, orderSpecs));
|
|
7366
7714
|
if (relationConfig && typeof relationConfig === "object" && "with" in relationConfig) {
|
|
7367
7715
|
const targetTableEdges = this._getTargetTableEdges(edge.targetTable);
|
|
@@ -7613,6 +7961,12 @@ var ConvexDeleteBuilder = class extends QueryPromise {
|
|
|
7613
7961
|
if (!allowFullScan) throw new Error("update/delete without where() requires allowFullScan: true.");
|
|
7614
7962
|
if (strict) console.warn("update/delete without where() is running with allowFullScan: true.");
|
|
7615
7963
|
}
|
|
7964
|
+
const rls = ormContext?.rls;
|
|
7965
|
+
assertRlsRolesResolvable({
|
|
7966
|
+
table: this.table,
|
|
7967
|
+
operation: "delete",
|
|
7968
|
+
rls
|
|
7969
|
+
});
|
|
7616
7970
|
let rows;
|
|
7617
7971
|
let continueCursor = null;
|
|
7618
7972
|
let isDone = true;
|
|
@@ -7714,7 +8068,6 @@ var ConvexDeleteBuilder = class extends QueryPromise {
|
|
|
7714
8068
|
if (this.whereExpression) rows = rows.filter((row) => evaluateFilter(row, this.whereExpression));
|
|
7715
8069
|
const results = [];
|
|
7716
8070
|
let numAffected = 0;
|
|
7717
|
-
const rls = ormContext?.rls;
|
|
7718
8071
|
const foreignKeyGraph = ormContext?.foreignKeyGraph;
|
|
7719
8072
|
if (!foreignKeyGraph) throw new Error("Foreign key actions require orm.db(ctx) configured from createOrm({ schema, ... }).");
|
|
7720
8073
|
const cascadeMode = this.cascadeMode ?? (deleteMode === "soft" || deleteMode === "scheduled" ? "soft" : "hard");
|
|
@@ -8744,6 +9097,12 @@ var ConvexUpdateBuilder = class extends QueryPromise {
|
|
|
8744
9097
|
if (!allowFullScan) throw new Error("update/delete without where() requires allowFullScan: true.");
|
|
8745
9098
|
if (strict) console.warn("update/delete without where() is running with allowFullScan: true.");
|
|
8746
9099
|
}
|
|
9100
|
+
const rls = ormContext?.rls;
|
|
9101
|
+
assertRlsRolesResolvable({
|
|
9102
|
+
table: this.table,
|
|
9103
|
+
operation: "update",
|
|
9104
|
+
rls
|
|
9105
|
+
});
|
|
8747
9106
|
const onUpdateSet = {};
|
|
8748
9107
|
for (const [columnName, builder] of Object.entries(getTableColumns$2(this.table))) {
|
|
8749
9108
|
if (columnName in normalizedSetValues) continue;
|
|
@@ -8855,7 +9214,6 @@ var ConvexUpdateBuilder = class extends QueryPromise {
|
|
|
8855
9214
|
maxRows
|
|
8856
9215
|
});
|
|
8857
9216
|
if (this.whereExpression) rows = rows.filter((row) => evaluateFilter(row, this.whereExpression));
|
|
8858
|
-
const rls = ormContext?.rls;
|
|
8859
9217
|
const foreignKeyGraph = ormContext?.foreignKeyGraph;
|
|
8860
9218
|
if (!foreignKeyGraph) throw new Error("Foreign key actions require orm.db(ctx) configured from createOrm({ schema, ... }).");
|
|
8861
9219
|
const updates = await Promise.all(rows.map(async (row) => {
|
|
@@ -10122,6 +10480,8 @@ function scheduledDeleteFactory(schema, edgeMetadata, scheduledMutationBatch) {
|
|
|
10122
10480
|
|
|
10123
10481
|
//#endregion
|
|
10124
10482
|
//#region src/orm/scheduled-mutation-batch.ts
|
|
10483
|
+
/** Column stamped by `softDeleteRow`; see mutation-utils.ts. */
|
|
10484
|
+
const DELETION_TIME_FIELD = "deletionTime";
|
|
10125
10485
|
const isRecord = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
10126
10486
|
function scheduledMutationBatchFactory(schema, edgeMetadata, scheduledMutationBatch) {
|
|
10127
10487
|
const tableByName = /* @__PURE__ */ new Map();
|
|
@@ -10185,12 +10545,17 @@ function scheduledMutationBatchFactory(schema, edgeMetadata, scheduledMutationBa
|
|
|
10185
10545
|
const targetValues = decodeUndefinedDeep(args.targetValues);
|
|
10186
10546
|
if (!targetValues || !Array.isArray(targetValues)) throw new Error("scheduledMutationBatch: targetValues are required for cascade work.");
|
|
10187
10547
|
if (!args.foreignIndexName) throw new Error("scheduledMutationBatch: foreignIndexName is required for cascade work.");
|
|
10188
|
-
const queryWithIndex = () => ctx.db.query(args.table).withIndex(args.foreignIndexName, (q) => {
|
|
10189
|
-
let builder = q.eq(sourceColumns[0], targetValues[0]);
|
|
10190
|
-
for (let i = 1; i < sourceColumns.length; i += 1) builder = builder.eq(sourceColumns[i], targetValues[i]);
|
|
10191
|
-
return builder;
|
|
10192
|
-
});
|
|
10193
10548
|
const action = args.foreignAction ?? "no action";
|
|
10549
|
+
const skipSoftDeletedRows = workType === "cascade-delete" && action === "cascade" && (args.cascadeMode ?? "hard") === "soft";
|
|
10550
|
+
const queryWithIndex = () => {
|
|
10551
|
+
const indexed = ctx.db.query(args.table).withIndex(args.foreignIndexName, (q) => {
|
|
10552
|
+
let builder = q.eq(sourceColumns[0], targetValues[0]);
|
|
10553
|
+
for (let i = 1; i < sourceColumns.length; i += 1) builder = builder.eq(sourceColumns[i], targetValues[i]);
|
|
10554
|
+
return builder;
|
|
10555
|
+
});
|
|
10556
|
+
if (!skipSoftDeletedRows) return indexed;
|
|
10557
|
+
return indexed.filter((q) => q.or(q.eq(q.field(DELETION_TIME_FIELD), void 0), q.eq(q.field(DELETION_TIME_FIELD), null)));
|
|
10558
|
+
};
|
|
10194
10559
|
const paged = await queryWithIndex().paginate({
|
|
10195
10560
|
cursor: null,
|
|
10196
10561
|
numItems: args.batchSize
|