@objy/server 0.0.0-stage → 0.1.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/API.md +1469 -0
- package/LICENSE +21 -0
- package/NOTICE.md +9 -0
- package/README.md +21 -2
- package/objydb.config.json +48 -0
- package/package.json +42 -4
- package/public/index.html +4134 -0
- package/public/objy-db-logo.png +0 -0
- package/src/aggregate.js +438 -0
- package/src/api.js +15 -0
- package/src/auth.js +472 -0
- package/src/clock.js +18 -0
- package/src/cluster-secret.js +25 -0
- package/src/cluster-status.js +562 -0
- package/src/compression-stats.js +32 -0
- package/src/config.js +360 -0
- package/src/crdt.js +1076 -0
- package/src/derived-state-scheduler.js +128 -0
- package/src/files.js +324 -0
- package/src/hnsw.js +269 -0
- package/src/http-cors.js +31 -0
- package/src/http-router.js +162 -0
- package/src/index.js +137 -0
- package/src/interfaces/auth.js +123 -0
- package/src/interfaces/cluster.js +296 -0
- package/src/interfaces/public-object.js +2452 -0
- package/src/interfaces/ui.js +38 -0
- package/src/lib/crypto.cjs +54 -0
- package/src/lib/jwt.cjs +231 -0
- package/src/local-wal.js +122 -0
- package/src/memory-guard.js +152 -0
- package/src/ndjson.js +117 -0
- package/src/node-runtime.js +29 -0
- package/src/object-hooks.js +282 -0
- package/src/object-id.js +7 -0
- package/src/object-storage.js +263 -0
- package/src/object-store.js +288 -0
- package/src/patch.js +87 -0
- package/src/permissions.js +178 -0
- package/src/persistence.js +1918 -0
- package/src/plugin-loader.js +21 -0
- package/src/query-planner.js +266 -0
- package/src/query.js +416 -0
- package/src/references.js +349 -0
- package/src/replication-scheduler.js +303 -0
- package/src/restore-status.js +77 -0
- package/src/runtime.js +47 -0
- package/src/server.js +242 -0
- package/src/shape.js +286 -0
- package/src/shard-map-store.js +85 -0
- package/src/shard-map.js +333 -0
- package/src/shard-router.js +737 -0
- package/src/store.js +3410 -0
- package/src/system-secrets.js +107 -0
- package/src/vector.js +83 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export async function loadPlugins(plugins, { configPath, ...context } = {}) {
|
|
5
|
+
if (!Array.isArray(plugins)) throw new TypeError("plugins must be an array");
|
|
6
|
+
const base = configPath ? dirname(configPath) : process.cwd();
|
|
7
|
+
|
|
8
|
+
for (const entry of plugins) {
|
|
9
|
+
const descriptor = typeof entry === "string" ? { file: entry } : entry;
|
|
10
|
+
if (!descriptor || typeof descriptor.file !== "string" || !descriptor.file.trim()) {
|
|
11
|
+
throw new TypeError("plugin must be a file path or { file, options }");
|
|
12
|
+
}
|
|
13
|
+
const file = isAbsolute(descriptor.file) ? descriptor.file : resolve(base, descriptor.file);
|
|
14
|
+
const module = await import(pathToFileURL(file).href);
|
|
15
|
+
const register = module.register || module.default;
|
|
16
|
+
if (typeof register !== "function") {
|
|
17
|
+
throw new TypeError(`plugin ${descriptor.file} must export register()`);
|
|
18
|
+
}
|
|
19
|
+
await register({ ...context, configPath, options: descriptor.options || {} });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { normalizeAggregateSpec } from "./aggregate.js";
|
|
2
|
+
|
|
3
|
+
export const READ_PATH = {
|
|
4
|
+
INDEX_EXACT: "index-exact",
|
|
5
|
+
INDEX_PARTIAL: "index-partial",
|
|
6
|
+
ORDERED_INDEX: "ordered-index",
|
|
7
|
+
SCAN: "scan",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const QUERY_PATH_HEADER = "x-objydb-query-path";
|
|
11
|
+
|
|
12
|
+
export const AGGREGATE_PATH = {
|
|
13
|
+
COLUMN_SUMMARY: "aggregate-column-summary",
|
|
14
|
+
COLUMN_SCAN: "aggregate-column-scan",
|
|
15
|
+
GROUP_FORWARD_INDEX: "aggregate-group-forward-index",
|
|
16
|
+
GROUP_INDEX: "aggregate-group-index",
|
|
17
|
+
GROUP_COLUMN_SCAN: "aggregate-group-column-scan",
|
|
18
|
+
SCAN: "aggregate-scan",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function publicReadPath(path) {
|
|
22
|
+
switch (path) {
|
|
23
|
+
case READ_PATH.INDEX_EXACT:
|
|
24
|
+
return "index-full";
|
|
25
|
+
case READ_PATH.INDEX_PARTIAL:
|
|
26
|
+
return "index-partial";
|
|
27
|
+
case READ_PATH.ORDERED_INDEX:
|
|
28
|
+
return "ordered-index";
|
|
29
|
+
case READ_PATH.SCAN:
|
|
30
|
+
return "scan";
|
|
31
|
+
default:
|
|
32
|
+
return path ? String(path) : null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function publicAggregatePath(path) {
|
|
37
|
+
return path ? String(path) : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function combineQueryPaths(paths = []) {
|
|
41
|
+
const out = [];
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
for (const path of paths) {
|
|
44
|
+
for (const part of String(path || "").split(",")) {
|
|
45
|
+
const value = part.trim();
|
|
46
|
+
if (!value || seen.has(value)) continue;
|
|
47
|
+
seen.add(value);
|
|
48
|
+
out.push(value);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out.join(",");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function planRead(criteria = {}, options = {}, adapter) {
|
|
55
|
+
const hasCriteria = hasObjectCriteria(criteria);
|
|
56
|
+
const idPlan = hasCriteria ? planIndexedIds(criteria, adapter) : allIdsPlan();
|
|
57
|
+
|
|
58
|
+
if (options.sortBy && adapter.hasOrderedIndex(options.sortBy)) {
|
|
59
|
+
return {
|
|
60
|
+
path: READ_PATH.ORDERED_INDEX,
|
|
61
|
+
idPlan,
|
|
62
|
+
sortBy: String(options.sortBy),
|
|
63
|
+
reason: "sort field has an ordered field index",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!hasCriteria) {
|
|
68
|
+
return {
|
|
69
|
+
path: READ_PATH.SCAN,
|
|
70
|
+
idPlan,
|
|
71
|
+
reason: "no criteria",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (idPlan?.exact) {
|
|
76
|
+
return {
|
|
77
|
+
path: READ_PATH.INDEX_EXACT,
|
|
78
|
+
idPlan,
|
|
79
|
+
reason: "all criteria are satisfied by field indexes",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (idPlan) {
|
|
84
|
+
return {
|
|
85
|
+
path: READ_PATH.INDEX_PARTIAL,
|
|
86
|
+
idPlan,
|
|
87
|
+
reason: "field indexes narrow part of the criteria",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
path: READ_PATH.SCAN,
|
|
93
|
+
idPlan: allIdsPlan(),
|
|
94
|
+
reason: "criteria are not indexable",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function planCount(criteria = {}, options = {}, adapter) {
|
|
99
|
+
const read = planRead(criteria, options, adapter);
|
|
100
|
+
if (read.path === READ_PATH.ORDERED_INDEX) {
|
|
101
|
+
if (!hasObjectCriteria(criteria)) return { ...read, path: READ_PATH.SCAN, idPlan: allIdsPlan() };
|
|
102
|
+
const idPlan = planIndexedIds(criteria, adapter);
|
|
103
|
+
return idPlan
|
|
104
|
+
? { ...read, path: idPlan.exact ? READ_PATH.INDEX_EXACT : READ_PATH.INDEX_PARTIAL, idPlan }
|
|
105
|
+
: { ...read, path: READ_PATH.SCAN, idPlan: allIdsPlan() };
|
|
106
|
+
}
|
|
107
|
+
return read;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function planAggregate(criteria = {}, spec = {}, options = {}, adapter) {
|
|
111
|
+
const normalized = normalizeAggregateSpec(spec);
|
|
112
|
+
const hasCriteria = hasObjectCriteria(criteria);
|
|
113
|
+
const includeId = typeof options.includeId === "function" ? options.includeId : null;
|
|
114
|
+
const includeIds = options.includeIds instanceof Set ? options.includeIds : null;
|
|
115
|
+
const idPlan = aggregateIdPlan(criteria, includeId, includeIds, adapter);
|
|
116
|
+
const metricColumns = readyColumns(metricColumnPaths(normalized), adapter);
|
|
117
|
+
|
|
118
|
+
if (!metricColumns.ready) {
|
|
119
|
+
return aggregateScanPlan(normalized, `missing or building column index for ${metricColumns.missing}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (normalized.groupBy.length === 0) {
|
|
123
|
+
if (!hasCriteria && !includeId && !includeIds && canUseColumnSummary(normalized)) {
|
|
124
|
+
return {
|
|
125
|
+
path: AGGREGATE_PATH.COLUMN_SUMMARY,
|
|
126
|
+
normalized,
|
|
127
|
+
columns: metricColumns.columns,
|
|
128
|
+
reason: "flat aggregate can be answered from column summaries",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!idPlan) return aggregateScanPlan(normalized, "criteria cannot be narrowed by indexes");
|
|
133
|
+
return {
|
|
134
|
+
path: AGGREGATE_PATH.COLUMN_SCAN,
|
|
135
|
+
normalized,
|
|
136
|
+
columns: metricColumns.columns,
|
|
137
|
+
idPlan,
|
|
138
|
+
reason: "flat aggregate can scan column values over planned ids",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!idPlan) return aggregateScanPlan(normalized, "criteria cannot be narrowed by indexes");
|
|
143
|
+
|
|
144
|
+
const scopeShards = Array.isArray(options.scopeShards) ? options.scopeShards : null;
|
|
145
|
+
if (normalized.groupBy.length === 1 && !isRowCountOnlyAggregate(normalized) && idPlan.exact && idPlan.ids instanceof Set &&
|
|
146
|
+
adapter.hasGroupForwardIndex?.(normalized.groupBy[0])) {
|
|
147
|
+
return {
|
|
148
|
+
path: AGGREGATE_PATH.GROUP_FORWARD_INDEX,
|
|
149
|
+
normalized,
|
|
150
|
+
columns: metricColumns.columns,
|
|
151
|
+
groupBy: normalized.groupBy[0],
|
|
152
|
+
groupIndex: adapter.fieldIndex(normalized.groupBy[0]),
|
|
153
|
+
idPlan,
|
|
154
|
+
reason: "groupBy field can be read from the field index forward map and metrics have column indexes",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (normalized.groupBy.length === 1 && adapter.hasGroupIndex(normalized.groupBy[0])) {
|
|
159
|
+
return {
|
|
160
|
+
path: AGGREGATE_PATH.GROUP_INDEX,
|
|
161
|
+
normalized,
|
|
162
|
+
columns: metricColumns.columns,
|
|
163
|
+
groupBy: normalized.groupBy[0],
|
|
164
|
+
groupIndex: adapter.fieldIndex(normalized.groupBy[0]),
|
|
165
|
+
idPlan,
|
|
166
|
+
reason: "groupBy field has a field index and metrics have column indexes",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const groupColumns = readyColumns(normalized.groupBy, adapter);
|
|
171
|
+
if (!groupColumns.ready) {
|
|
172
|
+
return aggregateScanPlan(normalized, `missing or building group column index for ${groupColumns.missing}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
path: AGGREGATE_PATH.GROUP_COLUMN_SCAN,
|
|
177
|
+
normalized,
|
|
178
|
+
columns: mergeColumnMaps(metricColumns.columns, groupColumns.columns),
|
|
179
|
+
idPlan,
|
|
180
|
+
reason: "group fields and metric fields have ready column indexes",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function planIndexedIds(criteria, adapter) {
|
|
185
|
+
const exact = adapter.idsForCriteria(criteria);
|
|
186
|
+
if (exact !== null) return { ids: exact, exact: true, verify: false };
|
|
187
|
+
|
|
188
|
+
const partial = adapter.partialIdsForCriteria(criteria);
|
|
189
|
+
if (partial !== null) return { ids: partial, exact: false, verify: true };
|
|
190
|
+
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function aggregateIdPlan(criteria, includeId, includeIds, adapter) {
|
|
195
|
+
if (!hasObjectCriteria(criteria)) {
|
|
196
|
+
if (includeIds && includeId) return { ids: adapter.filterIds(includeIds, includeId), exact: true, verify: false };
|
|
197
|
+
if (includeIds) return { ids: includeIds, exact: true, verify: false };
|
|
198
|
+
if (includeId) return { ids: adapter.idsForAll(includeId), exact: true, verify: false };
|
|
199
|
+
return allIdsPlan();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const indexed = planIndexedIds(criteria, adapter);
|
|
203
|
+
if (!indexed) return null;
|
|
204
|
+
if (includeIds) return { ...indexed, scopeIds: includeIds };
|
|
205
|
+
if (!includeId) return indexed;
|
|
206
|
+
return {
|
|
207
|
+
...indexed,
|
|
208
|
+
ids: adapter.filterIds(indexed.ids, includeId),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function allIdsPlan() {
|
|
213
|
+
return { ids: null, exact: true, verify: false };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function aggregateScanPlan(normalized, reason) {
|
|
217
|
+
return {
|
|
218
|
+
path: AGGREGATE_PATH.SCAN,
|
|
219
|
+
normalized,
|
|
220
|
+
reason,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function readyColumns(paths, adapter) {
|
|
225
|
+
const columns = new Map();
|
|
226
|
+
for (const path of unique(paths)) {
|
|
227
|
+
const column = adapter.column(path);
|
|
228
|
+
if (!column || !column.ready) return { ready: false, missing: path, columns };
|
|
229
|
+
columns.set(path, column);
|
|
230
|
+
}
|
|
231
|
+
return { ready: true, columns };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function metricColumnPaths(normalized) {
|
|
235
|
+
const paths = [];
|
|
236
|
+
for (const field of normalized.fields) {
|
|
237
|
+
if (field.operator === "$count" && field.operand === true) continue;
|
|
238
|
+
paths.push(field.operand);
|
|
239
|
+
}
|
|
240
|
+
return paths;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function canUseColumnSummary(normalized) {
|
|
244
|
+
return normalized.fields.every((field) => {
|
|
245
|
+
return field.operator === "$sum" || field.operator === "$avg" || field.operator === "$count";
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isRowCountOnlyAggregate(normalized) {
|
|
250
|
+
return normalized.fields.length > 0 &&
|
|
251
|
+
normalized.fields.every((field) => field.operator === "$count" && field.operand === true);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function mergeColumnMaps(left, right) {
|
|
255
|
+
const merged = new Map(left);
|
|
256
|
+
for (const [path, column] of right) merged.set(path, column);
|
|
257
|
+
return merged;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function hasObjectCriteria(criteria) {
|
|
261
|
+
return Boolean(criteria && typeof criteria === "object" && !Array.isArray(criteria) && Object.keys(criteria).length > 0);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function unique(values) {
|
|
265
|
+
return [...new Set(values.map((value) => String(value)).filter(Boolean))];
|
|
266
|
+
}
|
package/src/query.js
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
const rhsOperators = new Set([
|
|
2
|
+
"$cb",
|
|
3
|
+
"$deepEquals",
|
|
4
|
+
"$eq",
|
|
5
|
+
"$equal",
|
|
6
|
+
"$ne",
|
|
7
|
+
"$not",
|
|
8
|
+
"$nor",
|
|
9
|
+
"$and",
|
|
10
|
+
"$or",
|
|
11
|
+
"$any",
|
|
12
|
+
"$all",
|
|
13
|
+
"$null",
|
|
14
|
+
"$in",
|
|
15
|
+
"$likeI",
|
|
16
|
+
"$like",
|
|
17
|
+
"$startsWith",
|
|
18
|
+
"$endsWith",
|
|
19
|
+
"$elemMatch",
|
|
20
|
+
"$contains",
|
|
21
|
+
"$nin",
|
|
22
|
+
"$regex",
|
|
23
|
+
"$gte",
|
|
24
|
+
"$gt",
|
|
25
|
+
"$lt",
|
|
26
|
+
"$lte",
|
|
27
|
+
"$before",
|
|
28
|
+
"$after",
|
|
29
|
+
"$type",
|
|
30
|
+
"$size",
|
|
31
|
+
"$mod",
|
|
32
|
+
"$between",
|
|
33
|
+
"$exists",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const queryControls = new Set([
|
|
37
|
+
"$aggregate",
|
|
38
|
+
"$cursor",
|
|
39
|
+
"$limit",
|
|
40
|
+
"$offset",
|
|
41
|
+
"$page",
|
|
42
|
+
"$pageSize",
|
|
43
|
+
"$refDepth",
|
|
44
|
+
"$skip",
|
|
45
|
+
"$sort",
|
|
46
|
+
"$vector",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
export function getPathValue(source, path) {
|
|
50
|
+
if (source === null || source === undefined) return undefined;
|
|
51
|
+
const key = String(path);
|
|
52
|
+
if (key === "_id") return source._id;
|
|
53
|
+
if (key === "id") return source.id;
|
|
54
|
+
|
|
55
|
+
return key.split(".").reduce((cursor, part) => {
|
|
56
|
+
if (cursor === null || cursor === undefined || typeof cursor !== "object") return undefined;
|
|
57
|
+
return cursor[part];
|
|
58
|
+
}, source);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function stableKey(value) {
|
|
62
|
+
return JSON.stringify(sortForKey(value));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function matchesQuery(object, query = {}) {
|
|
66
|
+
const criteria = stripQueryControls(query);
|
|
67
|
+
if (!isPlainObject(criteria)) return false;
|
|
68
|
+
|
|
69
|
+
for (const [path, expected] of Object.entries(criteria)) {
|
|
70
|
+
if (path === "$and") {
|
|
71
|
+
if (!logicalConstraints(expected).every((constraint) => matchesQuery(object, constraint))) return false;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (path === "$or") {
|
|
75
|
+
if (!logicalConstraints(expected).some((constraint) => matchesQuery(object, constraint))) return false;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (path === "$nor") {
|
|
79
|
+
if (logicalConstraints(expected).some((constraint) => matchesQuery(object, constraint))) return false;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (path === "$not") {
|
|
83
|
+
if (matchesQuery(object, expected)) return false;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (path === "$count") {
|
|
87
|
+
if (!matchesCount(object, expected)) return false;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (path === "$where") {
|
|
91
|
+
if (!matchesWhere(object, expected)) return false;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const actual = getPathValue(object, path);
|
|
96
|
+
if (!matchesConstraint(actual, expected, path)) return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function stripQueryControls(query = {}) {
|
|
103
|
+
if (!isPlainObject(query)) return query;
|
|
104
|
+
const out = {};
|
|
105
|
+
for (const [key, value] of Object.entries(query)) {
|
|
106
|
+
if (!queryControls.has(key)) out[key] = value;
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function splitQueryControls(query = {}) {
|
|
112
|
+
if (!isPlainObject(query)) {
|
|
113
|
+
return {
|
|
114
|
+
criteria: {},
|
|
115
|
+
controls: {},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const criteria = {};
|
|
119
|
+
const controls = {};
|
|
120
|
+
for (const [key, value] of Object.entries(query)) {
|
|
121
|
+
if (queryControls.has(key)) controls[key] = value;
|
|
122
|
+
else criteria[key] = value;
|
|
123
|
+
}
|
|
124
|
+
return { criteria, controls };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function parseQuerySort(value) {
|
|
128
|
+
if (typeof value === "string" && value.length > 0) {
|
|
129
|
+
return value.startsWith("-")
|
|
130
|
+
? { sortBy: value.slice(1), sort: "desc" }
|
|
131
|
+
: { sortBy: value.replace(/^\+/, ""), sort: "asc" };
|
|
132
|
+
}
|
|
133
|
+
if (isPlainObject(value)) {
|
|
134
|
+
const [field, direction] = Object.entries(value)[0] || [];
|
|
135
|
+
if (!field) return {};
|
|
136
|
+
return { sortBy: field, sort: direction === -1 || direction === "desc" ? "desc" : "asc" };
|
|
137
|
+
}
|
|
138
|
+
return {};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function queryInteger(value) {
|
|
142
|
+
if (value === null || value === undefined || value === "") return undefined;
|
|
143
|
+
const number = Number(value);
|
|
144
|
+
return Number.isFinite(number) && number >= 0 ? Math.floor(number) : undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function firstQueryInteger(...values) {
|
|
148
|
+
for (const value of values) {
|
|
149
|
+
const parsed = queryInteger(value);
|
|
150
|
+
if (parsed !== undefined) return parsed;
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function queryPageOffset(page, pageSize) {
|
|
156
|
+
const parsedPage = queryInteger(page);
|
|
157
|
+
return parsedPage === undefined || pageSize === undefined
|
|
158
|
+
? undefined
|
|
159
|
+
: Math.max(0, parsedPage - 1) * pageSize;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function compareSortValues(left, right) {
|
|
163
|
+
if (Object.is(left, right)) return 0;
|
|
164
|
+
if (left === undefined || left === null) return 1;
|
|
165
|
+
if (right === undefined || right === null) return -1;
|
|
166
|
+
if (typeof left === "number" && typeof right === "number") return left - right;
|
|
167
|
+
return String(left).localeCompare(String(right));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function matchesConstraint(actual, expected, parentKey) {
|
|
171
|
+
if (expected === actual) return true;
|
|
172
|
+
if (expected instanceof RegExp) return regexMatches(actual, expected);
|
|
173
|
+
if (Array.isArray(expected)) return inOperator(actual, expected);
|
|
174
|
+
|
|
175
|
+
if (expected && typeof expected === "object") {
|
|
176
|
+
if (expected instanceof Date) return equal(actual, expected.getTime());
|
|
177
|
+
if (expected.$regex !== undefined) return regexMatches(actual, regexFromLegacySpec(expected));
|
|
178
|
+
|
|
179
|
+
for (const [operator, operand] of Object.entries(expected)) {
|
|
180
|
+
if (!rhsOperators.has(operator)) return equal(actual, expected);
|
|
181
|
+
if (!matchesOperator(operator, actual, operand, parentKey)) return false;
|
|
182
|
+
}
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (Array.isArray(actual)) return actual.some((entry) => equal(entry, expected));
|
|
187
|
+
if (isNullishConstraint(expected)) return nullOperator(actual);
|
|
188
|
+
return equal(actual, expected);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function matchesOperator(operator, actual, operand, parentKey) {
|
|
192
|
+
switch (operator) {
|
|
193
|
+
case "$cb":
|
|
194
|
+
return typeof operand === "function" && Boolean(operand(actual, parentKey));
|
|
195
|
+
case "$deepEquals":
|
|
196
|
+
return stableKey(actual) === stableKey(operand);
|
|
197
|
+
case "$eq":
|
|
198
|
+
case "$equal":
|
|
199
|
+
return equal(actual, operand);
|
|
200
|
+
case "$ne":
|
|
201
|
+
return !matchesConstraint(actual, operand, parentKey);
|
|
202
|
+
case "$not":
|
|
203
|
+
return !matchesConstraint(actual, operand, parentKey);
|
|
204
|
+
case "$nor":
|
|
205
|
+
return !orOperator(actual, operand, parentKey);
|
|
206
|
+
case "$and":
|
|
207
|
+
case "$all":
|
|
208
|
+
return andOperator(actual, operand, parentKey);
|
|
209
|
+
case "$or":
|
|
210
|
+
case "$any":
|
|
211
|
+
return orOperator(actual, operand, parentKey);
|
|
212
|
+
case "$null":
|
|
213
|
+
return Boolean(operand) ? nullOperator(actual) : !nullOperator(actual);
|
|
214
|
+
case "$in":
|
|
215
|
+
return inOperator(actual, operand);
|
|
216
|
+
case "$likeI":
|
|
217
|
+
return likeOperator(actual, operand, { ignoreCase: true });
|
|
218
|
+
case "$like":
|
|
219
|
+
return likeOperator(actual, operand);
|
|
220
|
+
case "$startsWith":
|
|
221
|
+
return stringList(actual).some((value) => value.startsWith(String(operand)));
|
|
222
|
+
case "$endsWith":
|
|
223
|
+
return stringList(actual).some((value) => value.endsWith(String(operand)));
|
|
224
|
+
case "$elemMatch":
|
|
225
|
+
return Array.isArray(actual) && actual.some((entry) => matchesQuery(entry, operand));
|
|
226
|
+
case "$contains":
|
|
227
|
+
return contains(actual, operand);
|
|
228
|
+
case "$nin":
|
|
229
|
+
return !inOperator(actual, operand);
|
|
230
|
+
case "$regex":
|
|
231
|
+
return regexMatches(actual, regexFromOperand(operand));
|
|
232
|
+
case "$gte":
|
|
233
|
+
return !nullOperator(actual) && actual >= resolveOperand(operand);
|
|
234
|
+
case "$gt":
|
|
235
|
+
return !nullOperator(actual) && actual > resolveOperand(operand);
|
|
236
|
+
case "$lt":
|
|
237
|
+
return !nullOperator(actual) && actual < resolveOperand(operand);
|
|
238
|
+
case "$lte":
|
|
239
|
+
return !nullOperator(actual) && actual <= resolveOperand(operand);
|
|
240
|
+
case "$before":
|
|
241
|
+
return beforeAfter(actual, operand, "$before");
|
|
242
|
+
case "$after":
|
|
243
|
+
return beforeAfter(actual, operand, "$after");
|
|
244
|
+
case "$type":
|
|
245
|
+
return typeOf(actual) === operand;
|
|
246
|
+
case "$size":
|
|
247
|
+
return sizeOf(actual) === Number(operand);
|
|
248
|
+
case "$mod":
|
|
249
|
+
return Array.isArray(operand) && operand.length >= 2 && actual % operand[0] == operand[1];
|
|
250
|
+
case "$between":
|
|
251
|
+
return Array.isArray(operand) &&
|
|
252
|
+
operand.length >= 2 &&
|
|
253
|
+
matchesConstraint(actual, { $gt: operand[0], $lt: operand[1] }, parentKey);
|
|
254
|
+
case "$exists":
|
|
255
|
+
return existsOperator(actual, operand);
|
|
256
|
+
default:
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function matchesCount(object, condition) {
|
|
262
|
+
if (!isPlainObject(condition) || !Array.isArray(condition.$constraints)) return false;
|
|
263
|
+
const count = condition.$constraints.filter((constraint) => matchesQuery(object, constraint)).length;
|
|
264
|
+
return matchesConstraint(count, condition.$constraint);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function matchesWhere(object, condition) {
|
|
268
|
+
if (typeof condition !== "function") return false;
|
|
269
|
+
return Boolean(condition.call(object, object));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function logicalConstraints(value) {
|
|
273
|
+
if (Array.isArray(value)) return value;
|
|
274
|
+
if (!isPlainObject(value)) return [];
|
|
275
|
+
return Object.entries(value).map(([key, constraint]) => ({ [key]: constraint }));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function andOperator(actual, constraints, parentKey) {
|
|
279
|
+
if (!Array.isArray(constraints)) return false;
|
|
280
|
+
return constraints.every((constraint) => matchesConstraint(actual, constraint, parentKey));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function orOperator(actual, constraints, parentKey) {
|
|
284
|
+
if (!Array.isArray(constraints)) return false;
|
|
285
|
+
const values = Array.isArray(actual) ? actual : [actual];
|
|
286
|
+
return values.some((value) => constraints.some((constraint) => matchesConstraint(value, constraint, parentKey)));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function inOperator(actual, expected) {
|
|
290
|
+
if (!Array.isArray(expected)) return false;
|
|
291
|
+
const values = Array.isArray(actual) ? actual : [actual];
|
|
292
|
+
return values.some((value) => expected.some((constraint) => matchesConstraint(value, constraint)));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function contains(actual, expected) {
|
|
296
|
+
if (typeof actual === "string") return actual.includes(String(expected));
|
|
297
|
+
if (Array.isArray(actual)) return actual.some((entry) => equal(entry, expected));
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function regexMatches(actual, regex) {
|
|
302
|
+
if (!(regex instanceof RegExp)) return false;
|
|
303
|
+
const values = Array.isArray(actual) ? actual : [actual];
|
|
304
|
+
return values.some((value) => {
|
|
305
|
+
regex.lastIndex = 0;
|
|
306
|
+
return regex.test(value);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function regexFromLegacySpec(spec) {
|
|
311
|
+
if (spec.$regex && typeof spec.$regex === "object" && typeof spec.$regex.pattern === "string") {
|
|
312
|
+
return new RegExp(spec.$regex.pattern, spec.$regex.flags || spec.$options || "");
|
|
313
|
+
}
|
|
314
|
+
return new RegExp(spec.$regex, spec.$options || "");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function regexFromOperand(operand) {
|
|
318
|
+
if (operand instanceof RegExp) return operand;
|
|
319
|
+
if (typeof operand === "string") return new RegExp(operand);
|
|
320
|
+
if (operand && typeof operand === "object" && typeof operand.pattern === "string") {
|
|
321
|
+
return new RegExp(operand.pattern, operand.flags || "");
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function likeOperator(actual, expected, { ignoreCase = false } = {}) {
|
|
327
|
+
const needle = ignoreCase ? String(expected).toLowerCase() : String(expected);
|
|
328
|
+
return stringList(actual).some((value) => {
|
|
329
|
+
const haystack = ignoreCase ? value.toLowerCase() : value;
|
|
330
|
+
return haystack.includes(needle);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function stringList(value) {
|
|
335
|
+
const values = Array.isArray(value) ? value : [value];
|
|
336
|
+
return values.filter((entry) => typeof entry === "string");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function beforeAfter(actual, operand, operator) {
|
|
340
|
+
const left = dateComparable(actual);
|
|
341
|
+
const right = dateComparable(resolveOperand(operand));
|
|
342
|
+
if (!Number.isFinite(left) || !Number.isFinite(right)) return false;
|
|
343
|
+
return operator === "$before" ? left <= right : left >= right;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function dateComparable(value) {
|
|
347
|
+
if (typeof value === "string") return Date.parse(value);
|
|
348
|
+
if (value instanceof Date) return value.getTime();
|
|
349
|
+
return value;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function resolveOperand(value) {
|
|
353
|
+
if (value && typeof value === "object" && value.$date !== undefined) return Date.parse(value.$date);
|
|
354
|
+
return value;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function typeOf(value) {
|
|
358
|
+
if (Array.isArray(value)) return "array";
|
|
359
|
+
return typeof value;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function sizeOf(value) {
|
|
363
|
+
if (value === null || value === undefined) return NaN;
|
|
364
|
+
if (typeof value === "string" || Array.isArray(value)) return value.length;
|
|
365
|
+
if (typeof value === "object") return Object.keys(value).length;
|
|
366
|
+
return NaN;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function existsOperator(value, expected) {
|
|
370
|
+
return (value !== undefined && value !== null) === Boolean(expected);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function nullOperator(value) {
|
|
374
|
+
if (value === "" || value === null || value === undefined) return true;
|
|
375
|
+
if (Array.isArray(value)) return value.every((entry) => nullOperator(entry));
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function isNullishConstraint(value) {
|
|
380
|
+
return value === "" || value === null || value === undefined;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function equal(left, right) {
|
|
384
|
+
if (Object.is(left, right)) return true;
|
|
385
|
+
if (Array.isArray(left)) return left.some((entry) => equal(entry, right));
|
|
386
|
+
if (isNullishConstraint(right)) return nullOperator(left);
|
|
387
|
+
if (isNullishConstraint(left)) return false;
|
|
388
|
+
|
|
389
|
+
if (left instanceof Date) {
|
|
390
|
+
if (right instanceof Date) return left.getTime() == right.getTime();
|
|
391
|
+
if (typeof right === "number") return left.getTime() == right;
|
|
392
|
+
if (typeof right === "string") return left.getTime() == Date.parse(right);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if ((left && typeof left === "object") || (right && typeof right === "object")) {
|
|
396
|
+
return stableKey(left) === stableKey(right);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return left == right;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function sortForKey(value) {
|
|
403
|
+
if (Array.isArray(value)) return value.map(sortForKey);
|
|
404
|
+
if (value && typeof value === "object") {
|
|
405
|
+
return Object.fromEntries(
|
|
406
|
+
Object.entries(value)
|
|
407
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
408
|
+
.map(([key, entry]) => [key, sortForKey(entry)]),
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
return value;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function isPlainObject(value) {
|
|
415
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
416
|
+
}
|