@tanstack/db 0.4.19 → 0.5.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/cjs/collection/change-events.cjs +10 -12
- package/dist/cjs/collection/change-events.cjs.map +1 -1
- package/dist/cjs/collection/change-events.d.cts +1 -8
- package/dist/cjs/collection/index.cjs +19 -1
- package/dist/cjs/collection/index.cjs.map +1 -1
- package/dist/cjs/collection/index.d.cts +7 -5
- package/dist/cjs/collection/sync.cjs +7 -1
- package/dist/cjs/collection/sync.cjs.map +1 -1
- package/dist/cjs/errors.cjs +9 -4
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +4 -1
- package/dist/cjs/index.cjs +21 -3
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +2 -0
- package/dist/cjs/indexes/auto-index.cjs +7 -3
- package/dist/cjs/indexes/auto-index.cjs.map +1 -1
- package/dist/cjs/local-storage.cjs.map +1 -1
- package/dist/cjs/local-storage.d.cts +2 -2
- package/dist/cjs/query/builder/functions.cjs +34 -0
- package/dist/cjs/query/builder/functions.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.d.cts +5 -0
- package/dist/cjs/query/builder/index.cjs +2 -2
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/types.d.cts +18 -24
- package/dist/cjs/query/compiler/evaluators.cjs +57 -4
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/evaluators.d.cts +13 -0
- package/dist/cjs/query/compiler/expressions.cjs +4 -1
- package/dist/cjs/query/compiler/expressions.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.cjs +3 -3
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.cjs +2 -2
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.cjs +18 -6
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +7 -1
- package/dist/cjs/query/expression-helpers.cjs +217 -0
- package/dist/cjs/query/expression-helpers.cjs.map +1 -0
- package/dist/cjs/query/expression-helpers.d.cts +216 -0
- package/dist/cjs/query/index.d.cts +2 -0
- package/dist/cjs/query/live/collection-config-builder.cjs +34 -2
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +7 -1
- package/dist/cjs/query/live/collection-registry.cjs +2 -1
- package/dist/cjs/query/live/collection-registry.cjs.map +1 -1
- package/dist/cjs/query/live/collection-registry.d.cts +1 -1
- package/dist/cjs/query/live/internal.cjs +5 -0
- package/dist/cjs/query/live/internal.cjs.map +1 -0
- package/dist/cjs/query/live/internal.d.cts +13 -0
- package/dist/cjs/query/live/types.d.cts +6 -1
- package/dist/cjs/query/predicate-utils.cjs +816 -0
- package/dist/cjs/query/predicate-utils.cjs.map +1 -0
- package/dist/cjs/query/predicate-utils.d.cts +116 -0
- package/dist/cjs/query/subset-dedupe.cjs +111 -0
- package/dist/cjs/query/subset-dedupe.cjs.map +1 -0
- package/dist/cjs/query/subset-dedupe.d.cts +66 -0
- package/dist/cjs/types.d.cts +31 -2
- package/dist/cjs/utils/comparison.cjs +30 -0
- package/dist/cjs/utils/comparison.cjs.map +1 -1
- package/dist/cjs/utils/comparison.d.cts +7 -1
- package/dist/cjs/utils/index-optimization.cjs +26 -22
- package/dist/cjs/utils/index-optimization.cjs.map +1 -1
- package/dist/cjs/utils/index-optimization.d.cts +5 -4
- package/dist/esm/collection/change-events.d.ts +1 -8
- package/dist/esm/collection/change-events.js +7 -9
- package/dist/esm/collection/change-events.js.map +1 -1
- package/dist/esm/collection/index.d.ts +7 -5
- package/dist/esm/collection/index.js +19 -1
- package/dist/esm/collection/index.js.map +1 -1
- package/dist/esm/collection/sync.js +7 -1
- package/dist/esm/collection/sync.js.map +1 -1
- package/dist/esm/errors.d.ts +4 -1
- package/dist/esm/errors.js +9 -4
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +19 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/indexes/auto-index.js +7 -3
- package/dist/esm/indexes/auto-index.js.map +1 -1
- package/dist/esm/local-storage.d.ts +2 -2
- package/dist/esm/local-storage.js.map +1 -1
- package/dist/esm/query/builder/functions.d.ts +5 -0
- package/dist/esm/query/builder/functions.js +34 -0
- package/dist/esm/query/builder/functions.js.map +1 -1
- package/dist/esm/query/builder/index.js +2 -2
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +18 -24
- package/dist/esm/query/compiler/evaluators.d.ts +13 -0
- package/dist/esm/query/compiler/evaluators.js +59 -6
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/expressions.js +4 -1
- package/dist/esm/query/compiler/expressions.js.map +1 -1
- package/dist/esm/query/compiler/group-by.js +4 -4
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/index.js +3 -3
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +7 -1
- package/dist/esm/query/compiler/order-by.js +18 -6
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/expression-helpers.d.ts +216 -0
- package/dist/esm/query/expression-helpers.js +217 -0
- package/dist/esm/query/expression-helpers.js.map +1 -0
- package/dist/esm/query/index.d.ts +2 -0
- package/dist/esm/query/live/collection-config-builder.d.ts +7 -1
- package/dist/esm/query/live/collection-config-builder.js +34 -2
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/collection-registry.d.ts +1 -1
- package/dist/esm/query/live/collection-registry.js +2 -1
- package/dist/esm/query/live/collection-registry.js.map +1 -1
- package/dist/esm/query/live/internal.d.ts +13 -0
- package/dist/esm/query/live/internal.js +5 -0
- package/dist/esm/query/live/internal.js.map +1 -0
- package/dist/esm/query/live/types.d.ts +6 -1
- package/dist/esm/query/predicate-utils.d.ts +116 -0
- package/dist/esm/query/predicate-utils.js +816 -0
- package/dist/esm/query/predicate-utils.js.map +1 -0
- package/dist/esm/query/subset-dedupe.d.ts +66 -0
- package/dist/esm/query/subset-dedupe.js +111 -0
- package/dist/esm/query/subset-dedupe.js.map +1 -0
- package/dist/esm/types.d.ts +31 -2
- package/dist/esm/utils/comparison.d.ts +7 -1
- package/dist/esm/utils/comparison.js +30 -0
- package/dist/esm/utils/comparison.js.map +1 -1
- package/dist/esm/utils/index-optimization.d.ts +5 -4
- package/dist/esm/utils/index-optimization.js +26 -22
- package/dist/esm/utils/index-optimization.js.map +1 -1
- package/package.json +2 -2
- package/src/collection/change-events.ts +14 -24
- package/src/collection/index.ts +34 -6
- package/src/collection/sync.ts +9 -1
- package/src/errors.ts +20 -4
- package/src/index.ts +4 -0
- package/src/indexes/auto-index.ts +8 -4
- package/src/local-storage.ts +11 -3
- package/src/query/builder/functions.ts +39 -0
- package/src/query/builder/index.ts +2 -2
- package/src/query/builder/types.ts +19 -27
- package/src/query/compiler/evaluators.ts +103 -5
- package/src/query/compiler/expressions.ts +3 -0
- package/src/query/compiler/group-by.ts +4 -4
- package/src/query/compiler/index.ts +3 -3
- package/src/query/compiler/order-by.ts +33 -7
- package/src/query/expression-helpers.ts +522 -0
- package/src/query/index.ts +12 -0
- package/src/query/live/collection-config-builder.ts +54 -2
- package/src/query/live/collection-registry.ts +3 -2
- package/src/query/live/internal.ts +15 -0
- package/src/query/live/types.ts +11 -1
- package/src/query/predicate-utils.ts +1415 -0
- package/src/query/subset-dedupe.ts +243 -0
- package/src/types.ts +41 -2
- package/src/utils/comparison.ts +70 -1
- package/src/utils/index-optimization.ts +77 -63
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
function extractFieldPath(expr) {
|
|
4
|
+
if (expr.type === `ref`) {
|
|
5
|
+
return expr.path;
|
|
6
|
+
}
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
function extractValue(expr) {
|
|
10
|
+
if (expr.type === `val`) {
|
|
11
|
+
return expr.value;
|
|
12
|
+
}
|
|
13
|
+
return void 0;
|
|
14
|
+
}
|
|
15
|
+
function walkExpression(expr, visitor) {
|
|
16
|
+
if (!expr) return;
|
|
17
|
+
visitor(expr);
|
|
18
|
+
if (expr.type === `func`) {
|
|
19
|
+
expr.args.forEach((arg) => walkExpression(arg, visitor));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function parseWhereExpression(expr, options) {
|
|
23
|
+
if (!expr) return null;
|
|
24
|
+
const { handlers, onUnknownOperator } = options;
|
|
25
|
+
if (expr.type === `val`) {
|
|
26
|
+
return expr.value;
|
|
27
|
+
}
|
|
28
|
+
if (expr.type === `ref`) {
|
|
29
|
+
return expr.path;
|
|
30
|
+
}
|
|
31
|
+
const { name, args } = expr;
|
|
32
|
+
const handler = handlers[name];
|
|
33
|
+
if (!handler) {
|
|
34
|
+
if (onUnknownOperator) {
|
|
35
|
+
return onUnknownOperator(name, args);
|
|
36
|
+
}
|
|
37
|
+
throw new Error(
|
|
38
|
+
`No handler provided for operator: ${name}. Available handlers: ${Object.keys(handlers).join(`, `)}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const parsedArgs = args.map((arg) => {
|
|
42
|
+
if (arg.type === `ref`) {
|
|
43
|
+
return arg.path;
|
|
44
|
+
}
|
|
45
|
+
if (arg.type === `val`) {
|
|
46
|
+
return arg.value;
|
|
47
|
+
}
|
|
48
|
+
return parseWhereExpression(arg, options);
|
|
49
|
+
});
|
|
50
|
+
return handler(...parsedArgs);
|
|
51
|
+
}
|
|
52
|
+
function parseOrderByExpression(orderBy) {
|
|
53
|
+
if (!orderBy || orderBy.length === 0) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
return orderBy.map((clause) => {
|
|
57
|
+
const field = extractFieldPath(clause.expression);
|
|
58
|
+
if (!field) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`ORDER BY expression must be a field reference, got: ${clause.expression.type}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const { direction, nulls } = clause.compareOptions;
|
|
64
|
+
const result = {
|
|
65
|
+
field,
|
|
66
|
+
direction,
|
|
67
|
+
nulls
|
|
68
|
+
};
|
|
69
|
+
if (`stringSort` in clause.compareOptions) {
|
|
70
|
+
result.stringSort = clause.compareOptions.stringSort;
|
|
71
|
+
}
|
|
72
|
+
if (`locale` in clause.compareOptions) {
|
|
73
|
+
result.locale = clause.compareOptions.locale;
|
|
74
|
+
}
|
|
75
|
+
if (`localeOptions` in clause.compareOptions) {
|
|
76
|
+
result.localeOptions = clause.compareOptions.localeOptions;
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function extractSimpleComparisons(expr) {
|
|
82
|
+
if (!expr) return [];
|
|
83
|
+
const comparisons = [];
|
|
84
|
+
function extract(e) {
|
|
85
|
+
if (e.type === `func`) {
|
|
86
|
+
if (e.name === `and`) {
|
|
87
|
+
e.args.forEach((arg) => extract(arg));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (e.name === `not`) {
|
|
91
|
+
const [arg] = e.args;
|
|
92
|
+
if (!arg || arg.type !== `func`) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`extractSimpleComparisons requires a comparison or null check inside 'not' operator.`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const nullCheckOps2 = [`isNull`, `isUndefined`];
|
|
98
|
+
if (nullCheckOps2.includes(arg.name)) {
|
|
99
|
+
const [fieldArg] = arg.args;
|
|
100
|
+
const field = fieldArg?.type === `ref` ? fieldArg.path : null;
|
|
101
|
+
if (field) {
|
|
102
|
+
comparisons.push({
|
|
103
|
+
field,
|
|
104
|
+
operator: `not_${arg.name}`
|
|
105
|
+
// No value for null/undefined checks
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`extractSimpleComparisons requires a field reference for '${arg.name}' operator.`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const comparisonOps2 = [`eq`, `gt`, `gte`, `lt`, `lte`, `in`];
|
|
115
|
+
if (comparisonOps2.includes(arg.name)) {
|
|
116
|
+
const [leftArg, rightArg] = arg.args;
|
|
117
|
+
const field = leftArg?.type === `ref` ? leftArg.path : null;
|
|
118
|
+
const value = rightArg?.type === `val` ? rightArg.value : null;
|
|
119
|
+
if (field && value !== void 0) {
|
|
120
|
+
comparisons.push({
|
|
121
|
+
field,
|
|
122
|
+
operator: `not_${arg.name}`,
|
|
123
|
+
value
|
|
124
|
+
});
|
|
125
|
+
} else {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`extractSimpleComparisons requires simple field-value comparisons. Found complex expression for 'not(${arg.name})' operator.`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
throw new Error(
|
|
133
|
+
`extractSimpleComparisons does not support 'not(${arg.name})'. NOT can only wrap comparison operators (eq, gt, gte, lt, lte, in) or null checks (isNull, isUndefined).`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
const unsupportedOps = [
|
|
137
|
+
`or`,
|
|
138
|
+
`like`,
|
|
139
|
+
`ilike`,
|
|
140
|
+
`upper`,
|
|
141
|
+
`lower`,
|
|
142
|
+
`length`,
|
|
143
|
+
`concat`,
|
|
144
|
+
`add`,
|
|
145
|
+
`coalesce`,
|
|
146
|
+
`count`,
|
|
147
|
+
`avg`,
|
|
148
|
+
`sum`,
|
|
149
|
+
`min`,
|
|
150
|
+
`max`
|
|
151
|
+
];
|
|
152
|
+
if (unsupportedOps.includes(e.name)) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`extractSimpleComparisons does not support '${e.name}' operator. Use parseWhereExpression with custom handlers for complex expressions.`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const nullCheckOps = [`isNull`, `isUndefined`];
|
|
158
|
+
if (nullCheckOps.includes(e.name)) {
|
|
159
|
+
const [fieldArg] = e.args;
|
|
160
|
+
const field = fieldArg?.type === `ref` ? fieldArg.path : null;
|
|
161
|
+
if (field) {
|
|
162
|
+
comparisons.push({
|
|
163
|
+
field,
|
|
164
|
+
operator: e.name
|
|
165
|
+
// No value for null/undefined checks
|
|
166
|
+
});
|
|
167
|
+
} else {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`extractSimpleComparisons requires a field reference for '${e.name}' operator.`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `in`];
|
|
175
|
+
if (comparisonOps.includes(e.name)) {
|
|
176
|
+
const [leftArg, rightArg] = e.args;
|
|
177
|
+
const field = leftArg?.type === `ref` ? leftArg.path : null;
|
|
178
|
+
const value = rightArg?.type === `val` ? rightArg.value : null;
|
|
179
|
+
if (field && value !== void 0) {
|
|
180
|
+
comparisons.push({
|
|
181
|
+
field,
|
|
182
|
+
operator: e.name,
|
|
183
|
+
value
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`extractSimpleComparisons requires simple field-value comparisons. Found complex expression for '${e.name}' operator.`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`extractSimpleComparisons encountered unknown operator: '${e.name}'`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
extract(expr);
|
|
198
|
+
return comparisons;
|
|
199
|
+
}
|
|
200
|
+
function parseLoadSubsetOptions(options) {
|
|
201
|
+
if (!options) {
|
|
202
|
+
return { filters: [], sorts: [] };
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
filters: extractSimpleComparisons(options.where),
|
|
206
|
+
sorts: parseOrderByExpression(options.orderBy),
|
|
207
|
+
limit: options.limit
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
exports.extractFieldPath = extractFieldPath;
|
|
211
|
+
exports.extractSimpleComparisons = extractSimpleComparisons;
|
|
212
|
+
exports.extractValue = extractValue;
|
|
213
|
+
exports.parseLoadSubsetOptions = parseLoadSubsetOptions;
|
|
214
|
+
exports.parseOrderByExpression = parseOrderByExpression;
|
|
215
|
+
exports.parseWhereExpression = parseWhereExpression;
|
|
216
|
+
exports.walkExpression = walkExpression;
|
|
217
|
+
//# sourceMappingURL=expression-helpers.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expression-helpers.cjs","sources":["../../../src/query/expression-helpers.ts"],"sourcesContent":["/**\n * Expression Helpers for TanStack DB\n *\n * These utilities help parse LoadSubsetOptions (where, orderBy, limit) from TanStack DB\n * into formats suitable for your API backend. They provide a generic way to traverse\n * expression trees without having to implement your own parser.\n *\n * @example\n * ```typescript\n * import { parseWhereExpression, parseOrderByExpression } from '@tanstack/db'\n *\n * queryFn: async (ctx) => {\n * const { limit, where, orderBy } = ctx.meta?.loadSubsetOptions ?? {}\n *\n * // Convert expression tree to filters\n * const filters = parseWhereExpression(where, {\n * eq: (field, value) => ({ [field]: value }),\n * lt: (field, value) => ({ [`${field}_lt`]: value }),\n * and: (filters) => Object.assign({}, ...filters)\n * })\n *\n * // Extract sort information\n * const sort = parseOrderByExpression(orderBy)\n *\n * return api.getProducts({ ...filters, sort, limit })\n * }\n * ```\n */\n\nimport type { IR, OperatorName } from \"../index.js\"\n\ntype BasicExpression<T = any> = IR.BasicExpression<T>\ntype OrderBy = IR.OrderBy\n\n/**\n * Represents a simple field path extracted from an expression.\n * Can include string keys for object properties and numbers for array indices.\n */\nexport type FieldPath = Array<string | number>\n\n/**\n * Represents a simple comparison operation\n */\nexport interface SimpleComparison {\n field: FieldPath\n operator: string\n value?: any // Optional for operators like isNull and isUndefined that don't have a value\n}\n\n/**\n * Options for customizing how WHERE expressions are parsed\n */\nexport interface ParseWhereOptions<T = any> {\n /**\n * Handler functions for different operators.\n * Each handler receives the parsed field path(s) and value(s) and returns your custom format.\n *\n * Supported operators from TanStack DB:\n * - Comparison: eq, gt, gte, lt, lte, in, like, ilike\n * - Logical: and, or, not\n * - Null checking: isNull, isUndefined\n * - String functions: upper, lower, length, concat\n * - Numeric: add\n * - Utility: coalesce\n * - Aggregates: count, avg, sum, min, max\n */\n handlers: {\n [K in OperatorName]?: (...args: Array<any>) => T\n } & {\n [key: string]: (...args: Array<any>) => T\n }\n /**\n * Optional handler for when an unknown operator is encountered.\n * If not provided, unknown operators throw an error.\n */\n onUnknownOperator?: (operator: string, args: Array<any>) => T\n}\n\n/**\n * Result of parsing an ORDER BY expression\n */\nexport interface ParsedOrderBy {\n field: FieldPath\n direction: `asc` | `desc`\n nulls: `first` | `last`\n /** String sorting method: 'lexical' (default) or 'locale' (locale-aware) */\n stringSort?: `lexical` | `locale`\n /** Locale for locale-aware string sorting (e.g., 'en-US') */\n locale?: string\n /** Additional options for locale-aware sorting */\n localeOptions?: object\n}\n\n/**\n * Extracts the field path from a PropRef expression.\n * Returns null for non-ref expressions.\n *\n * @param expr - The expression to extract from\n * @returns The field path array, or null\n *\n * @example\n * ```typescript\n * const field = extractFieldPath(someExpression)\n * // Returns: ['product', 'category']\n * ```\n */\nexport function extractFieldPath(expr: BasicExpression): FieldPath | null {\n if (expr.type === `ref`) {\n return expr.path\n }\n return null\n}\n\n/**\n * Extracts the value from a Value expression.\n * Returns undefined for non-value expressions.\n *\n * @param expr - The expression to extract from\n * @returns The extracted value\n *\n * @example\n * ```typescript\n * const val = extractValue(someExpression)\n * // Returns: 'electronics'\n * ```\n */\nexport function extractValue(expr: BasicExpression): any {\n if (expr.type === `val`) {\n return expr.value\n }\n return undefined\n}\n\n/**\n * Generic expression tree walker that visits each node in the expression.\n * Useful for implementing custom parsing logic.\n *\n * @param expr - The expression to walk\n * @param visitor - Visitor function called for each node\n *\n * @example\n * ```typescript\n * walkExpression(whereExpr, (node) => {\n * if (node.type === 'func' && node.name === 'eq') {\n * console.log('Found equality comparison')\n * }\n * })\n * ```\n */\nexport function walkExpression(\n expr: BasicExpression | undefined | null,\n visitor: (node: BasicExpression) => void\n): void {\n if (!expr) return\n\n visitor(expr)\n\n if (expr.type === `func`) {\n expr.args.forEach((arg: BasicExpression) => walkExpression(arg, visitor))\n }\n}\n\n/**\n * Parses a WHERE expression into a custom format using provided handlers.\n *\n * This is the main helper for converting TanStack DB where clauses into your API's filter format.\n * You provide handlers for each operator, and this function traverses the expression tree\n * and calls the appropriate handlers.\n *\n * @param expr - The WHERE expression to parse\n * @param options - Configuration with handler functions for each operator\n * @returns The parsed result in your custom format\n *\n * @example\n * ```typescript\n * // REST API with query parameters\n * const filters = parseWhereExpression(where, {\n * handlers: {\n * eq: (field, value) => ({ [field.join('.')]: value }),\n * lt: (field, value) => ({ [`${field.join('.')}_lt`]: value }),\n * gt: (field, value) => ({ [`${field.join('.')}_gt`]: value }),\n * and: (...filters) => Object.assign({}, ...filters),\n * or: (...filters) => ({ $or: filters })\n * }\n * })\n * // Returns: { category: 'electronics', price_lt: 100 }\n * ```\n *\n * @example\n * ```typescript\n * // GraphQL where clause\n * const where = parseWhereExpression(whereExpr, {\n * handlers: {\n * eq: (field, value) => ({ [field.join('_')]: { _eq: value } }),\n * lt: (field, value) => ({ [field.join('_')]: { _lt: value } }),\n * and: (...filters) => ({ _and: filters })\n * }\n * })\n * ```\n */\nexport function parseWhereExpression<T = any>(\n expr: BasicExpression<boolean> | undefined | null,\n options: ParseWhereOptions<T>\n): T | null {\n if (!expr) return null\n\n const { handlers, onUnknownOperator } = options\n\n // Handle value expressions\n if (expr.type === `val`) {\n return expr.value as unknown as T\n }\n\n // Handle property references\n if (expr.type === `ref`) {\n return expr.path as unknown as T\n }\n\n // Handle function expressions\n // After checking val and ref, expr must be func\n const { name, args } = expr\n const handler = handlers[name]\n\n if (!handler) {\n if (onUnknownOperator) {\n return onUnknownOperator(name, args)\n }\n throw new Error(\n `No handler provided for operator: ${name}. Available handlers: ${Object.keys(handlers).join(`, `)}`\n )\n }\n\n // Parse arguments recursively\n const parsedArgs = args.map((arg: BasicExpression) => {\n // For refs, extract the field path\n if (arg.type === `ref`) {\n return arg.path\n }\n // For values, extract the value\n if (arg.type === `val`) {\n return arg.value\n }\n // For nested functions, recurse (after checking ref and val, must be func)\n return parseWhereExpression(arg, options)\n })\n\n return handler(...parsedArgs)\n}\n\n/**\n * Parses an ORDER BY expression into a simple array of sort specifications.\n *\n * @param orderBy - The ORDER BY expression array\n * @returns Array of parsed order by specifications\n *\n * @example\n * ```typescript\n * const sorts = parseOrderByExpression(orderBy)\n * // Returns: [\n * // { field: ['category'], direction: 'asc', nulls: 'last' },\n * // { field: ['price'], direction: 'desc', nulls: 'last' }\n * // ]\n * ```\n */\nexport function parseOrderByExpression(\n orderBy: OrderBy | undefined | null\n): Array<ParsedOrderBy> {\n if (!orderBy || orderBy.length === 0) {\n return []\n }\n\n return orderBy.map((clause: IR.OrderByClause) => {\n const field = extractFieldPath(clause.expression)\n\n if (!field) {\n throw new Error(\n `ORDER BY expression must be a field reference, got: ${clause.expression.type}`\n )\n }\n\n const { direction, nulls } = clause.compareOptions\n const result: ParsedOrderBy = {\n field,\n direction,\n nulls,\n }\n\n // Add string collation options if present (discriminated union)\n if (`stringSort` in clause.compareOptions) {\n result.stringSort = clause.compareOptions.stringSort\n }\n if (`locale` in clause.compareOptions) {\n result.locale = clause.compareOptions.locale\n }\n if (`localeOptions` in clause.compareOptions) {\n result.localeOptions = clause.compareOptions.localeOptions\n }\n\n return result\n })\n}\n\n/**\n * Extracts all simple comparisons from a WHERE expression.\n * This is useful for simple APIs that only support basic filters.\n *\n * Note: This only works for simple AND-ed conditions and NOT-wrapped comparisons.\n * Throws an error if it encounters unsupported operations like OR or complex nested expressions.\n *\n * NOT operators are flattened by prefixing the operator name (e.g., `not(eq(...))` becomes `not_eq`).\n *\n * @param expr - The WHERE expression to parse\n * @returns Array of simple comparisons\n * @throws Error if expression contains OR or other unsupported operations\n *\n * @example\n * ```typescript\n * const comparisons = extractSimpleComparisons(where)\n * // Returns: [\n * // { field: ['category'], operator: 'eq', value: 'electronics' },\n * // { field: ['price'], operator: 'lt', value: 100 },\n * // { field: ['email'], operator: 'isNull' }, // No value for null checks\n * // { field: ['status'], operator: 'not_eq', value: 'archived' }\n * // ]\n * ```\n */\nexport function extractSimpleComparisons(\n expr: BasicExpression<boolean> | undefined | null\n): Array<SimpleComparison> {\n if (!expr) return []\n\n const comparisons: Array<SimpleComparison> = []\n\n function extract(e: BasicExpression): void {\n if (e.type === `func`) {\n // Handle AND - recurse into both sides\n if (e.name === `and`) {\n e.args.forEach((arg: BasicExpression) => extract(arg))\n return\n }\n\n // Handle NOT - recurse into argument and prefix operator with 'not_'\n if (e.name === `not`) {\n const [arg] = e.args\n if (!arg || arg.type !== `func`) {\n throw new Error(\n `extractSimpleComparisons requires a comparison or null check inside 'not' operator.`\n )\n }\n\n // Handle NOT with null/undefined checks\n const nullCheckOps = [`isNull`, `isUndefined`]\n if (nullCheckOps.includes(arg.name)) {\n const [fieldArg] = arg.args\n const field = fieldArg?.type === `ref` ? fieldArg.path : null\n\n if (field) {\n comparisons.push({\n field,\n operator: `not_${arg.name}`,\n // No value for null/undefined checks\n })\n } else {\n throw new Error(\n `extractSimpleComparisons requires a field reference for '${arg.name}' operator.`\n )\n }\n return\n }\n\n // Handle NOT with comparison operators\n const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `in`]\n if (comparisonOps.includes(arg.name)) {\n const [leftArg, rightArg] = arg.args\n const field = leftArg?.type === `ref` ? leftArg.path : null\n const value = rightArg?.type === `val` ? rightArg.value : null\n\n if (field && value !== undefined) {\n comparisons.push({\n field,\n operator: `not_${arg.name}`,\n value,\n })\n } else {\n throw new Error(\n `extractSimpleComparisons requires simple field-value comparisons. Found complex expression for 'not(${arg.name})' operator.`\n )\n }\n return\n }\n\n // NOT can only wrap simple comparisons or null checks\n throw new Error(\n `extractSimpleComparisons does not support 'not(${arg.name})'. NOT can only wrap comparison operators (eq, gt, gte, lt, lte, in) or null checks (isNull, isUndefined).`\n )\n }\n\n // Throw on unsupported operations\n const unsupportedOps = [\n `or`,\n `like`,\n `ilike`,\n `upper`,\n `lower`,\n `length`,\n `concat`,\n `add`,\n `coalesce`,\n `count`,\n `avg`,\n `sum`,\n `min`,\n `max`,\n ]\n if (unsupportedOps.includes(e.name)) {\n throw new Error(\n `extractSimpleComparisons does not support '${e.name}' operator. Use parseWhereExpression with custom handlers for complex expressions.`\n )\n }\n\n // Handle null/undefined check operators (single argument, no value)\n const nullCheckOps = [`isNull`, `isUndefined`]\n if (nullCheckOps.includes(e.name)) {\n const [fieldArg] = e.args\n\n // Extract field (must be a ref)\n const field = fieldArg?.type === `ref` ? fieldArg.path : null\n\n if (field) {\n comparisons.push({\n field,\n operator: e.name,\n // No value for null/undefined checks\n })\n } else {\n throw new Error(\n `extractSimpleComparisons requires a field reference for '${e.name}' operator.`\n )\n }\n return\n }\n\n // Handle comparison operators\n const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `in`]\n if (comparisonOps.includes(e.name)) {\n const [leftArg, rightArg] = e.args\n\n // Extract field and value\n const field = leftArg?.type === `ref` ? leftArg.path : null\n const value = rightArg?.type === `val` ? rightArg.value : null\n\n if (field && value !== undefined) {\n comparisons.push({\n field,\n operator: e.name,\n value,\n })\n } else {\n throw new Error(\n `extractSimpleComparisons requires simple field-value comparisons. Found complex expression for '${e.name}' operator.`\n )\n }\n } else {\n // Unknown operator\n throw new Error(\n `extractSimpleComparisons encountered unknown operator: '${e.name}'`\n )\n }\n }\n }\n\n extract(expr)\n return comparisons\n}\n\n/**\n * Convenience function to get all LoadSubsetOptions in a pre-parsed format.\n * Good starting point for simple use cases.\n *\n * @param options - The LoadSubsetOptions from ctx.meta\n * @returns Pre-parsed filters, sorts, and limit\n *\n * @example\n * ```typescript\n * queryFn: async (ctx) => {\n * const parsed = parseLoadSubsetOptions(ctx.meta?.loadSubsetOptions)\n *\n * // Convert to your API format\n * return api.getProducts({\n * ...Object.fromEntries(\n * parsed.filters.map(f => [`${f.field.join('.')}_${f.operator}`, f.value])\n * ),\n * sort: parsed.sorts.map(s => `${s.field.join('.')}:${s.direction}`).join(','),\n * limit: parsed.limit\n * })\n * }\n * ```\n */\nexport function parseLoadSubsetOptions(\n options:\n | {\n where?: BasicExpression<boolean>\n orderBy?: OrderBy\n limit?: number\n }\n | undefined\n | null\n): {\n filters: Array<SimpleComparison>\n sorts: Array<ParsedOrderBy>\n limit?: number\n} {\n if (!options) {\n return { filters: [], sorts: [] }\n }\n\n return {\n filters: extractSimpleComparisons(options.where),\n sorts: parseOrderByExpression(options.orderBy),\n limit: options.limit,\n }\n}\n"],"names":["nullCheckOps","comparisonOps"],"mappings":"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"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { IR, OperatorName } from '../index.js';
|
|
2
|
+
type BasicExpression<T = any> = IR.BasicExpression<T>;
|
|
3
|
+
type OrderBy = IR.OrderBy;
|
|
4
|
+
/**
|
|
5
|
+
* Represents a simple field path extracted from an expression.
|
|
6
|
+
* Can include string keys for object properties and numbers for array indices.
|
|
7
|
+
*/
|
|
8
|
+
export type FieldPath = Array<string | number>;
|
|
9
|
+
/**
|
|
10
|
+
* Represents a simple comparison operation
|
|
11
|
+
*/
|
|
12
|
+
export interface SimpleComparison {
|
|
13
|
+
field: FieldPath;
|
|
14
|
+
operator: string;
|
|
15
|
+
value?: any;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Options for customizing how WHERE expressions are parsed
|
|
19
|
+
*/
|
|
20
|
+
export interface ParseWhereOptions<T = any> {
|
|
21
|
+
/**
|
|
22
|
+
* Handler functions for different operators.
|
|
23
|
+
* Each handler receives the parsed field path(s) and value(s) and returns your custom format.
|
|
24
|
+
*
|
|
25
|
+
* Supported operators from TanStack DB:
|
|
26
|
+
* - Comparison: eq, gt, gte, lt, lte, in, like, ilike
|
|
27
|
+
* - Logical: and, or, not
|
|
28
|
+
* - Null checking: isNull, isUndefined
|
|
29
|
+
* - String functions: upper, lower, length, concat
|
|
30
|
+
* - Numeric: add
|
|
31
|
+
* - Utility: coalesce
|
|
32
|
+
* - Aggregates: count, avg, sum, min, max
|
|
33
|
+
*/
|
|
34
|
+
handlers: {
|
|
35
|
+
[K in OperatorName]?: (...args: Array<any>) => T;
|
|
36
|
+
} & {
|
|
37
|
+
[key: string]: (...args: Array<any>) => T;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Optional handler for when an unknown operator is encountered.
|
|
41
|
+
* If not provided, unknown operators throw an error.
|
|
42
|
+
*/
|
|
43
|
+
onUnknownOperator?: (operator: string, args: Array<any>) => T;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Result of parsing an ORDER BY expression
|
|
47
|
+
*/
|
|
48
|
+
export interface ParsedOrderBy {
|
|
49
|
+
field: FieldPath;
|
|
50
|
+
direction: `asc` | `desc`;
|
|
51
|
+
nulls: `first` | `last`;
|
|
52
|
+
/** String sorting method: 'lexical' (default) or 'locale' (locale-aware) */
|
|
53
|
+
stringSort?: `lexical` | `locale`;
|
|
54
|
+
/** Locale for locale-aware string sorting (e.g., 'en-US') */
|
|
55
|
+
locale?: string;
|
|
56
|
+
/** Additional options for locale-aware sorting */
|
|
57
|
+
localeOptions?: object;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Extracts the field path from a PropRef expression.
|
|
61
|
+
* Returns null for non-ref expressions.
|
|
62
|
+
*
|
|
63
|
+
* @param expr - The expression to extract from
|
|
64
|
+
* @returns The field path array, or null
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* const field = extractFieldPath(someExpression)
|
|
69
|
+
* // Returns: ['product', 'category']
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export declare function extractFieldPath(expr: BasicExpression): FieldPath | null;
|
|
73
|
+
/**
|
|
74
|
+
* Extracts the value from a Value expression.
|
|
75
|
+
* Returns undefined for non-value expressions.
|
|
76
|
+
*
|
|
77
|
+
* @param expr - The expression to extract from
|
|
78
|
+
* @returns The extracted value
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* const val = extractValue(someExpression)
|
|
83
|
+
* // Returns: 'electronics'
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export declare function extractValue(expr: BasicExpression): any;
|
|
87
|
+
/**
|
|
88
|
+
* Generic expression tree walker that visits each node in the expression.
|
|
89
|
+
* Useful for implementing custom parsing logic.
|
|
90
|
+
*
|
|
91
|
+
* @param expr - The expression to walk
|
|
92
|
+
* @param visitor - Visitor function called for each node
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* walkExpression(whereExpr, (node) => {
|
|
97
|
+
* if (node.type === 'func' && node.name === 'eq') {
|
|
98
|
+
* console.log('Found equality comparison')
|
|
99
|
+
* }
|
|
100
|
+
* })
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export declare function walkExpression(expr: BasicExpression | undefined | null, visitor: (node: BasicExpression) => void): void;
|
|
104
|
+
/**
|
|
105
|
+
* Parses a WHERE expression into a custom format using provided handlers.
|
|
106
|
+
*
|
|
107
|
+
* This is the main helper for converting TanStack DB where clauses into your API's filter format.
|
|
108
|
+
* You provide handlers for each operator, and this function traverses the expression tree
|
|
109
|
+
* and calls the appropriate handlers.
|
|
110
|
+
*
|
|
111
|
+
* @param expr - The WHERE expression to parse
|
|
112
|
+
* @param options - Configuration with handler functions for each operator
|
|
113
|
+
* @returns The parsed result in your custom format
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```typescript
|
|
117
|
+
* // REST API with query parameters
|
|
118
|
+
* const filters = parseWhereExpression(where, {
|
|
119
|
+
* handlers: {
|
|
120
|
+
* eq: (field, value) => ({ [field.join('.')]: value }),
|
|
121
|
+
* lt: (field, value) => ({ [`${field.join('.')}_lt`]: value }),
|
|
122
|
+
* gt: (field, value) => ({ [`${field.join('.')}_gt`]: value }),
|
|
123
|
+
* and: (...filters) => Object.assign({}, ...filters),
|
|
124
|
+
* or: (...filters) => ({ $or: filters })
|
|
125
|
+
* }
|
|
126
|
+
* })
|
|
127
|
+
* // Returns: { category: 'electronics', price_lt: 100 }
|
|
128
|
+
* ```
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```typescript
|
|
132
|
+
* // GraphQL where clause
|
|
133
|
+
* const where = parseWhereExpression(whereExpr, {
|
|
134
|
+
* handlers: {
|
|
135
|
+
* eq: (field, value) => ({ [field.join('_')]: { _eq: value } }),
|
|
136
|
+
* lt: (field, value) => ({ [field.join('_')]: { _lt: value } }),
|
|
137
|
+
* and: (...filters) => ({ _and: filters })
|
|
138
|
+
* }
|
|
139
|
+
* })
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
export declare function parseWhereExpression<T = any>(expr: BasicExpression<boolean> | undefined | null, options: ParseWhereOptions<T>): T | null;
|
|
143
|
+
/**
|
|
144
|
+
* Parses an ORDER BY expression into a simple array of sort specifications.
|
|
145
|
+
*
|
|
146
|
+
* @param orderBy - The ORDER BY expression array
|
|
147
|
+
* @returns Array of parsed order by specifications
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* const sorts = parseOrderByExpression(orderBy)
|
|
152
|
+
* // Returns: [
|
|
153
|
+
* // { field: ['category'], direction: 'asc', nulls: 'last' },
|
|
154
|
+
* // { field: ['price'], direction: 'desc', nulls: 'last' }
|
|
155
|
+
* // ]
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
export declare function parseOrderByExpression(orderBy: OrderBy | undefined | null): Array<ParsedOrderBy>;
|
|
159
|
+
/**
|
|
160
|
+
* Extracts all simple comparisons from a WHERE expression.
|
|
161
|
+
* This is useful for simple APIs that only support basic filters.
|
|
162
|
+
*
|
|
163
|
+
* Note: This only works for simple AND-ed conditions and NOT-wrapped comparisons.
|
|
164
|
+
* Throws an error if it encounters unsupported operations like OR or complex nested expressions.
|
|
165
|
+
*
|
|
166
|
+
* NOT operators are flattened by prefixing the operator name (e.g., `not(eq(...))` becomes `not_eq`).
|
|
167
|
+
*
|
|
168
|
+
* @param expr - The WHERE expression to parse
|
|
169
|
+
* @returns Array of simple comparisons
|
|
170
|
+
* @throws Error if expression contains OR or other unsupported operations
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```typescript
|
|
174
|
+
* const comparisons = extractSimpleComparisons(where)
|
|
175
|
+
* // Returns: [
|
|
176
|
+
* // { field: ['category'], operator: 'eq', value: 'electronics' },
|
|
177
|
+
* // { field: ['price'], operator: 'lt', value: 100 },
|
|
178
|
+
* // { field: ['email'], operator: 'isNull' }, // No value for null checks
|
|
179
|
+
* // { field: ['status'], operator: 'not_eq', value: 'archived' }
|
|
180
|
+
* // ]
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
export declare function extractSimpleComparisons(expr: BasicExpression<boolean> | undefined | null): Array<SimpleComparison>;
|
|
184
|
+
/**
|
|
185
|
+
* Convenience function to get all LoadSubsetOptions in a pre-parsed format.
|
|
186
|
+
* Good starting point for simple use cases.
|
|
187
|
+
*
|
|
188
|
+
* @param options - The LoadSubsetOptions from ctx.meta
|
|
189
|
+
* @returns Pre-parsed filters, sorts, and limit
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```typescript
|
|
193
|
+
* queryFn: async (ctx) => {
|
|
194
|
+
* const parsed = parseLoadSubsetOptions(ctx.meta?.loadSubsetOptions)
|
|
195
|
+
*
|
|
196
|
+
* // Convert to your API format
|
|
197
|
+
* return api.getProducts({
|
|
198
|
+
* ...Object.fromEntries(
|
|
199
|
+
* parsed.filters.map(f => [`${f.field.join('.')}_${f.operator}`, f.value])
|
|
200
|
+
* ),
|
|
201
|
+
* sort: parsed.sorts.map(s => `${s.field.join('.')}:${s.direction}`).join(','),
|
|
202
|
+
* limit: parsed.limit
|
|
203
|
+
* })
|
|
204
|
+
* }
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
export declare function parseLoadSubsetOptions(options: {
|
|
208
|
+
where?: BasicExpression<boolean>;
|
|
209
|
+
orderBy?: OrderBy;
|
|
210
|
+
limit?: number;
|
|
211
|
+
} | undefined | null): {
|
|
212
|
+
filters: Array<SimpleComparison>;
|
|
213
|
+
sorts: Array<ParsedOrderBy>;
|
|
214
|
+
limit?: number;
|
|
215
|
+
};
|
|
216
|
+
export {};
|
|
@@ -5,3 +5,5 @@ export { compileQuery } from './compiler/index.js';
|
|
|
5
5
|
export { createLiveQueryCollection, liveQueryCollectionOptions, } from './live-query-collection.js';
|
|
6
6
|
export { type LiveQueryCollectionConfig } from './live/types.js';
|
|
7
7
|
export { type LiveQueryCollectionUtils } from './live/collection-config-builder.js';
|
|
8
|
+
export { isWhereSubset, unionWherePredicates, minusWherePredicates, isOrderBySubset, isLimitSubset, isPredicateSubset, } from './predicate-utils.js';
|
|
9
|
+
export { DeduplicatedLoadSubset } from './subset-dedupe.js';
|
|
@@ -8,6 +8,7 @@ const scheduler = require("../../scheduler.cjs");
|
|
|
8
8
|
const transactions = require("../../transactions.cjs");
|
|
9
9
|
const collectionSubscriber = require("./collection-subscriber.cjs");
|
|
10
10
|
const collectionRegistry = require("./collection-registry.cjs");
|
|
11
|
+
const internal = require("./internal.cjs");
|
|
11
12
|
let liveQueryCollectionCounter = 0;
|
|
12
13
|
class CollectionConfigBuilder {
|
|
13
14
|
constructor(config) {
|
|
@@ -40,14 +41,30 @@ class CollectionConfigBuilder {
|
|
|
40
41
|
if (this.query.orderBy && this.query.orderBy.length > 0) {
|
|
41
42
|
this.compare = createOrderByComparator(this.orderByIndices);
|
|
42
43
|
}
|
|
44
|
+
this.compareOptions = this.config.defaultStringCollation ?? extractCollectionFromSource(this.query).compareOptions;
|
|
43
45
|
this.compileBasePipeline();
|
|
44
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Recursively checks if a query or any of its subqueries contains joins
|
|
49
|
+
*/
|
|
50
|
+
hasJoins(query) {
|
|
51
|
+
if (query.join && query.join.length > 0) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
if (query.from.type === `queryRef`) {
|
|
55
|
+
if (this.hasJoins(query.from.query)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
45
61
|
getConfig() {
|
|
46
62
|
return {
|
|
47
63
|
id: this.id,
|
|
48
64
|
getKey: this.config.getKey || ((item) => this.resultKeys.get(item)),
|
|
49
65
|
sync: this.getSyncConfig(),
|
|
50
66
|
compare: this.compare,
|
|
67
|
+
defaultStringCollation: this.compareOptions,
|
|
51
68
|
gcTime: this.config.gcTime || 5e3,
|
|
52
69
|
// 5 seconds by default for live queries
|
|
53
70
|
schema: this.config.schema,
|
|
@@ -58,9 +75,13 @@ class CollectionConfigBuilder {
|
|
|
58
75
|
singleResult: this.query.singleResult,
|
|
59
76
|
utils: {
|
|
60
77
|
getRunCount: this.getRunCount.bind(this),
|
|
61
|
-
getBuilder: () => this,
|
|
62
78
|
setWindow: this.setWindow.bind(this),
|
|
63
|
-
getWindow: this.getWindow.bind(this)
|
|
79
|
+
getWindow: this.getWindow.bind(this),
|
|
80
|
+
[internal.LIVE_QUERY_INTERNAL]: {
|
|
81
|
+
getBuilder: () => this,
|
|
82
|
+
hasCustomGetKey: !!this.config.getKey,
|
|
83
|
+
hasJoins: this.hasJoins(this.query)
|
|
84
|
+
}
|
|
64
85
|
}
|
|
65
86
|
};
|
|
66
87
|
}
|
|
@@ -558,6 +579,17 @@ function extractCollectionsFromQuery(query) {
|
|
|
558
579
|
extractFromQuery(query);
|
|
559
580
|
return collections;
|
|
560
581
|
}
|
|
582
|
+
function extractCollectionFromSource(query) {
|
|
583
|
+
const from = query.from;
|
|
584
|
+
if (from.type === `collectionRef`) {
|
|
585
|
+
return from.collection;
|
|
586
|
+
} else if (from.type === `queryRef`) {
|
|
587
|
+
return extractCollectionFromSource(from.query);
|
|
588
|
+
}
|
|
589
|
+
throw new Error(
|
|
590
|
+
`Failed to extract collection. Invalid FROM clause: ${JSON.stringify(query)}`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
561
593
|
function extractCollectionAliases(query) {
|
|
562
594
|
const aliasesById = /* @__PURE__ */ new Map();
|
|
563
595
|
function recordAlias(source) {
|