kitcn 0.32.2 → 0.33.1
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/CHANGELOG.md +109 -0
- package/dist/aggregate/index.d.ts +2 -2
- package/dist/auth/generated/index.d.ts +1 -1
- package/dist/auth/index.d.ts +16 -16
- package/dist/auth/index.js +1 -1
- package/dist/auth/nextjs/index.d.ts +2 -2
- package/dist/auth/start/server/index.d.ts +2 -2
- package/dist/{builder-Cyd5nCxZ.js → builder-DDYTlmTB.js} +12 -3
- package/dist/caller-factory-C3pcbzo0.d.ts +140 -0
- package/dist/{capabilities-BJm_VSDT.d.ts → capabilities-Bxzoofl4.d.ts} +149 -9
- package/dist/crpc/index.d.ts +3 -2
- package/dist/{generated-contract-disabled-BRm1dNQE.d.ts → generated-contract-disabled-B_EaHGCP.d.ts} +33 -33
- package/dist/{http-types-BhhPZtwa.d.ts → http-types-8FBr10yl.d.ts} +3 -79
- package/dist/{middleware-BBS7oLtT.d.ts → middleware-D2hKJuSZ.d.ts} +1 -1
- package/dist/{middleware-D61Nm-iB.js → middleware-LvZov-hA.js} +1 -1
- package/dist/orm/aggregate-index/index.d.ts +1 -1
- package/dist/orm/aggregate-index/index.js +226 -96
- package/dist/orm/index.d.ts +2 -2
- package/dist/orm/index.js +364 -108
- package/dist/orm/migrations/index.d.ts +2 -2
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/index.js +1 -1
- package/dist/{procedure-caller-DgMiD0-a.js → procedure-caller-DgGF_hA8.js} +1 -1
- package/dist/{procedure-name-DPYyLiVS.d.ts → procedure-name-kn-awLk1.d.ts} +12 -142
- package/dist/{query-context-D4z1CnDH.js → query-context-DQLv58LH.js} +34 -1
- package/dist/ratelimit/index.d.ts +2 -2
- package/dist/ratelimit/index.js +2 -2
- package/dist/react/index.d.ts +5 -6
- package/dist/react/index.js +24 -69
- package/dist/rsc/index.d.ts +4 -3
- package/dist/{schema-Bh7AmJwY.js → schema-BF4P0ZjS.js} +322 -1
- package/dist/server/index.d.ts +5 -4
- package/dist/server/index.js +2 -2
- package/dist/solid/index.d.ts +5 -6
- package/dist/solid/index.js +20 -66
- package/dist/transformer-BEkxlnss.d.ts +78 -0
- package/dist/{types-BXpu7xIU.d.ts → types-Cl0utsTL.d.ts} +4 -3
- package/dist/{where-clause-compiler-Dgf4lrO-.d.ts → where-clause-compiler-CoPVAVDO.d.ts} +136 -136
- package/package.json +1 -1
- package/skills/kitcn/SKILL.md +2 -1
- package/skills/kitcn/references/features/aggregates.md +13 -0
- package/skills/kitcn/references/features/auth-admin.md +1 -0
- package/skills/kitcn/references/features/orm.md +24 -4
- package/skills/kitcn/references/features/react.md +4 -5
- /package/dist/{context-utils-Cbv4r0AA.d.ts → context-utils-ax351iFc.d.ts} +0 -0
- /package/dist/{token-tlbNKQS0.d.ts → token-CZpDQUHd.d.ts} +0 -0
- /package/dist/{types-Bkeicpo6.d.ts → types-CzGsrLrY.d.ts} +0 -0
package/dist/orm/index.js
CHANGED
|
@@ -4,8 +4,8 @@ import { a as pretendRequired, i as pretend, n as deprecated } from "../validato
|
|
|
4
4
|
import { o as id } from "../schema-BFP_awgP.js";
|
|
5
5
|
import { a as OrmSchemaDefinition, c as OrmSchemaExtensionTriggers, d as OrmSchemaRelations, f as OrmSchemaTriggers, g as TablePolymorphic, h as TableName, i as OrmContext, l as OrmSchemaExtensions, n as Columns, o as OrmSchemaExtensionRelations, p as RlsPolicies, r as EnableRLS, s as OrmSchemaExtensionTables, t as Brand, u as OrmSchemaOptions } from "../symbols-DDNAddkd.js";
|
|
6
6
|
import { A as inArray, B as notBetween, C as eq, D as gt, E as filterValuesEqual, F as lt, G as startsWith, H as notInArray, I as lte, L as matchLikePattern, M as isNotNull, N as isNull, O as gte, P as like, R as ne, S as endsWith, T as filterValueInList, U as notLike, V as notIlike, W as or, _ as arrayContains, a as findVectorIndexByName, b as column, c as getRankIndexes, d as INTERNAL_CREATION_TIME_FIELD, f as PUBLIC_CREATED_AT_FIELD, g as arrayContained, h as and, i as findSearchIndexByName, j as isFieldReference, k as ilike, l as resolveIndexOrderPushdown, m as usesSystemCreatedAtAlias, n as findIndexForColumns, o as getAggregateIndexes, r as findRelationIndex, s as getIndexes, t as findExactIndexForColumns, u as CREATED_AT_MIGRATION_MESSAGE, v as arrayOverlaps, w as fieldRef, x as contains, y as between, z as not } from "../index-utils-DvK7P6Q1.js";
|
|
7
|
-
import { a as
|
|
8
|
-
import { $ as
|
|
7
|
+
import { a as getIndexFields, c as stream, d as isUnsetToken, f as unsetToken, i as concatStreams, l as streamCanOrderBy, n as EmptyStream, o as indexKeyWithinBounds, r as QueryStream, s as mergedStream, t as getByIdWithOrmQueryFallback, u as streamIndexRange } from "../query-context-DQLv58LH.js";
|
|
8
|
+
import { $ as getOrmContext, A as canUsePrimaryIdLookupCursor, B as enforceUniqueIndexes, Ct as isConvexEnforceableFilter, D as applyIncomingForeignKeyActionsOnDelete, E as applyDefaults, F as deserializeFilterExpression, G as evaluateFilter, H as ensureNonNullValues, I as encodeUndefinedDeep, J as getColumnName$1, K as extractPrimaryIdLookup, L as enforceCheckConstraints, M as collectPrimaryIdLookupRows, N as createForeignKeyProbeMemo, O as applyIncomingForeignKeyActionsOnUpdate, P as decodeUndefinedDeep, Q as getMutationExecutionMode, R as enforceForeignKeys, S as createCountError, St as convexAnd, T as ensureCountAllowedForRls, U as ensureNullableColumns, V as ensureDefaultColumns, W as evaluateCheckConstraintTriState, X as getMutationAsyncDelayMs, Y as getForeignKeys, Z as getMutationCollectionLimits, _t as toConvexFilter, at as hydrateDateFieldsForRead, b as COUNT_ERROR, bt as markLifecycleHookedTables, c as getAggregateIndexDefinitions, ct as patchReferencingRows, dt as selectReturningRowWithHydration, et as getTableColumns$2, ft as serializeFilterExpression, g as runInOrmWriteScope, gt as takeRowsWithinByteBudget, ht as stripUnsetFields, it as hardDeleteRow, j as collectMutationRowsBounded, k as buildForeignKeyGraph, l as getRankIndexDefinitions, lt as resolveOrmRuntimeDefaults, m as runInOrmWriteBatch, mt as splitReturningSelection, nt as getTableName, o as aggregateExtension, ot as normalizeDateFieldsForWrite, p as runInOrmUserCallback, pt as softDeleteRow, q as getChecks, rt as getUniqueIndexes, st as normalizeTemporalComparableValue, tt as getTableDeleteConfig, ut as returningSelectionReadsCreationTime, v as markOrmTransactionAnchor, vt as unsetFieldsOf, w as ensureAggregateAllowedForRls, wt as mapWithConcurrency, x as createAggregateError, xt as compileConvexFilter, y as AGGREGATE_ERROR, yt as hasLifecycleHooks, z as enforcePolymorphicWrite } from "../schema-BF4P0ZjS.js";
|
|
9
9
|
import { t as defineSchemaExtension } from "../extensions-Bp0XHanR.js";
|
|
10
10
|
import { compareValues, convexToJson, jsonToConvex, v } from "convex/values";
|
|
11
11
|
import { defineSchema as defineSchema$1, internalActionGeneric, internalMutationGeneric, internalQueryGeneric } from "convex/server";
|
|
@@ -560,7 +560,7 @@ function policyExpressionPasses(row, expression) {
|
|
|
560
560
|
async function resolveExpression(policy, checkType, ctx, table) {
|
|
561
561
|
const candidate = checkType === "withCheck" ? policy.withCheck ?? policy.using : policy.using;
|
|
562
562
|
if (!candidate) return;
|
|
563
|
-
if (typeof candidate === "function") return await candidate(ctx, table);
|
|
563
|
+
if (typeof candidate === "function") return await runInOrmUserCallback(() => candidate(ctx, table));
|
|
564
564
|
return candidate;
|
|
565
565
|
}
|
|
566
566
|
/**
|
|
@@ -703,13 +703,14 @@ async function filterSelectRows(options) {
|
|
|
703
703
|
*/
|
|
704
704
|
const INDEX_ORDER_BONUS = 30;
|
|
705
705
|
/**
|
|
706
|
-
* Widest index union worth
|
|
707
|
-
*
|
|
708
|
-
*
|
|
706
|
+
* Widest index union worth reading as one merged stream. A merge holds every
|
|
707
|
+
* probe's index range open at once, and past this width that fan-out costs more
|
|
708
|
+
* than reading the same ranges one after another.
|
|
709
709
|
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
710
|
+
* Only the executor's business: it picks between a merged and a concatenated
|
|
711
|
+
* union. Width is never a reason to compile a scan instead of a union — the
|
|
712
|
+
* ranges an `in` opens are what bounds the read, and a table scan does not stop
|
|
713
|
+
* being a table scan because the list got longer.
|
|
713
714
|
*/
|
|
714
715
|
const MAX_INDEX_UNION_PROBES = 64;
|
|
715
716
|
var WhereClauseCompiler = class {
|
|
@@ -789,7 +790,7 @@ var WhereClauseCompiler = class {
|
|
|
789
790
|
for (const term of terms) {
|
|
790
791
|
if (term.type !== "binary" || term.operator !== "inArray") continue;
|
|
791
792
|
const probePlan = this.tryCompileInArray(term);
|
|
792
|
-
if (!probePlan
|
|
793
|
+
if (!probePlan) continue;
|
|
793
794
|
return {
|
|
794
795
|
...probePlan,
|
|
795
796
|
postFilters: [expression]
|
|
@@ -2713,34 +2714,70 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
2713
2714
|
for (let i = 0; i < fields.length; i += 1) builder = builder.eq(fields[i], values[i]);
|
|
2714
2715
|
return builder;
|
|
2715
2716
|
}
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2717
|
+
/**
|
|
2718
|
+
* The row filter for an already-resolved pipeline stage `where`.
|
|
2719
|
+
*
|
|
2720
|
+
* Takes the resolved shape rather than the clause so the rows that are read
|
|
2721
|
+
* and the rows that are kept come from the same resolution. Resolving twice
|
|
2722
|
+
* would let a `where` callback that closes over changing state — a clock, a
|
|
2723
|
+
* counter — bound the read with one expression and filter it with another,
|
|
2724
|
+
* dropping rows that the second expression matched but the first never read.
|
|
2725
|
+
*/
|
|
2726
|
+
_buildResolvedWherePredicate(where) {
|
|
2727
|
+
if (where.kind === "none") return null;
|
|
2728
|
+
if (where.kind === "predicate") {
|
|
2729
|
+
const { clause } = where;
|
|
2730
|
+
return async (row) => await clause.predicate(row);
|
|
2731
|
+
}
|
|
2732
|
+
const { expression } = where;
|
|
2733
|
+
return async (row) => this._evaluatePostFetchFilter(row, expression);
|
|
2729
2734
|
}
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2735
|
+
/**
|
|
2736
|
+
* What a pipeline stage `where` is, resolved once.
|
|
2737
|
+
*
|
|
2738
|
+
* Object clauses and callbacks that return an expression can be compiled and
|
|
2739
|
+
* therefore index-lowered. `predicate(...)` is opaque JavaScript and never
|
|
2740
|
+
* can be. Both the anchoring check and the index lowering need to tell those
|
|
2741
|
+
* apart, and a callback `where` should be run once per read rather than once
|
|
2742
|
+
* per caller that asks.
|
|
2743
|
+
*/
|
|
2744
|
+
_resolvePipelineWhere(where, tableConfig) {
|
|
2745
|
+
if (!where) return { kind: "none" };
|
|
2734
2746
|
if (typeof where === "function") {
|
|
2735
2747
|
const result = this._resolveWhereCallbackExpression(where, tableConfig, { context: "pipeline" });
|
|
2736
|
-
if (!result) return;
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
}
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2748
|
+
if (!result) return { kind: "none" };
|
|
2749
|
+
return this._isPredicateWhereClause(result) ? {
|
|
2750
|
+
kind: "predicate",
|
|
2751
|
+
clause: result
|
|
2752
|
+
} : {
|
|
2753
|
+
kind: "expression",
|
|
2754
|
+
expression: result
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
const expression = this._buildFilterExpression(where, tableConfig);
|
|
2758
|
+
return expression ? {
|
|
2759
|
+
kind: "expression",
|
|
2760
|
+
expression
|
|
2761
|
+
} : { kind: "none" };
|
|
2762
|
+
}
|
|
2763
|
+
/**
|
|
2764
|
+
* Reject a `predicate(...)` stage `where` on a read nothing bounds.
|
|
2765
|
+
*
|
|
2766
|
+
* A predicate can only run after the row is read, so it costs one read per
|
|
2767
|
+
* candidate row. That is fine over an index range and unbounded over a table
|
|
2768
|
+
* scan, which is the same line `findMany` draws for a chain-level
|
|
2769
|
+
* `predicate(...)`. An object `where` is not held to it: the compiler
|
|
2770
|
+
* lowers what it can and post-filters the rest, exactly as the chain-level
|
|
2771
|
+
* read does.
|
|
2772
|
+
*
|
|
2773
|
+
* `remedy` is per call site because the anchor a caller can add differs: a
|
|
2774
|
+
* union source takes an index, a `flatMap` stage has no index option at all
|
|
2775
|
+
* and is anchored by the relation's own declared index.
|
|
2776
|
+
*/
|
|
2777
|
+
_assertPipelineWhereIsAnchored(options) {
|
|
2778
|
+
const { where, isAnchored, context, remedy } = options;
|
|
2779
|
+
if (isAnchored || where.kind !== "predicate") return;
|
|
2780
|
+
throw new Error(`${context} where uses predicate(...), which is evaluated after each row is read, so it needs an index-bounded read. ${remedy}`);
|
|
2744
2781
|
}
|
|
2745
2782
|
_isFilterExpressionNode(value) {
|
|
2746
2783
|
return typeof value === "object" && value !== null && "accept" in value && typeof value.accept === "function";
|
|
@@ -2837,16 +2874,23 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
2837
2874
|
/**
|
|
2838
2875
|
* The compiled index union, as one ordered stream.
|
|
2839
2876
|
*
|
|
2840
|
-
* Each probe is its own index range, so the union is only usable where
|
|
2841
|
-
*
|
|
2842
|
-
*
|
|
2877
|
+
* Each probe is its own index range, so the union is only usable where its
|
|
2878
|
+
* order is the order the read has to produce. `mergedStream` orders by a
|
|
2879
|
+
* suffix of the index key, and a suffix may only drop key components the
|
|
2843
2880
|
* probes all pin to a single value — so the requested field has to sit inside
|
|
2844
2881
|
* the pinned run or immediately after it. Returns null when it does not, and
|
|
2845
2882
|
* the caller falls back to the plan's plain index range or a bounded scan.
|
|
2883
|
+
*
|
|
2884
|
+
* Two executors, because a union that is too wide to merge is not too wide to
|
|
2885
|
+
* read. A merge registers every probe up front and holds them open for the
|
|
2886
|
+
* life of the read, which is what `MAX_INDEX_UNION_PROBES` refuses; past that
|
|
2887
|
+
* width the probes are concatenated instead, one open query at a time. What
|
|
2888
|
+
* the cap picks is therefore fan-out versus sequential — never index versus
|
|
2889
|
+
* table scan, which is a trade no read wins.
|
|
2846
2890
|
*/
|
|
2847
2891
|
_buildProbeUnionStream(params) {
|
|
2848
2892
|
const { probeFilters } = params;
|
|
2849
|
-
if (probeFilters.length === 0
|
|
2893
|
+
if (probeFilters.length === 0) return null;
|
|
2850
2894
|
const streamIndexFields = getIndexFields(this.tableConfig.name, params.indexName, params.schemaDefinition);
|
|
2851
2895
|
const pinned = this._indexEqPrefixCount({ probeFilters });
|
|
2852
2896
|
let mergeOffset = -1;
|
|
@@ -2855,11 +2899,102 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
2855
2899
|
break;
|
|
2856
2900
|
}
|
|
2857
2901
|
if (mergeOffset === -1) return null;
|
|
2858
|
-
|
|
2902
|
+
const probeStream = (filters) => stream(this.db, params.schemaDefinition).query(this.tableConfig.name).withIndex(params.indexName, (q) => {
|
|
2859
2903
|
let indexQuery = q;
|
|
2860
2904
|
for (const filter of filters) indexQuery = this._applyFilterToQuery(indexQuery, filter);
|
|
2861
2905
|
return indexQuery;
|
|
2862
|
-
}).order(params.order)
|
|
2906
|
+
}).order(params.order);
|
|
2907
|
+
if (probeFilters.length > MAX_INDEX_UNION_PROBES) {
|
|
2908
|
+
if (mergeOffset !== 0) return null;
|
|
2909
|
+
const orderedProbes = this._orderDisjointProbes({
|
|
2910
|
+
probeFilters,
|
|
2911
|
+
indexField: streamIndexFields[0],
|
|
2912
|
+
order: params.order
|
|
2913
|
+
});
|
|
2914
|
+
if (!orderedProbes) return null;
|
|
2915
|
+
return concatStreams(orderedProbes.map(probeStream));
|
|
2916
|
+
}
|
|
2917
|
+
return mergedStream(probeFilters.map(probeStream), streamIndexFields.slice(mergeOffset));
|
|
2918
|
+
}
|
|
2919
|
+
/**
|
|
2920
|
+
* `probeFilters` in the order a scan of `indexField` reaches them, or null
|
|
2921
|
+
* when they cannot be proven to be pairwise-disjoint ranges on that field.
|
|
2922
|
+
*
|
|
2923
|
+
* Concatenation stands in for a merge only under those two properties, and
|
|
2924
|
+
* both have to come from the filters themselves rather than from trust in the
|
|
2925
|
+
* compiler that emitted them: overlapping probes would emit a row twice, and
|
|
2926
|
+
* probes handed over out of order would emit index keys backwards.
|
|
2927
|
+
*/
|
|
2928
|
+
_orderDisjointProbes(params) {
|
|
2929
|
+
const ranges = [];
|
|
2930
|
+
for (const filters of params.probeFilters) {
|
|
2931
|
+
let lower = null;
|
|
2932
|
+
let upper = null;
|
|
2933
|
+
let usable = filters.length > 0;
|
|
2934
|
+
for (const filter of filters) {
|
|
2935
|
+
if (filter.type !== "binary") {
|
|
2936
|
+
usable = false;
|
|
2937
|
+
break;
|
|
2938
|
+
}
|
|
2939
|
+
const [field, rawValue] = filter.operands;
|
|
2940
|
+
if (!isFieldReference(field) || field.fieldName !== params.indexField || rawValue === void 0 || isFieldReference(rawValue)) {
|
|
2941
|
+
usable = false;
|
|
2942
|
+
break;
|
|
2943
|
+
}
|
|
2944
|
+
const value = this._normalizeComparableValue(field.fieldName, rawValue);
|
|
2945
|
+
switch (filter.operator) {
|
|
2946
|
+
case "eq":
|
|
2947
|
+
usable = lower === null && upper === null;
|
|
2948
|
+
lower = {
|
|
2949
|
+
inclusive: true,
|
|
2950
|
+
value
|
|
2951
|
+
};
|
|
2952
|
+
upper = {
|
|
2953
|
+
inclusive: true,
|
|
2954
|
+
value
|
|
2955
|
+
};
|
|
2956
|
+
break;
|
|
2957
|
+
case "gt":
|
|
2958
|
+
case "gte":
|
|
2959
|
+
usable = lower === null;
|
|
2960
|
+
lower = {
|
|
2961
|
+
inclusive: filter.operator === "gte",
|
|
2962
|
+
value
|
|
2963
|
+
};
|
|
2964
|
+
break;
|
|
2965
|
+
case "lt":
|
|
2966
|
+
case "lte":
|
|
2967
|
+
usable = upper === null;
|
|
2968
|
+
upper = {
|
|
2969
|
+
inclusive: filter.operator === "lte",
|
|
2970
|
+
value
|
|
2971
|
+
};
|
|
2972
|
+
break;
|
|
2973
|
+
default: usable = false;
|
|
2974
|
+
}
|
|
2975
|
+
if (!usable) break;
|
|
2976
|
+
}
|
|
2977
|
+
if (!usable) return null;
|
|
2978
|
+
ranges.push({
|
|
2979
|
+
filters,
|
|
2980
|
+
lower,
|
|
2981
|
+
upper
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2984
|
+
ranges.sort((a, b) => {
|
|
2985
|
+
if (!(a.lower && b.lower)) return a.lower ? 1 : b.lower ? -1 : 0;
|
|
2986
|
+
return compareValues(a.lower.value, b.lower.value) || Number(!a.lower.inclusive) - Number(!b.lower.inclusive);
|
|
2987
|
+
});
|
|
2988
|
+
for (let i = 1; i < ranges.length; i += 1) {
|
|
2989
|
+
const previous = ranges[i - 1];
|
|
2990
|
+
const current = ranges[i];
|
|
2991
|
+
if (!(previous.upper && current.lower)) return null;
|
|
2992
|
+
const gap = compareValues(previous.upper.value, current.lower.value);
|
|
2993
|
+
if (gap > 0) return null;
|
|
2994
|
+
if (gap === 0 && previous.upper.inclusive && current.lower.inclusive) return null;
|
|
2995
|
+
}
|
|
2996
|
+
const ordered = ranges.map((range) => range.filters);
|
|
2997
|
+
return params.order === "asc" ? ordered : ordered.reverse();
|
|
2863
2998
|
}
|
|
2864
2999
|
/**
|
|
2865
3000
|
* The read the compiled plan describes, as a stream, with nothing filtered
|
|
@@ -2886,7 +3021,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
2886
3021
|
indexName: probeIndex.name,
|
|
2887
3022
|
probeFilters: queryConfig.probeFilters,
|
|
2888
3023
|
order,
|
|
2889
|
-
orderField: primaryOrder?.field ?? probeIndex.fields[0]
|
|
3024
|
+
orderField: params.probeOrderField ?? primaryOrder?.field ?? probeIndex.fields[0]
|
|
2890
3025
|
});
|
|
2891
3026
|
if (probeUnion) return {
|
|
2892
3027
|
stream: probeUnion,
|
|
@@ -2959,22 +3094,82 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
2959
3094
|
});
|
|
2960
3095
|
return streamQuery;
|
|
2961
3096
|
}
|
|
2962
|
-
|
|
3097
|
+
/**
|
|
3098
|
+
* One `pipeline.union` source, as a stream.
|
|
3099
|
+
*
|
|
3100
|
+
* The source's own `where` is compiled into an index plan first, so an
|
|
3101
|
+
* object `where` bounds the read instead of being discarded row by row after
|
|
3102
|
+
* it. Two rules keep that from changing what the union produces:
|
|
3103
|
+
*
|
|
3104
|
+
* - `_compileQueryPlan` discards a plan that would displace an index the
|
|
3105
|
+
* caller pinned, so a source's own range (a tenant scope, say) still wins.
|
|
3106
|
+
* - A merged union is ordered by `interleaveBy`, which only some index
|
|
3107
|
+
* shapes can supply. The lowered read is checked against that before it is
|
|
3108
|
+
* committed to, and the unlowered scan is used when it cannot serve the
|
|
3109
|
+
* merge — an index that saves reads is not worth an ordering that throws.
|
|
3110
|
+
*
|
|
3111
|
+
* The `where` stays applied in JS either way. The index range is a bound on
|
|
3112
|
+
* what is read, not a replacement for the predicate.
|
|
3113
|
+
*/
|
|
3114
|
+
_buildUnionSourceStream(source, fallbackOrder, interleaveFields) {
|
|
2963
3115
|
const sourceIndex = source.index ?? this.configuredIndex;
|
|
2964
|
-
this.
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
context: "pipeline.union source"
|
|
3116
|
+
const sourceWhere = this._resolvePipelineWhere(source.where, this.tableConfig);
|
|
3117
|
+
this._assertPipelineWhereIsAnchored({
|
|
3118
|
+
where: sourceWhere,
|
|
3119
|
+
isAnchored: Boolean(sourceIndex?.name),
|
|
3120
|
+
context: "pipeline.union source",
|
|
3121
|
+
remedy: "Give the source an index: { name, range }, or pin .withIndex(name, range?) on the chain."
|
|
2969
3122
|
});
|
|
2970
3123
|
const schemaDefinition = this._getSchemaDefinitionOrThrow();
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
3124
|
+
const buildRead = (whereExpression) => this._buildPlanStream({
|
|
3125
|
+
queryConfig: this._compileQueryPlan({
|
|
3126
|
+
whereExpression,
|
|
3127
|
+
configuredIndex: sourceIndex,
|
|
3128
|
+
orderSpecs: []
|
|
3129
|
+
}),
|
|
3130
|
+
configuredIndex: sourceIndex,
|
|
3131
|
+
order: fallbackOrder,
|
|
3132
|
+
orderIndexName: null,
|
|
3133
|
+
probeOrderField: interleaveFields?.[0],
|
|
3134
|
+
schemaDefinition
|
|
3135
|
+
}).stream;
|
|
3136
|
+
const planned = sourceWhere.kind === "expression" ? buildRead(sourceWhere.expression) : null;
|
|
3137
|
+
let sourceStream = planned !== null && (!interleaveFields || streamCanOrderBy(planned, interleaveFields)) ? planned : buildRead();
|
|
3138
|
+
const sourcePredicate = this._buildResolvedWherePredicate(sourceWhere);
|
|
2975
3139
|
if (sourcePredicate) sourceStream = sourceStream.filterWith(sourcePredicate);
|
|
2976
3140
|
return sourceStream;
|
|
2977
3141
|
}
|
|
3142
|
+
/**
|
|
3143
|
+
* The index a `flatMap` stage `where` can ride on top of the join keys.
|
|
3144
|
+
*
|
|
3145
|
+
* The join is only correct while `targetFields` stay pinned by equality, and
|
|
3146
|
+
* a Convex range builder pins fields in index-key order with no gaps, so the
|
|
3147
|
+
* candidates are exactly the declared indexes that lead with `targetFields`
|
|
3148
|
+
* and carry at least one more field. Those trailing fields are what the
|
|
3149
|
+
* stage `where` is compiled against — the compiler never sees the join keys,
|
|
3150
|
+
* because their values are per parent and the index choice has to be made
|
|
3151
|
+
* once for all of them.
|
|
3152
|
+
*
|
|
3153
|
+
* Returns null when nothing is gained, and the caller keeps the relation's
|
|
3154
|
+
* own index plus the post-filter. A multi-probe plan is refused: each probe
|
|
3155
|
+
* is its own range, and a union of ranges per parent is a different stream
|
|
3156
|
+
* shape than the one `flatMap` was handed as `mappedIndexFields`.
|
|
3157
|
+
*/
|
|
3158
|
+
_resolveFlatMapStageIndex(params) {
|
|
3159
|
+
const { where, targetTableConfig, targetFields } = params;
|
|
3160
|
+
if (where.kind !== "expression") return null;
|
|
3161
|
+
const candidates = getIndexes(targetTableConfig.table).filter((index) => index.fields.length > targetFields.length && targetFields.every((field, i) => index.fields[i] === field)).map((index) => ({
|
|
3162
|
+
indexName: index.name,
|
|
3163
|
+
indexFields: index.fields.slice(targetFields.length)
|
|
3164
|
+
}));
|
|
3165
|
+
if (candidates.length === 0) return null;
|
|
3166
|
+
const compiled = new WhereClauseCompiler(targetTableConfig.table.tableName, candidates).compile(where.expression);
|
|
3167
|
+
if (!compiled.selectedIndex || compiled.probeFilters.length > 0 || compiled.indexFilters.length === 0) return null;
|
|
3168
|
+
return {
|
|
3169
|
+
name: compiled.selectedIndex.indexName,
|
|
3170
|
+
filters: compiled.indexFilters
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
2978
3173
|
async _applyFlatMapStage(sourceStream, stage) {
|
|
2979
3174
|
const relationName = stage.relation;
|
|
2980
3175
|
const edge = this.edgeMetadata.find((e) => e.edgeName === relationName);
|
|
@@ -2990,24 +3185,36 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
2990
3185
|
rls: this.rls
|
|
2991
3186
|
});
|
|
2992
3187
|
const strict = this.tableConfig.strict !== false;
|
|
2993
|
-
const
|
|
3188
|
+
const useGetById = targetFields.length === 1 && targetFields[0] === "_id";
|
|
3189
|
+
const relationIndexName = useGetById ? "by_id" : findRelationIndex(targetTableConfig.table, targetFields, `${this.tableConfig.name}.${relationName}`, edge.targetTable, strict, this.allowFullScan);
|
|
3190
|
+
const stageWhere = this._resolvePipelineWhere(stage.where, targetTableConfig);
|
|
3191
|
+
const stageIndex = useGetById ? null : this._resolveFlatMapStageIndex({
|
|
3192
|
+
where: stageWhere,
|
|
3193
|
+
targetTableConfig,
|
|
3194
|
+
targetFields
|
|
3195
|
+
});
|
|
3196
|
+
const indexName = stageIndex?.name ?? relationIndexName;
|
|
2994
3197
|
const outerOrder = sourceStream.getOrder();
|
|
2995
3198
|
const schemaDefinition = this._getSchemaDefinitionOrThrow();
|
|
2996
3199
|
const innerIndexFields = getIndexFields(edge.targetTable, indexName ?? "by_creation_time" ?? "by_creation_time", schemaDefinition);
|
|
2997
3200
|
if (stage.limit !== void 0 && (!Number.isInteger(stage.limit) || stage.limit < 1)) throw new Error("pipeline.flatMap.limit must be a positive integer");
|
|
2998
3201
|
const mappedIndexFields = stage.limit === void 0 ? innerIndexFields : [...innerIndexFields, PIPELINE_LIMIT_ORDINAL_FIELD];
|
|
2999
|
-
const stageWherePredicate = this.
|
|
3000
|
-
this.
|
|
3001
|
-
where:
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3202
|
+
const stageWherePredicate = this._buildResolvedWherePredicate(stageWhere);
|
|
3203
|
+
this._assertPipelineWhereIsAnchored({
|
|
3204
|
+
where: stageWhere,
|
|
3205
|
+
isAnchored: Boolean(indexName),
|
|
3206
|
+
context: `pipeline.flatMap(${relationName})`,
|
|
3207
|
+
remedy: `flatMap takes no index of its own — add an index on ${edge.targetTable}(${targetFields.join(", ")}) so the join is anchored, or use an object where().`
|
|
3005
3208
|
});
|
|
3006
3209
|
return sourceStream.flatMap(async (parent) => {
|
|
3007
3210
|
const values = sourceFields.map((field) => parent[field]);
|
|
3008
3211
|
if (values.some((value) => value === null || value === void 0)) return new EmptyStream(outerOrder, mappedIndexFields);
|
|
3009
3212
|
let inner = stream(this.db, schemaDefinition).query(edge.targetTable);
|
|
3010
|
-
if (indexName) inner = inner.withIndex(indexName, (q) =>
|
|
3213
|
+
if (indexName) inner = inner.withIndex(indexName, (q) => {
|
|
3214
|
+
let range = this._applyEqBounds(q, targetFields, values);
|
|
3215
|
+
for (const filter of stageIndex?.filters ?? []) range = this._applyFilterToQuery(range, filter, targetTableConfig);
|
|
3216
|
+
return range;
|
|
3217
|
+
});
|
|
3011
3218
|
inner = inner.order(outerOrder);
|
|
3012
3219
|
if (stageWherePredicate) inner = inner.filterWith(stageWherePredicate);
|
|
3013
3220
|
if (isRlsEnabled(targetTableConfig.table)) inner = inner.filterWith(async (row) => {
|
|
@@ -4109,12 +4316,14 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4109
4316
|
let streamQuery;
|
|
4110
4317
|
const unionSources = pipeline?.union ?? [];
|
|
4111
4318
|
if (unionSources.length > 0) {
|
|
4112
|
-
|
|
4113
|
-
if (
|
|
4114
|
-
|
|
4115
|
-
if (!
|
|
4116
|
-
|
|
4319
|
+
let interleaveFields;
|
|
4320
|
+
if (unionSources.length > 1) {
|
|
4321
|
+
const declared = pipeline?.interleaveBy;
|
|
4322
|
+
if (!declared || declared.length === 0) throw new Error("pipeline.interleaveBy is required when pipeline.union has multiple sources.");
|
|
4323
|
+
interleaveFields = declared.map((field) => this._normalizePublicFieldName(field));
|
|
4117
4324
|
}
|
|
4325
|
+
const streams = unionSources.map((source) => this._buildUnionSourceStream(source, fallbackOrder, interleaveFields));
|
|
4326
|
+
streamQuery = interleaveFields ? mergedStream(streams, interleaveFields) : streams[0];
|
|
4118
4327
|
} else {
|
|
4119
4328
|
const orderedIdList = isCursorPaginated && maxScan !== void 0 && idLookup?.kind === "in" ? queryConfig.order?.[0] : void 0;
|
|
4120
4329
|
if (orderedIdList) {
|
|
@@ -4399,41 +4608,53 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4399
4608
|
const selectedRows = await this._finalizeRows(rows);
|
|
4400
4609
|
return this._returnSelectedRows(selectedRows);
|
|
4401
4610
|
}
|
|
4611
|
+
const matchesPostFetchMembership = async (row) => {
|
|
4612
|
+
const visibleRows = await this._applyRlsSelectFilter([row], this.tableConfig);
|
|
4613
|
+
if (visibleRows.length === 0) return false;
|
|
4614
|
+
if (!whereFilter) return true;
|
|
4615
|
+
return (await this._applyRelationsFilterToRows(visibleRows, this.tableConfig, whereFilter, this.edgeMetadata, 0, MAX_RELATION_DEPTH, this.config.with)).length > 0;
|
|
4616
|
+
};
|
|
4402
4617
|
if (queryConfig.strategy === "multiProbe" && queryConfig.index && !isCursorPaginated) {
|
|
4403
4618
|
const probeOffset = config.offset ?? 0;
|
|
4404
4619
|
if (typeof probeOffset !== "number") throw new Error("Only numeric offset is supported in kitcn ORM.");
|
|
4405
4620
|
const probeLimit = this._resolveNonPaginatedLimit(config);
|
|
4406
4621
|
const convexProbeFilters = queryConfig.postFilters.filter((filter) => this._isConvexEnforceableFilter(filter));
|
|
4407
4622
|
const probeHasResidualFilter = convexProbeFilters.length !== queryConfig.postFilters.length;
|
|
4408
|
-
const
|
|
4623
|
+
const probeHasRelationMembership = this._hasSearchDisallowedRelationFilter(whereFilter, this.tableConfig);
|
|
4624
|
+
const probeHasPostFetchMembership = probeHasRelationMembership || this.rls?.mode !== "skip" && isRlsEnabled(this.tableConfig.table);
|
|
4409
4625
|
const probeOrderDirection = orderPushdownDirection;
|
|
4410
|
-
const probeBound = probeLimit !== void 0 &&
|
|
4626
|
+
const probeBound = probeLimit !== void 0 && (postFetchOrders.length === 0 || probeOrderDirection !== null) ? probeOffset + probeLimit : void 0;
|
|
4627
|
+
const probeBoundedTake = probeBound !== void 0 && !probeHasResidualFilter && !probeHasPostFetchMembership;
|
|
4628
|
+
const probeSchemaDefinition = this.schema[OrmSchemaDefinition];
|
|
4629
|
+
const probeStreamed = probeBound !== void 0 && !probeBoundedTake && !probeHasRelationMembership && !!probeSchemaDefinition;
|
|
4411
4630
|
const probeRows = await Promise.all(queryConfig.probeFilters.map(async (probeFilters) => {
|
|
4631
|
+
if (probeStreamed) return await stream(this.db, probeSchemaDefinition).query(this.tableConfig.name).withIndex(queryConfig.index.name, (q) => {
|
|
4632
|
+
let indexQuery = q;
|
|
4633
|
+
for (const filter of probeFilters) indexQuery = this._applyFilterToQuery(indexQuery, filter);
|
|
4634
|
+
return indexQuery;
|
|
4635
|
+
}).order(probeOrderDirection ?? "asc").filterWith(async (row) => {
|
|
4636
|
+
for (const filter of queryConfig.postFilters) if (!this._evaluatePostFetchFilter(row, filter)) return false;
|
|
4637
|
+
return await matchesPostFetchMembership(row);
|
|
4638
|
+
}).take(probeBound);
|
|
4412
4639
|
let probeQuery = this.db.query(queryConfig.table).withIndex(queryConfig.index.name, (q) => {
|
|
4413
4640
|
let indexQuery = q;
|
|
4414
4641
|
for (const filter of probeFilters) indexQuery = this._applyFilterToQuery(indexQuery, filter);
|
|
4415
4642
|
return indexQuery;
|
|
4416
4643
|
});
|
|
4417
|
-
if (
|
|
4644
|
+
if (probeBoundedTake && probeOrderDirection) probeQuery = probeQuery.order(probeOrderDirection);
|
|
4418
4645
|
if (convexProbeFilters.length > 0) probeQuery = probeQuery.filter((q) => convexAnd(q, convexProbeFilters.map((filter) => this._toConvexExpression(filter)(q))));
|
|
4419
|
-
return
|
|
4646
|
+
return probeBoundedTake ? await probeQuery.take(probeBound) : await probeQuery.collect();
|
|
4420
4647
|
}));
|
|
4421
4648
|
let rows = Array.from(new Map(probeRows.flat().map((row) => [String(row._id), row])).values());
|
|
4422
4649
|
if (queryConfig.postFilters.length > 0) rows = rows.filter((row) => queryConfig.postFilters.every((filter) => this._evaluatePostFetchFilter(row, filter)));
|
|
4423
4650
|
rows = await this._applyRlsSelectFilter(rows, this.tableConfig);
|
|
4424
|
-
if (whereFilter) rows = await this._applyRelationsFilterToRows(rows, this.tableConfig, whereFilter, this.edgeMetadata, 0, MAX_RELATION_DEPTH, this.config.with);
|
|
4651
|
+
if (whereFilter && !probeStreamed) rows = await this._applyRelationsFilterToRows(rows, this.tableConfig, whereFilter, this.edgeMetadata, 0, MAX_RELATION_DEPTH, this.config.with);
|
|
4425
4652
|
if (postFetchOrders.length > 0) rows = rows.sort((a, b) => this._compareByOrderSpecs(a, b, postFetchOrders));
|
|
4426
4653
|
if (probeOffset > 0) rows = rows.slice(probeOffset);
|
|
4427
4654
|
if (probeLimit !== void 0) rows = rows.slice(0, probeLimit);
|
|
4428
4655
|
const selectedRows = await this._finalizeRows(rows);
|
|
4429
4656
|
return this._returnSelectedRows(selectedRows);
|
|
4430
4657
|
}
|
|
4431
|
-
const matchesPostFetchMembership = async (row) => {
|
|
4432
|
-
const visibleRows = await this._applyRlsSelectFilter([row], this.tableConfig);
|
|
4433
|
-
if (visibleRows.length === 0) return false;
|
|
4434
|
-
if (!whereFilter) return true;
|
|
4435
|
-
return (await this._applyRelationsFilterToRows(visibleRows, this.tableConfig, whereFilter, this.edgeMetadata, 0, MAX_RELATION_DEPTH, this.config.with)).length > 0;
|
|
4436
|
-
};
|
|
4437
4658
|
if (isCursorPaginated) {
|
|
4438
4659
|
const isMultiProbePlan = queryConfig.strategy === "multiProbe" && !!queryConfig.index;
|
|
4439
4660
|
const isScanFallbackPlan = !queryConfig.index && queryConfig.postFilters.length > 0;
|
|
@@ -4583,11 +4804,6 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4583
4804
|
*/
|
|
4584
4805
|
_toConvexQuery(whereExpressionOverride, configuredIndex) {
|
|
4585
4806
|
const config = this.config;
|
|
4586
|
-
const tableIndexes = getIndexes(this.tableConfig.table).map((index) => ({
|
|
4587
|
-
indexName: index.name,
|
|
4588
|
-
indexFields: index.fields
|
|
4589
|
-
}));
|
|
4590
|
-
const compiler = new WhereClauseCompiler(this.tableConfig.table.tableName, tableIndexes);
|
|
4591
4807
|
let orderSpecs = [];
|
|
4592
4808
|
if (config.orderBy) {
|
|
4593
4809
|
const orderByValue = typeof config.orderBy === "function" ? config.orderBy(this.tableConfig.table, {
|
|
@@ -4598,7 +4814,29 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4598
4814
|
}
|
|
4599
4815
|
let whereExpression = whereExpressionOverride;
|
|
4600
4816
|
if (!whereExpression && config.where && typeof config.where !== "function") whereExpression = this._buildFilterExpression(config.where, this.tableConfig);
|
|
4601
|
-
|
|
4817
|
+
return this._compileQueryPlan({
|
|
4818
|
+
whereExpression,
|
|
4819
|
+
configuredIndex,
|
|
4820
|
+
orderSpecs
|
|
4821
|
+
});
|
|
4822
|
+
}
|
|
4823
|
+
/**
|
|
4824
|
+
* The read plan for one `where` on this table.
|
|
4825
|
+
*
|
|
4826
|
+
* Split out of `_toConvexQuery` because the chain-level `where` is not the
|
|
4827
|
+
* only one that has to be index-lowered: a `pipeline.union` source carries
|
|
4828
|
+
* its own `where` and its own pinned index, and compiles against the same
|
|
4829
|
+
* table with a different ordering contract (`interleaveBy`, not `orderBy`).
|
|
4830
|
+
* Both go through here so a plain-object `where` is planned the same way
|
|
4831
|
+
* wherever it is written.
|
|
4832
|
+
*/
|
|
4833
|
+
_compileQueryPlan(params) {
|
|
4834
|
+
const { whereExpression, configuredIndex, orderSpecs } = params;
|
|
4835
|
+
const tableIndexes = getIndexes(this.tableConfig.table).map((index) => ({
|
|
4836
|
+
indexName: index.name,
|
|
4837
|
+
indexFields: index.fields
|
|
4838
|
+
}));
|
|
4839
|
+
const planned = new WhereClauseCompiler(this.tableConfig.table.tableName, tableIndexes).compile(whereExpression, { orderFields: orderSpecs.map((spec) => spec.field) });
|
|
4602
4840
|
const plannedUsesIndex = !!planned.selectedIndex && (planned.indexFilters.length > 0 || planned.probeFilters.length > 0);
|
|
4603
4841
|
const compiled = !configuredIndex?.name || !plannedUsesIndex || planned.selectedIndex?.indexName === configuredIndex.name && !configuredIndex.range ? planned : {
|
|
4604
4842
|
strategy: "none",
|
|
@@ -4680,11 +4918,11 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4680
4918
|
* Apply a single filter expression to a Convex query builder
|
|
4681
4919
|
* Used for index filters (eq operations)
|
|
4682
4920
|
*/
|
|
4683
|
-
_applyFilterToQuery(query, filter) {
|
|
4921
|
+
_applyFilterToQuery(query, filter, tableConfig = this.tableConfig) {
|
|
4684
4922
|
if (filter.type === "binary") {
|
|
4685
4923
|
const [field, value] = filter.operands;
|
|
4686
4924
|
if (!isFieldReference(field)) return query;
|
|
4687
|
-
const normalizedValue = this._normalizeComparableValue(field.fieldName, value);
|
|
4925
|
+
const normalizedValue = this._normalizeComparableValue(field.fieldName, value, tableConfig);
|
|
4688
4926
|
switch (filter.operator) {
|
|
4689
4927
|
case "eq": return query.eq(field.fieldName, normalizedValue);
|
|
4690
4928
|
case "gt": return query.gt(field.fieldName, normalizedValue);
|
|
@@ -5637,6 +5875,9 @@ var ConvexDeleteBuilder = class extends QueryPromise {
|
|
|
5637
5875
|
});
|
|
5638
5876
|
}
|
|
5639
5877
|
async execute(...args) {
|
|
5878
|
+
return await runInOrmWriteScope(this.db, () => runInOrmWriteBatch(this.db, () => this._runStatement(...args)));
|
|
5879
|
+
}
|
|
5880
|
+
async _runStatement(...args) {
|
|
5640
5881
|
const config = args[0];
|
|
5641
5882
|
const tableName = getTableName(this.table);
|
|
5642
5883
|
const ormContext = getOrmContext(this.db);
|
|
@@ -5928,6 +6169,9 @@ var ConvexInsertBuilder = class extends QueryPromise {
|
|
|
5928
6169
|
return this;
|
|
5929
6170
|
}
|
|
5930
6171
|
async execute() {
|
|
6172
|
+
return await runInOrmWriteScope(this.db, () => runInOrmWriteBatch(this.db, () => this._runStatement()));
|
|
6173
|
+
}
|
|
6174
|
+
async _runStatement() {
|
|
5931
6175
|
if (this.valuesList.length === 0) throw new Error("values() must be called before execute()");
|
|
5932
6176
|
const ormContext = getOrmContext(this.db);
|
|
5933
6177
|
const tableName = getTableName(this.table);
|
|
@@ -6180,6 +6424,7 @@ function normalizeOrmTriggers(triggers) {
|
|
|
6180
6424
|
//#region src/orm/lifecycle.ts
|
|
6181
6425
|
const ORMLIFECYCLE_WRAPPED_DB = Symbol.for("kitcn:OrmLifecycleWrappedDB");
|
|
6182
6426
|
const ORMLIFECYCLE_INNER_DB = Symbol.for("kitcn:OrmLifecycleInnerDB");
|
|
6427
|
+
const wrapUserHook = (hook) => hook ? (...args) => runInOrmUserCallback(() => hook(...args)) : void 0;
|
|
6183
6428
|
const isWriterDb = (db) => typeof db.insert === "function" && typeof db.patch === "function" && typeof db.delete === "function";
|
|
6184
6429
|
const isLifecycleWrappedDb = (db) => db[ORMLIFECYCLE_WRAPPED_DB] === true;
|
|
6185
6430
|
const markLifecycleWrappedDb = (db) => {
|
|
@@ -6363,6 +6608,7 @@ function writerWithHooks(ctx, innerDb, hooksByTable, isWithinHook = false) {
|
|
|
6363
6608
|
const tableHooks = hooksByTable.get(tableName);
|
|
6364
6609
|
if (!tableHooks) return innerDb.patch(tableName, id, value);
|
|
6365
6610
|
return executeThenDrainHooks(ctx, innerDb, hooksByTable, isWithinHook, async (hookCtx) => {
|
|
6611
|
+
await tableHooks.writeBarrier?.(innerDb);
|
|
6366
6612
|
const needsDocuments = Boolean(tableHooks.update?.after || tableHooks.change);
|
|
6367
6613
|
const oldDoc = needsDocuments ? await innerDb.get(tableName, id) : null;
|
|
6368
6614
|
const updatePayload = await mergeBeforeData(tableName, "update", tableHooks.update?.before, value, hookCtx);
|
|
@@ -6408,6 +6654,7 @@ function writerWithHooks(ctx, innerDb, hooksByTable, isWithinHook = false) {
|
|
|
6408
6654
|
const tableHooks = hooksByTable.get(tableName);
|
|
6409
6655
|
if (!tableHooks) return innerDb.replace(tableName, id, value);
|
|
6410
6656
|
return executeThenDrainHooks(ctx, innerDb, hooksByTable, isWithinHook, async (hookCtx) => {
|
|
6657
|
+
await tableHooks.writeBarrier?.(innerDb);
|
|
6411
6658
|
const oldDoc = Boolean(tableHooks.update?.after || tableHooks.change) ? await innerDb.get(tableName, id) : null;
|
|
6412
6659
|
const updatePayload = await mergeBeforeData(tableName, "update", tableHooks.update?.before, value, hookCtx);
|
|
6413
6660
|
await innerDb.replace(tableName, id, updatePayload);
|
|
@@ -6443,6 +6690,7 @@ function writerWithHooks(ctx, innerDb, hooksByTable, isWithinHook = false) {
|
|
|
6443
6690
|
const tableHooks = hooksByTable.get(tableName);
|
|
6444
6691
|
if (!tableHooks) return innerDb.delete(tableName, id);
|
|
6445
6692
|
return executeThenDrainHooks(ctx, innerDb, hooksByTable, isWithinHook, async (hookCtx) => {
|
|
6693
|
+
await tableHooks.writeBarrier?.(innerDb);
|
|
6446
6694
|
const oldDoc = Boolean(tableHooks.delete || tableHooks.change) ? await innerDb.get(tableName, id) : null;
|
|
6447
6695
|
if (!oldDoc) {
|
|
6448
6696
|
await innerDb.delete(tableName, id);
|
|
@@ -6478,6 +6726,7 @@ function writerWithHooks(ctx, innerDb, hooksByTable, isWithinHook = false) {
|
|
|
6478
6726
|
const tableHooks = hooksByTable.get(table);
|
|
6479
6727
|
if (!tableHooks) return innerDb.insert(table, value);
|
|
6480
6728
|
return executeThenDrainHooks(ctx, innerDb, hooksByTable, isWithinHook, async (hookCtx) => {
|
|
6729
|
+
await tableHooks.writeBarrier?.(innerDb);
|
|
6481
6730
|
const insertPayload = await mergeBeforeData(table, "create", tableHooks.create?.before, value, hookCtx);
|
|
6482
6731
|
const id = await innerDb.insert(table, insertPayload);
|
|
6483
6732
|
const newDoc = Boolean(tableHooks.create?.after || tableHooks.change) ? await innerDb.get(table, id) : null;
|
|
@@ -6540,53 +6789,57 @@ function createOrmDbLifecycle(schema, triggerDefinitions, capabilities) {
|
|
|
6540
6789
|
for (const [triggerKey, hooks] of normalizedTriggers.entries()) {
|
|
6541
6790
|
const tableName = tableNameBySchemaKey.get(triggerKey) ?? (tableNames.has(triggerKey) ? triggerKey : void 0);
|
|
6542
6791
|
if (!tableName) throw new Error(`Unknown trigger table '${triggerKey}'. Export triggers with defineTriggers(relations, { ... }) using keys from your relations export.`);
|
|
6543
|
-
tableHooks.set(tableName,
|
|
6792
|
+
tableHooks.set(tableName, {
|
|
6793
|
+
create: hooks.create && {
|
|
6794
|
+
before: wrapUserHook(hooks.create.before),
|
|
6795
|
+
after: wrapUserHook(hooks.create.after)
|
|
6796
|
+
},
|
|
6797
|
+
update: hooks.update && {
|
|
6798
|
+
before: wrapUserHook(hooks.update.before),
|
|
6799
|
+
after: wrapUserHook(hooks.update.after)
|
|
6800
|
+
},
|
|
6801
|
+
delete: hooks.delete && {
|
|
6802
|
+
before: wrapUserHook(hooks.delete.before),
|
|
6803
|
+
after: wrapUserHook(hooks.delete.after)
|
|
6804
|
+
},
|
|
6805
|
+
change: wrapUserHook(hooks.change)
|
|
6806
|
+
});
|
|
6544
6807
|
}
|
|
6545
6808
|
for (const tableConfig of Object.values(schema)) {
|
|
6546
6809
|
if (!tableConfig?.table || !tableConfig?.name) continue;
|
|
6547
6810
|
const aggregateIndexes = getAggregateIndexDefinitions(tableConfig);
|
|
6548
6811
|
const rankIndexes = getRankIndexDefinitions(tableConfig);
|
|
6549
6812
|
if (aggregateIndexes.length === 0 && rankIndexes.length === 0) continue;
|
|
6550
|
-
const
|
|
6551
|
-
const
|
|
6813
|
+
const tableName = tableConfig.name;
|
|
6814
|
+
const aggregate = requireAggregateCapability(capabilities, `Table '${tableName}' declares an aggregateIndex/rankIndex, which`);
|
|
6815
|
+
const existing = tableHooks.get(tableName) ?? {};
|
|
6552
6816
|
const existingChange = existing.change;
|
|
6817
|
+
const existingBarrier = existing.writeBarrier;
|
|
6553
6818
|
const metricIndexNames = aggregateIndexes.map((entry) => entry.name);
|
|
6554
6819
|
const rankIndexNames = rankIndexes.map((entry) => entry.name);
|
|
6555
|
-
|
|
6556
|
-
await aggregate.assertAggregateIndexesWritable(ctx.db, tableConfig.name, metricIndexNames, rankIndexNames);
|
|
6557
|
-
return before?.(data, ctx);
|
|
6558
|
-
};
|
|
6559
|
-
tableHooks.set(tableConfig.name, {
|
|
6820
|
+
tableHooks.set(tableName, {
|
|
6560
6821
|
...existing,
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
},
|
|
6565
|
-
update: {
|
|
6566
|
-
...existing.update,
|
|
6567
|
-
before: prependWriteBarrier(existing.update?.before)
|
|
6568
|
-
},
|
|
6569
|
-
delete: {
|
|
6570
|
-
...existing.delete,
|
|
6571
|
-
before: prependWriteBarrier(existing.delete?.before)
|
|
6822
|
+
writeBarrier: async (db) => {
|
|
6823
|
+
await existingBarrier?.(db);
|
|
6824
|
+
await aggregate.assertAggregateIndexesWritable(db, tableName, metricIndexNames, rankIndexNames);
|
|
6572
6825
|
},
|
|
6573
6826
|
change: async (change, ctx) => {
|
|
6574
6827
|
if (change.operation === "delete") {
|
|
6575
|
-
if (aggregateIndexes.length > 0) await aggregate.applyAggregateIndexesForChange(ctx.db,
|
|
6828
|
+
if (aggregateIndexes.length > 0) await aggregate.applyAggregateIndexesForChange(ctx.db, tableName, aggregateIndexes, {
|
|
6576
6829
|
operation: "delete",
|
|
6577
6830
|
id: change.id
|
|
6578
6831
|
});
|
|
6579
|
-
if (rankIndexes.length > 0) await aggregate.applyRankIndexesForChange(ctx.db,
|
|
6832
|
+
if (rankIndexes.length > 0) await aggregate.applyRankIndexesForChange(ctx.db, tableName, rankIndexes, {
|
|
6580
6833
|
operation: "delete",
|
|
6581
6834
|
id: change.id
|
|
6582
6835
|
});
|
|
6583
6836
|
} else {
|
|
6584
|
-
if (aggregateIndexes.length > 0) await aggregate.applyAggregateIndexesForChange(ctx.db,
|
|
6837
|
+
if (aggregateIndexes.length > 0) await aggregate.applyAggregateIndexesForChange(ctx.db, tableName, aggregateIndexes, {
|
|
6585
6838
|
operation: change.operation,
|
|
6586
6839
|
id: change.id,
|
|
6587
6840
|
newDoc: change.newDoc
|
|
6588
6841
|
});
|
|
6589
|
-
if (rankIndexes.length > 0) await aggregate.applyRankIndexesForChange(ctx.db,
|
|
6842
|
+
if (rankIndexes.length > 0) await aggregate.applyRankIndexesForChange(ctx.db, tableName, rankIndexes, {
|
|
6590
6843
|
operation: change.operation,
|
|
6591
6844
|
id: change.id,
|
|
6592
6845
|
newDoc: change.newDoc
|
|
@@ -6857,6 +7110,9 @@ var ConvexUpdateBuilder = class extends QueryPromise {
|
|
|
6857
7110
|
});
|
|
6858
7111
|
}
|
|
6859
7112
|
async execute(...args) {
|
|
7113
|
+
return await runInOrmWriteScope(this.db, () => runInOrmWriteBatch(this.db, () => this._runStatement(...args)));
|
|
7114
|
+
}
|
|
7115
|
+
async _runStatement(...args) {
|
|
6860
7116
|
if (!this.setValues) throw new Error("set() must be called before execute()");
|
|
6861
7117
|
if (Object.keys(this.setValues).length === 0) {
|
|
6862
7118
|
if (this.paginateConfig !== void 0) {
|
|
@@ -7204,7 +7460,7 @@ function createDatabase(db, schema, edgeMetadata, options) {
|
|
|
7204
7460
|
defaults,
|
|
7205
7461
|
resolvedDefaults
|
|
7206
7462
|
};
|
|
7207
|
-
const baseDb = Object.assign(Object.create(db), { [OrmContext]: ormContext });
|
|
7463
|
+
const baseDb = markOrmTransactionAnchor(Object.assign(Object.create(db), { [OrmContext]: ormContext }), db);
|
|
7208
7464
|
const query = {};
|
|
7209
7465
|
const edgesBySourceTable = getEdgesBySourceTable(edgeMetadata);
|
|
7210
7466
|
for (const [tableName, tableConfig] of Object.entries(schema)) query[tableName] = new RelationalQueryBuilder(schema, tableConfig, edgesBySourceTable.get(tableConfig.name) ?? NO_EDGES, baseDb, edgeMetadata, rls, options?.relationLoading, options?.vectorSearch);
|