@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.
Files changed (55) hide show
  1. package/API.md +1469 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE.md +9 -0
  4. package/README.md +21 -2
  5. package/objydb.config.json +48 -0
  6. package/package.json +42 -4
  7. package/public/index.html +4134 -0
  8. package/public/objy-db-logo.png +0 -0
  9. package/src/aggregate.js +438 -0
  10. package/src/api.js +15 -0
  11. package/src/auth.js +472 -0
  12. package/src/clock.js +18 -0
  13. package/src/cluster-secret.js +25 -0
  14. package/src/cluster-status.js +562 -0
  15. package/src/compression-stats.js +32 -0
  16. package/src/config.js +360 -0
  17. package/src/crdt.js +1076 -0
  18. package/src/derived-state-scheduler.js +128 -0
  19. package/src/files.js +324 -0
  20. package/src/hnsw.js +269 -0
  21. package/src/http-cors.js +31 -0
  22. package/src/http-router.js +162 -0
  23. package/src/index.js +137 -0
  24. package/src/interfaces/auth.js +123 -0
  25. package/src/interfaces/cluster.js +296 -0
  26. package/src/interfaces/public-object.js +2452 -0
  27. package/src/interfaces/ui.js +38 -0
  28. package/src/lib/crypto.cjs +54 -0
  29. package/src/lib/jwt.cjs +231 -0
  30. package/src/local-wal.js +122 -0
  31. package/src/memory-guard.js +152 -0
  32. package/src/ndjson.js +117 -0
  33. package/src/node-runtime.js +29 -0
  34. package/src/object-hooks.js +282 -0
  35. package/src/object-id.js +7 -0
  36. package/src/object-storage.js +263 -0
  37. package/src/object-store.js +288 -0
  38. package/src/patch.js +87 -0
  39. package/src/permissions.js +178 -0
  40. package/src/persistence.js +1918 -0
  41. package/src/plugin-loader.js +21 -0
  42. package/src/query-planner.js +266 -0
  43. package/src/query.js +416 -0
  44. package/src/references.js +349 -0
  45. package/src/replication-scheduler.js +303 -0
  46. package/src/restore-status.js +77 -0
  47. package/src/runtime.js +47 -0
  48. package/src/server.js +242 -0
  49. package/src/shape.js +286 -0
  50. package/src/shard-map-store.js +85 -0
  51. package/src/shard-map.js +333 -0
  52. package/src/shard-router.js +737 -0
  53. package/src/store.js +3410 -0
  54. package/src/system-secrets.js +107 -0
  55. package/src/vector.js +83 -0
