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.
- 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 +335 -106
- 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-B47_3n97.js → query-context-C90vNlc9.js} +105 -22
- 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-C8UKgzTO.d.ts → where-clause-compiler-BRhLW1dp.d.ts} +24 -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,
|
|
@@ -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
|
-
|
|
4287
|
-
|
|
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.
|
|
4290
|
-
this.
|
|
4330
|
+
this.entries = entries;
|
|
4331
|
+
this.order = order;
|
|
4332
|
+
this.indexFields = indexFields;
|
|
4333
|
+
this.equalityIndexFilter = equalityIndexFilter;
|
|
4291
4334
|
}
|
|
4292
4335
|
iterWithKeys() {
|
|
4293
|
-
const
|
|
4294
|
-
const max = this.limit;
|
|
4336
|
+
const entries = this.entries;
|
|
4295
4337
|
return { [Symbol.asyncIterator]() {
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
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
|
|
4304
|
-
|
|
4305
|
-
return
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
4616
|
-
case "ne": return fieldValue
|
|
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
|
|
4622
|
-
case "notInArray": return !
|
|
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
|
|
4671
|
-
return fieldValue
|
|
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(
|
|
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(!
|
|
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
|
|
4861
|
+
results.push(filterValuesEqual(fieldValue, fieldName ? this._normalizeComparableValue(fieldName, value) : value));
|
|
4788
4862
|
continue;
|
|
4789
4863
|
case "ne":
|
|
4790
|
-
results.push(fieldValue
|
|
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,
|
|
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 (
|
|
5460
|
-
|
|
5461
|
-
|
|
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
|
-
},
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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.
|
|
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
|
|
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) => {
|