Binary file
@@ -0,0 +1,438 @@
1
+ import { compareSortValues, getPathValue, stableKey } from "./query.js";
2
+
3
+ const aggregateOperators = new Set(["$sum", "$min", "$max", "$avg", "$count"]);
4
+
5
+ export function isObjectDbAggregateSpec(spec) {
6
+ return Boolean(spec && typeof spec === "object" && !Array.isArray(spec) && spec.fields !== undefined);
7
+ }
8
+
9
+ export function aggregateObjects(rows, spec) {
10
+ return mergeAggregatePartials(aggregatePartialObjects(rows, spec), spec);
11
+ }
12
+
13
+ export function aggregateRows(rows, spec = {}) {
14
+ return isObjectDbAggregateSpec(spec) ? aggregateObjects(rows, spec) : aggregateLegacyObjects(rows, spec);
15
+ }
16
+
17
+ export function aggregateLegacyObjects(rows, spec = {}) {
18
+ const groupBy = spec.groupBy;
19
+ const metrics = normalizeLegacyMetrics(spec);
20
+ const groups = new Map();
21
+
22
+ for (const object of rows || []) {
23
+ const groupValue = groupBy ? getPathValue(object, groupBy) : undefined;
24
+ const key = groupBy ? stableKey(groupValue) : "__all__";
25
+ let bucket = groups.get(key);
26
+ if (!bucket) {
27
+ bucket = makeLegacyBucket(groupValue, metrics);
28
+ groups.set(key, bucket);
29
+ }
30
+ stepLegacyBucket(bucket, object, metrics);
31
+ }
32
+
33
+ if (!groupBy) return finishLegacyBucket(groups.get("__all__") || makeLegacyBucket(undefined, metrics), metrics);
34
+ return [...groups.values()].map((bucket) => finishLegacyBucket(bucket, metrics));
35
+ }
36
+
37
+ export function aggregatePartialObjects(rows, spec) {
38
+ const normalized = normalizeAggregateSpec(spec);
39
+ const buckets = new Map();
40
+ let rowsSeen = 0;
41
+
42
+ for (const row of rows || []) {
43
+ rowsSeen++;
44
+ const groupValues = {};
45
+ const groupKeyValues = [];
46
+ for (const path of normalized.groupBy) {
47
+ const value = getPathValue(row, path);
48
+ const stored = value === undefined ? null : value;
49
+ groupValues[path] = cloneResultValue(stored);
50
+ groupKeyValues.push(stored);
51
+ }
52
+
53
+ const key = stableKey(groupKeyValues);
54
+ let bucket = buckets.get(key);
55
+ if (!bucket) {
56
+ bucket = {
57
+ groupValues,
58
+ fields: makeFieldState(normalized.fields),
59
+ };
60
+ buckets.set(key, bucket);
61
+ }
62
+
63
+ for (const field of normalized.fields) stepField(bucket.fields[field.resultKey], field, row);
64
+ }
65
+
66
+ if (rowsSeen === 0) return [];
67
+
68
+ return [...buckets.values()].map((bucket) => {
69
+ const out = cloneGroupValues(bucket.groupValues);
70
+ for (const field of normalized.fields) writePartialField(out, bucket.fields[field.resultKey], field);
71
+ return out;
72
+ });
73
+ }
74
+
75
+ export function mergeAggregatePartials(rows, spec) {
76
+ const normalized = normalizeAggregateSpec(spec);
77
+ const buckets = aggregatePartialBuckets(rows, normalized);
78
+ if (buckets === null) return [];
79
+
80
+ let result = [...buckets.values()].map((bucket) => {
81
+ const out = cloneGroupValues(bucket.groupValues);
82
+ for (const field of normalized.fields) out[field.resultKey] = finishField(bucket.fields[field.resultKey], field);
83
+ return out;
84
+ });
85
+
86
+ if (normalized.sort.length) result = result.sort(makeComparator(normalized.sort));
87
+
88
+ const start = (normalized.page - 1) * normalized.pageSize;
89
+ return result.slice(start, start + normalized.pageSize);
90
+ }
91
+
92
+ function aggregatePartialBuckets(rows, normalized) {
93
+ const buckets = new Map();
94
+ let rowsSeen = 0;
95
+
96
+ for (const row of rows || []) {
97
+ rowsSeen++;
98
+ const groupValues = {};
99
+ const groupKeyValues = [];
100
+ for (const path of normalized.groupBy) {
101
+ const value = row?.[path];
102
+ const stored = value === undefined ? null : value;
103
+ groupValues[path] = cloneResultValue(stored);
104
+ groupKeyValues.push(stored);
105
+ }
106
+
107
+ const key = stableKey(groupKeyValues);
108
+ let bucket = buckets.get(key);
109
+ if (!bucket) {
110
+ bucket = {
111
+ groupValues,
112
+ fields: makeFieldState(normalized.fields),
113
+ };
114
+ buckets.set(key, bucket);
115
+ }
116
+
117
+ for (const field of normalized.fields) mergePartialField(bucket.fields[field.resultKey], field, row);
118
+ }
119
+
120
+ return rowsSeen === 0 ? null : buckets;
121
+ }
122
+
123
+ export function normalizeAggregateSpec(spec) {
124
+ if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
125
+ throw new TypeError("$aggregate must be a plain object");
126
+ }
127
+
128
+ return {
129
+ groupBy: normalizeGroupBy(spec.groupBy),
130
+ fields: normalizeFields(spec.fields),
131
+ sort: normalizeSort(spec.$sort),
132
+ page: normalizePositiveInteger(spec.$page, "$aggregate.$page", 1),
133
+ pageSize: normalizePositiveInteger(spec.$pageSize, "$aggregate.$pageSize", 20),
134
+ };
135
+ }
136
+
137
+ function normalizeGroupBy(groupBy) {
138
+ if (groupBy === undefined || groupBy === null) return [];
139
+ if (typeof groupBy === "string" && groupBy.length > 0) return [groupBy];
140
+ if (Array.isArray(groupBy)) {
141
+ return groupBy.map((path) => {
142
+ if (typeof path !== "string" || path.length === 0) {
143
+ throw new TypeError("$aggregate.groupBy entries must be non-empty strings");
144
+ }
145
+ return path;
146
+ });
147
+ }
148
+ throw new TypeError("$aggregate.groupBy must be a string or an array of strings");
149
+ }
150
+
151
+ function normalizeFields(fields) {
152
+ if (!fields || typeof fields !== "object" || Array.isArray(fields)) {
153
+ throw new TypeError("$aggregate.fields must be a plain object");
154
+ }
155
+
156
+ return Object.entries(fields).map(([resultKey, definition]) => {
157
+ if (!definition || typeof definition !== "object" || Array.isArray(definition)) {
158
+ throw new TypeError(`$aggregate.fields.${resultKey} must be an operator object`);
159
+ }
160
+ const entries = Object.entries(definition);
161
+ if (entries.length !== 1) {
162
+ throw new TypeError(`$aggregate.fields.${resultKey} must define exactly one operator`);
163
+ }
164
+
165
+ const [[operator, operand]] = entries;
166
+ if (!aggregateOperators.has(operator)) {
167
+ throw new TypeError(`$aggregate.fields.${resultKey} has unknown operator ${operator}`);
168
+ }
169
+ if (operator === "$count") {
170
+ if (operand !== true && (typeof operand !== "string" || operand.length === 0)) {
171
+ throw new TypeError(`$aggregate.fields.${resultKey}.$count must be true or a field path`);
172
+ }
173
+ } else if (typeof operand !== "string" || operand.length === 0) {
174
+ throw new TypeError(`$aggregate.fields.${resultKey}.${operator} must be a field path`);
175
+ }
176
+
177
+ return { resultKey, operator, operand };
178
+ });
179
+ }
180
+
181
+ function normalizeSort(sort) {
182
+ if (sort === undefined || sort === null) return [];
183
+ if (typeof sort !== "object" || Array.isArray(sort)) {
184
+ throw new TypeError("$aggregate.$sort must be a plain object");
185
+ }
186
+ return Object.entries(sort).map(([path, direction]) => {
187
+ if (direction !== 1 && direction !== -1) {
188
+ throw new TypeError(`$aggregate.$sort.${path} must be 1 or -1`);
189
+ }
190
+ return { path, direction };
191
+ });
192
+ }
193
+
194
+ function normalizePositiveInteger(value, name, fallback) {
195
+ if (value === undefined || value === null) return fallback;
196
+ const parsed = Number(value);
197
+ if (!Number.isInteger(parsed) || parsed < 1) throw new TypeError(`${name} must be an integer >= 1`);
198
+ return parsed;
199
+ }
200
+
201
+ function makeFieldState(fields) {
202
+ const state = {};
203
+ for (const field of fields) {
204
+ switch (field.operator) {
205
+ case "$sum":
206
+ state[field.resultKey] = { sum: 0 };
207
+ break;
208
+ case "$avg":
209
+ state[field.resultKey] = { sum: 0, count: 0 };
210
+ break;
211
+ case "$min":
212
+ case "$max":
213
+ state[field.resultKey] = { value: undefined };
214
+ break;
215
+ case "$count":
216
+ state[field.resultKey] = { count: 0 };
217
+ break;
218
+ }
219
+ }
220
+ return state;
221
+ }
222
+
223
+ function stepField(state, field, row) {
224
+ switch (field.operator) {
225
+ case "$sum": {
226
+ const value = getPathValue(row, field.operand);
227
+ if (typeof value === "number" && Number.isFinite(value)) state.sum += value;
228
+ return;
229
+ }
230
+ case "$avg": {
231
+ const value = getPathValue(row, field.operand);
232
+ if (typeof value === "number" && Number.isFinite(value)) {
233
+ state.sum += value;
234
+ state.count++;
235
+ }
236
+ return;
237
+ }
238
+ case "$min": {
239
+ const value = getPathValue(row, field.operand);
240
+ if (!isComparable(value)) return;
241
+ if (state.value === undefined || sameTypeLessThan(value, state.value)) state.value = cloneResultValue(value);
242
+ return;
243
+ }
244
+ case "$max": {
245
+ const value = getPathValue(row, field.operand);
246
+ if (!isComparable(value)) return;
247
+ if (state.value === undefined || sameTypeLessThan(state.value, value)) state.value = cloneResultValue(value);
248
+ return;
249
+ }
250
+ case "$count": {
251
+ if (field.operand === true) {
252
+ state.count++;
253
+ return;
254
+ }
255
+ const value = getPathValue(row, field.operand);
256
+ if (value !== undefined && value !== null) state.count++;
257
+ return;
258
+ }
259
+ }
260
+ }
261
+
262
+ function finishField(state, field) {
263
+ switch (field.operator) {
264
+ case "$sum":
265
+ return state.sum;
266
+ case "$avg":
267
+ return state.count ? state.sum / state.count : null;
268
+ case "$min":
269
+ case "$max":
270
+ return state.value === undefined ? null : cloneResultValue(state.value);
271
+ case "$count":
272
+ return state.count;
273
+ default:
274
+ return null;
275
+ }
276
+ }
277
+
278
+ function writePartialField(out, state, field) {
279
+ switch (field.operator) {
280
+ case "$sum":
281
+ out[field.resultKey] = state.sum;
282
+ return;
283
+ case "$avg":
284
+ out[avgSumKey(field.resultKey)] = state.sum;
285
+ out[avgCountKey(field.resultKey)] = state.count;
286
+ return;
287
+ case "$min":
288
+ case "$max":
289
+ out[field.resultKey] = state.value === undefined ? null : cloneResultValue(state.value);
290
+ return;
291
+ case "$count":
292
+ out[field.resultKey] = state.count;
293
+ }
294
+ }
295
+
296
+ function mergePartialField(state, field, row) {
297
+ switch (field.operator) {
298
+ case "$sum": {
299
+ const value = row?.[field.resultKey];
300
+ if (typeof value === "number" && Number.isFinite(value)) state.sum += value;
301
+ return;
302
+ }
303
+ case "$avg": {
304
+ const sum = row?.[avgSumKey(field.resultKey)];
305
+ const count = row?.[avgCountKey(field.resultKey)];
306
+ if (typeof sum === "number" && Number.isFinite(sum)) state.sum += sum;
307
+ if (typeof count === "number" && Number.isFinite(count)) state.count += count;
308
+ return;
309
+ }
310
+ case "$min": {
311
+ const value = row?.[field.resultKey];
312
+ if (!isComparable(value)) return;
313
+ if (state.value === undefined || sameTypeLessThan(value, state.value)) state.value = cloneResultValue(value);
314
+ return;
315
+ }
316
+ case "$max": {
317
+ const value = row?.[field.resultKey];
318
+ if (!isComparable(value)) return;
319
+ if (state.value === undefined || sameTypeLessThan(state.value, value)) state.value = cloneResultValue(value);
320
+ return;
321
+ }
322
+ case "$count": {
323
+ const value = row?.[field.resultKey];
324
+ if (typeof value === "number" && Number.isFinite(value)) state.count += value;
325
+ }
326
+ }
327
+ }
328
+
329
+ function avgSumKey(resultKey) {
330
+ return `$partial.${resultKey}.sum`;
331
+ }
332
+
333
+ function avgCountKey(resultKey) {
334
+ return `$partial.${resultKey}.count`;
335
+ }
336
+
337
+ function cloneGroupValues(values) {
338
+ return Object.fromEntries(Object.entries(values).map(([key, value]) => [key, cloneResultValue(value)]));
339
+ }
340
+
341
+ function cloneResultValue(value) {
342
+ if (value === null || typeof value !== "object") return value;
343
+ return structuredClone(value);
344
+ }
345
+
346
+ function isComparable(value) {
347
+ if (value === null || value === undefined) return false;
348
+ if (typeof value === "number") return Number.isFinite(value);
349
+ return typeof value === "string" || typeof value === "boolean";
350
+ }
351
+
352
+ function sameTypeLessThan(left, right) {
353
+ if (typeof left !== typeof right) return false;
354
+ return compareSortValues(left, right) < 0;
355
+ }
356
+
357
+ function makeComparator(sort) {
358
+ return (left, right) => {
359
+ for (const { path, direction } of sort) {
360
+ const leftValue = left[path];
361
+ const rightValue = right[path];
362
+ const leftMissing = leftValue === null || leftValue === undefined;
363
+ const rightMissing = rightValue === null || rightValue === undefined;
364
+ if (leftMissing && rightMissing) continue;
365
+ if (leftMissing) return 1;
366
+ if (rightMissing) return -1;
367
+ const compared = compareSortValues(leftValue, rightValue);
368
+ if (compared !== 0) return compared * direction;
369
+ }
370
+ return 0;
371
+ };
372
+ }
373
+
374
+ function normalizeLegacyMetrics(spec = {}) {
375
+ return {
376
+ sum: asArray(spec.sum),
377
+ avg: asArray(spec.avg),
378
+ min: asArray(spec.min),
379
+ max: asArray(spec.max),
380
+ };
381
+ }
382
+
383
+ function makeLegacyBucket(key, metrics) {
384
+ return {
385
+ key: cloneResultValue(key),
386
+ count: 0,
387
+ sum: Object.fromEntries(metrics.sum.map((field) => [field, 0])),
388
+ avg: Object.fromEntries(metrics.avg.map((field) => [field, { sum: 0, count: 0 }])),
389
+ min: Object.fromEntries(metrics.min.map((field) => [field, undefined])),
390
+ max: Object.fromEntries(metrics.max.map((field) => [field, undefined])),
391
+ };
392
+ }
393
+
394
+ function stepLegacyBucket(bucket, object, metrics) {
395
+ bucket.count++;
396
+ for (const field of metrics.sum) {
397
+ const value = getPathValue(object, field);
398
+ if (typeof value === "number") bucket.sum[field] += value;
399
+ }
400
+ for (const field of metrics.avg) {
401
+ const value = getPathValue(object, field);
402
+ if (typeof value === "number") {
403
+ bucket.avg[field].sum += value;
404
+ bucket.avg[field].count++;
405
+ }
406
+ }
407
+ for (const field of metrics.min) {
408
+ const value = getPathValue(object, field);
409
+ if (value !== undefined && (bucket.min[field] === undefined || compareSortValues(value, bucket.min[field]) < 0)) {
410
+ bucket.min[field] = cloneResultValue(value);
411
+ }
412
+ }
413
+ for (const field of metrics.max) {
414
+ const value = getPathValue(object, field);
415
+ if (value !== undefined && (bucket.max[field] === undefined || compareSortValues(value, bucket.max[field]) > 0)) {
416
+ bucket.max[field] = cloneResultValue(value);
417
+ }
418
+ }
419
+ }
420
+
421
+ function finishLegacyBucket(bucket, metrics) {
422
+ const out = { count: bucket.count };
423
+ if (bucket.key !== undefined) out.key = cloneResultValue(bucket.key);
424
+ if (metrics.sum.length) out.sum = bucket.sum;
425
+ if (metrics.avg.length) {
426
+ out.avg = Object.fromEntries(
427
+ Object.entries(bucket.avg).map(([field, state]) => [field, state.count ? state.sum / state.count : null]),
428
+ );
429
+ }
430
+ if (metrics.min.length) out.min = cloneResultValue(bucket.min);
431
+ if (metrics.max.length) out.max = cloneResultValue(bucket.max);
432
+ return out;
433
+ }
434
+
435
+ function asArray(value) {
436
+ if (!value) return [];
437
+ return Array.isArray(value) ? value : [value];
438
+ }
package/src/api.js ADDED
@@ -0,0 +1,15 @@
1
+ export {
2
+ ObjectHookRegistry,
3
+ ObjectHookRunner,
4
+ createObjectHookRegistry,
5
+ createObjectHookRunner,
6
+ normalizeObjectHooksMeta,
7
+ } from "./object-hooks.js";
8
+ export {
9
+ createInterfaceContext,
10
+ defaultInterfaces,
11
+ httpPeer,
12
+ shardMapFromHttp,
13
+ startRestoreStatusServer,
14
+ startServer,
15
+ } from "./server.js";