taladb 0.9.4 → 0.10.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/README.md +2 -2
- package/dist/index.browser.mjs +218 -24
- package/dist/index.d.mts +163 -5
- package/dist/index.d.ts +163 -5
- package/dist/index.js +218 -24
- package/dist/index.mjs +218 -24
- package/dist/index.react-native.mjs +218 -24
- package/package.json +9 -4
package/dist/index.d.ts
CHANGED
|
@@ -36,6 +36,67 @@ interface VectorSearchResult<T extends Document = Document> {
|
|
|
36
36
|
*/
|
|
37
37
|
score: number;
|
|
38
38
|
}
|
|
39
|
+
interface TextSearchResult<T extends Document = Document> {
|
|
40
|
+
/** The matched document. */
|
|
41
|
+
document: T;
|
|
42
|
+
/**
|
|
43
|
+
* BM25 relevance score — higher means more relevant. Unbounded above, and
|
|
44
|
+
* only meaningful for ordering within a single query's result set.
|
|
45
|
+
*/
|
|
46
|
+
score: number;
|
|
47
|
+
}
|
|
48
|
+
/** Tuning for `searchText`'s BM25 ranking. */
|
|
49
|
+
interface TextSearchOptions {
|
|
50
|
+
/**
|
|
51
|
+
* Term-frequency saturation (BM25 `k1`, default `1.2`). Higher values let a
|
|
52
|
+
* repeated term keep adding relevance for longer.
|
|
53
|
+
*/
|
|
54
|
+
k1?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Length normalisation (BM25 `b`, default `0.75`). `0` ignores document
|
|
57
|
+
* length; `1` normalises fully by length relative to the corpus average.
|
|
58
|
+
*/
|
|
59
|
+
b?: number;
|
|
60
|
+
}
|
|
61
|
+
interface HybridSearchResult<T extends Document = Document> {
|
|
62
|
+
/** The matched document. */
|
|
63
|
+
document: T;
|
|
64
|
+
/**
|
|
65
|
+
* Fused reciprocal-rank-fusion score. Small by construction and meaningful
|
|
66
|
+
* only as an ordering within one result set — never a similarity or a
|
|
67
|
+
* confidence.
|
|
68
|
+
*/
|
|
69
|
+
score: number;
|
|
70
|
+
/**
|
|
71
|
+
* Zero-based position in the text ranking, or `null` if the text retriever
|
|
72
|
+
* did not return this document.
|
|
73
|
+
*/
|
|
74
|
+
textRank: number | null;
|
|
75
|
+
/**
|
|
76
|
+
* Zero-based position in the vector ranking, or `null` if the vector
|
|
77
|
+
* retriever did not return this document.
|
|
78
|
+
*/
|
|
79
|
+
vectorRank: number | null;
|
|
80
|
+
}
|
|
81
|
+
/** Tuning for `hybridSearch`'s fusion and per-retriever scoring. */
|
|
82
|
+
interface HybridSearchOptions extends TextSearchOptions {
|
|
83
|
+
/**
|
|
84
|
+
* Reciprocal rank fusion smoothing constant (default `60`). Larger values
|
|
85
|
+
* flatten the advantage of the very top ranks.
|
|
86
|
+
*/
|
|
87
|
+
rrfK?: number;
|
|
88
|
+
/** Relative weight of the text ranking (default `1`). Set `0` to disable it. */
|
|
89
|
+
textWeight?: number;
|
|
90
|
+
/** Relative weight of the vector ranking (default `1`). Set `0` to disable it. */
|
|
91
|
+
vectorWeight?: number;
|
|
92
|
+
/**
|
|
93
|
+
* How many candidates to pull from each retriever before fusing
|
|
94
|
+
* (default `max(topK * 4, 20)`). Raise it for better recall at more cost;
|
|
95
|
+
* fusing only `topK` from each side drops documents that rank just outside
|
|
96
|
+
* one retriever but high in the other.
|
|
97
|
+
*/
|
|
98
|
+
candidates?: number;
|
|
99
|
+
}
|
|
39
100
|
type Value = null | boolean | number | string | Uint8Array | Value[] | {
|
|
40
101
|
[key: string]: Value;
|
|
41
102
|
};
|
|
@@ -221,12 +282,42 @@ interface CollectionOptions<T extends Document = Document> {
|
|
|
221
282
|
* });
|
|
222
283
|
*/
|
|
223
284
|
migrateDocument?: (doc: T, fromVersion: number) => T;
|
|
285
|
+
/**
|
|
286
|
+
* Lazy, read-time **downcast** — the mirror of {@link migrateDocument}, for a
|
|
287
|
+
* document written by a *newer* peer. When set, every document returned by
|
|
288
|
+
* `find` / `findOne` whose `_v` is **above** `syncSchema.version` is passed
|
|
289
|
+
* through `downgradeDocument(doc, fromVersion)` and projected into the shape
|
|
290
|
+
* this build understands, so application code on an old client sees a shape
|
|
291
|
+
* it can actually read instead of an unexpected future one.
|
|
292
|
+
*
|
|
293
|
+
* Requires `syncSchema.version`. Must be pure and deterministic.
|
|
294
|
+
*
|
|
295
|
+
* **The projection is view-only and is never persisted**, regardless of
|
|
296
|
+
* {@link persistMigrations}. The stored document keeps its original `_v` and
|
|
297
|
+
* its newer fields intact, because this replica must continue to replicate
|
|
298
|
+
* that document faithfully to other peers — an old client is a *reader* of a
|
|
299
|
+
* newer shape, never its editor. For the same reason the returned document
|
|
300
|
+
* keeps its original (higher) `_v`: it is a projection of a v-N document, not
|
|
301
|
+
* a v-M one, and writing it back wholesale would tell the fleet otherwise.
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* // This build understands v1. A v2 peer split `name` into first/last.
|
|
305
|
+
* const users = db.collection<User>('users', {
|
|
306
|
+
* syncSchema: { version: 1 },
|
|
307
|
+
* downgradeDocument: (doc) => ({ ...doc, name: `${doc.first} ${doc.last}` }),
|
|
308
|
+
* });
|
|
309
|
+
*/
|
|
310
|
+
downgradeDocument?: (doc: Readonly<T>, fromVersion: number) => T;
|
|
224
311
|
/**
|
|
225
312
|
* When `true`, a document upgraded by {@link migrateDocument} on read is
|
|
226
|
-
* **written back** to storage (a best-effort `updateOne` computing
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
313
|
+
* **written back** to storage (a best-effort `updateOne` computing a `$set`
|
|
314
|
+
* diff) so the migration becomes permanent — after which filters and indexes
|
|
315
|
+
* on the new shape match it. Default `false` (the migrated shape is returned
|
|
316
|
+
* but not persisted).
|
|
317
|
+
*
|
|
318
|
+
* The write-back is **additive-only** except for fields explicitly listed in
|
|
319
|
+
* {@link retiredFields}. A field present in storage but absent from the
|
|
320
|
+
* migrated document is otherwise left alone rather than `$unset`.
|
|
230
321
|
*
|
|
231
322
|
* Trade-offs: reads that encounter un-migrated documents now issue writes
|
|
232
323
|
* (which fire live-query and sync-hook notifications like any other write);
|
|
@@ -234,6 +325,35 @@ interface CollectionOptions<T extends Document = Document> {
|
|
|
234
325
|
* one-shot eager rewrite instead, prefer `openDB({ migrations })`.
|
|
235
326
|
*/
|
|
236
327
|
persistMigrations?: boolean;
|
|
328
|
+
/**
|
|
329
|
+
* Fields an upcast is explicitly allowed to remove during persist-on-read.
|
|
330
|
+
* Prefer this precise list to {@link allowFieldRemoval}: omissions of any
|
|
331
|
+
* other field remain additive and are preserved.
|
|
332
|
+
*/
|
|
333
|
+
retiredFields?: (keyof T & string)[];
|
|
334
|
+
/**
|
|
335
|
+
* How to read a field that {@link migrateDocument} left out of its output:
|
|
336
|
+
* as an intentional removal (`true`), or as a field the migration simply
|
|
337
|
+
* never heard of (`false`, the default).
|
|
338
|
+
*
|
|
339
|
+
* Default `false` — omitted fields are **preserved**: kept on the document
|
|
340
|
+
* returned to your code, and left in storage by the {@link persistMigrations}
|
|
341
|
+
* write-back rather than `$unset`.
|
|
342
|
+
*
|
|
343
|
+
* This default exists because on a synced collection the two cases are
|
|
344
|
+
* indistinguishable from the migration's output, and guessing "removal" is
|
|
345
|
+
* the destructive guess. A migration written today cannot mention a field a
|
|
346
|
+
* *newer* peer will add tomorrow, so an innocent `(doc) => ({ id, name })`
|
|
347
|
+
* becomes a deletion of a field its author never heard of — and under
|
|
348
|
+
* whole-document LWW that deletion replicates to the whole fleet. An old
|
|
349
|
+
* replica has to stay a faithful carrier of shapes it does not understand.
|
|
350
|
+
*
|
|
351
|
+
* Set `true` only on a collection that never syncs, or during a deliberate
|
|
352
|
+
* add → backfill → dual-read → **retire** rollout, where you already know the
|
|
353
|
+
* whole fleet has stopped writing the field.
|
|
354
|
+
*/
|
|
355
|
+
/** @deprecated Prefer {@link retiredFields}; this treats every omission as removal. */
|
|
356
|
+
allowFieldRemoval?: boolean;
|
|
237
357
|
}
|
|
238
358
|
/** A single MongoDB-style aggregation stage. */
|
|
239
359
|
type AggregateStage<T extends Document = Document> = {
|
|
@@ -351,6 +471,44 @@ interface Collection<T extends Document = Document> {
|
|
|
351
471
|
createFtsIndex(field: keyof Omit<T, '_id'> & string): Promise<void>;
|
|
352
472
|
/** Drop a full-text search index. */
|
|
353
473
|
dropFtsIndex(field: keyof Omit<T, '_id'> & string): Promise<void>;
|
|
474
|
+
/**
|
|
475
|
+
* Rank documents against a free-text `query` using BM25, most relevant first.
|
|
476
|
+
*
|
|
477
|
+
* Unlike the `$contains` filter, which requires **every** token to be
|
|
478
|
+
* present, this uses OR semantics — a document that matches more of the
|
|
479
|
+
* query simply scores higher. Requires an FTS index on `field`.
|
|
480
|
+
*
|
|
481
|
+
* @example
|
|
482
|
+
* const hits = await articles.searchText('body', 'reset my password', 5);
|
|
483
|
+
* // hits: Array<{ document: Article, score: number }>
|
|
484
|
+
*/
|
|
485
|
+
searchText(field: keyof Omit<T, '_id'> & string, query: string, topK: number, filter?: Filter<T>, options?: TextSearchOptions): Promise<TextSearchResult<T>[]>;
|
|
486
|
+
/**
|
|
487
|
+
* Hybrid retrieval: rank by keyword relevance (BM25) **and** vector
|
|
488
|
+
* similarity, then fuse the two rankings with reciprocal rank fusion.
|
|
489
|
+
*
|
|
490
|
+
* The two retrievers fail differently — keyword search misses paraphrases,
|
|
491
|
+
* vector search misses exact identifiers and rare proper nouns — so fusing
|
|
492
|
+
* them recovers both. A document both retrievers rank well outranks one that
|
|
493
|
+
* only a single retriever found. Requires an FTS index on `textField` and a
|
|
494
|
+
* vector index on `vectorField`.
|
|
495
|
+
*
|
|
496
|
+
* The optional `filter` is applied to both retrievers before ranking.
|
|
497
|
+
*
|
|
498
|
+
* @example
|
|
499
|
+
* const hits = await articles.hybridSearch(
|
|
500
|
+
* { textField: 'body', text: 'reset my password' },
|
|
501
|
+
* { vectorField: 'embedding', vector: queryVec },
|
|
502
|
+
* 5,
|
|
503
|
+
* );
|
|
504
|
+
*/
|
|
505
|
+
hybridSearch(text: {
|
|
506
|
+
textField: keyof Omit<T, '_id'> & string;
|
|
507
|
+
text: string;
|
|
508
|
+
}, vector: {
|
|
509
|
+
vectorField: keyof Omit<T, '_id'> & string;
|
|
510
|
+
vector: number[];
|
|
511
|
+
}, topK: number, filter?: Filter<T>, options?: HybridSearchOptions): Promise<HybridSearchResult<T>[]>;
|
|
354
512
|
/**
|
|
355
513
|
* Return the indexes that currently exist on this collection.
|
|
356
514
|
*
|
|
@@ -1321,4 +1479,4 @@ interface OpenDBOptions {
|
|
|
1321
1479
|
*/
|
|
1322
1480
|
declare function openDB(dbName?: string, options?: OpenDBOptions): Promise<TalaDB>;
|
|
1323
1481
|
|
|
1324
|
-
export { type AggregatePipeline, type AggregateStage, type BootstrapPage, type BootstrapRequest, type BridgeQuery, type BridgeResult, COVERAGE_COLLECTION, type Collection, type CollectionIndexInfo, type CollectionOptions, type CoordinatorOptions, type CoverageKey, type CoverageState, CoverageStore, type CursorSyncAdapter, type DeltaPage, type Document, type DurabilityConfig, type Filter, HttpSyncAdapter, type Migration, type OpenDBOptions, type PullResult, REPLICA_REVISION_FIELD, REPLICA_SCOPE_FIELD, type RemoteKey, ReplicationCoordinator, type ReplicationSource, type RestSourceOptions, type Schema, type SerializedChangeset, type SyncAdapter, type SyncConfig, type SyncDirection, type SyncOptions, type SyncResult, type TalaDB, type TalaDbConfig, TalaDbValidationError, type Update, type Value, type VectorIndexOptions, type VectorMetric, type VectorSearchResult, type WriteOrigin, applySchema, coverageKey, createRestSource, deriveDocId, isAuthoritative, openDB, progress, rowsApplied, runMigrations };
|
|
1482
|
+
export { type AggregatePipeline, type AggregateStage, type BootstrapPage, type BootstrapRequest, type BridgeQuery, type BridgeResult, COVERAGE_COLLECTION, type Collection, type CollectionIndexInfo, type CollectionOptions, type CoordinatorOptions, type CoverageKey, type CoverageState, CoverageStore, type CursorSyncAdapter, type DeltaPage, type Document, type DurabilityConfig, type Filter, HttpSyncAdapter, type HybridSearchOptions, type HybridSearchResult, type Migration, type OpenDBOptions, type PullResult, REPLICA_REVISION_FIELD, REPLICA_SCOPE_FIELD, type RemoteKey, ReplicationCoordinator, type ReplicationSource, type RestSourceOptions, type Schema, type SerializedChangeset, type SyncAdapter, type SyncConfig, type SyncDirection, type SyncOptions, type SyncResult, type TalaDB, type TalaDbConfig, TalaDbValidationError, type TextSearchOptions, type TextSearchResult, type Update, type Value, type VectorIndexOptions, type VectorMetric, type VectorSearchResult, type WriteOrigin, applySchema, coverageKey, createRestSource, deriveDocId, isAuthoritative, openDB, progress, rowsApplied, runMigrations };
|
package/dist/index.js
CHANGED
|
@@ -768,18 +768,52 @@ function deepEqual(a, b) {
|
|
|
768
768
|
);
|
|
769
769
|
}
|
|
770
770
|
function applySchema(col, options) {
|
|
771
|
-
const {
|
|
771
|
+
const {
|
|
772
|
+
schema,
|
|
773
|
+
validateOnRead = false,
|
|
774
|
+
migrateDocument,
|
|
775
|
+
downgradeDocument,
|
|
776
|
+
syncSchema,
|
|
777
|
+
persistMigrations = false,
|
|
778
|
+
allowFieldRemoval = false,
|
|
779
|
+
retiredFields = []
|
|
780
|
+
} = options;
|
|
772
781
|
const targetVersion = syncSchema?.version ?? 0;
|
|
773
782
|
if (migrateDocument && targetVersion < 1) {
|
|
774
783
|
throw new Error("CollectionOptions.migrateDocument requires syncSchema.version (the migration target)");
|
|
775
784
|
}
|
|
785
|
+
if (downgradeDocument && targetVersion < 1) {
|
|
786
|
+
throw new Error("CollectionOptions.downgradeDocument requires syncSchema.version (the shape this build reads)");
|
|
787
|
+
}
|
|
776
788
|
if (syncSchema && targetVersion < 1 && (syncSchema.renames || syncSchema.defaults)) {
|
|
777
789
|
throw new Error(
|
|
778
790
|
"CollectionOptions.syncSchema.renames/defaults require syncSchema.version >= 1 \u2014 without a version the import migration step never runs and documents missing the renamed/defaulted fields are quarantined instead of upgraded"
|
|
779
791
|
);
|
|
780
792
|
}
|
|
781
793
|
const stampVersion = targetVersion > 0;
|
|
782
|
-
if (!schema && !migrateDocument && !stampVersion) return col;
|
|
794
|
+
if (!schema && !migrateDocument && !downgradeDocument && !stampVersion) return col;
|
|
795
|
+
const retired = new Set(retiredFields);
|
|
796
|
+
const engineOwned = /* @__PURE__ */ new Set([
|
|
797
|
+
"_id",
|
|
798
|
+
"_v",
|
|
799
|
+
"_changed_at",
|
|
800
|
+
"_remote",
|
|
801
|
+
"_remote_rev",
|
|
802
|
+
"_replica_scope"
|
|
803
|
+
]);
|
|
804
|
+
const downcastViews = /* @__PURE__ */ new WeakSet();
|
|
805
|
+
function preserveFields(original, next, preserveUnknown, preserveVersion = true) {
|
|
806
|
+
let out = null;
|
|
807
|
+
for (const k of Object.keys(original)) {
|
|
808
|
+
const ownedField = engineOwned.has(k) && (k !== "_v" || preserveVersion);
|
|
809
|
+
const mustRestore = ownedField || preserveUnknown && !retired.has(k) && !(k in next);
|
|
810
|
+
if (!mustRestore) continue;
|
|
811
|
+
if (!ownedField && k in next) continue;
|
|
812
|
+
out ?? (out = { ...next });
|
|
813
|
+
out[k] = original[k];
|
|
814
|
+
}
|
|
815
|
+
return out ?? next;
|
|
816
|
+
}
|
|
783
817
|
function parseWrite(doc, label) {
|
|
784
818
|
try {
|
|
785
819
|
return schema.parse(doc);
|
|
@@ -787,6 +821,43 @@ function applySchema(col, options) {
|
|
|
787
821
|
throw new TalaDbValidationError(err, label);
|
|
788
822
|
}
|
|
789
823
|
}
|
|
824
|
+
function assertWritableDocument(doc, label) {
|
|
825
|
+
if (doc && typeof doc === "object" && downcastViews.has(doc)) {
|
|
826
|
+
throw new Error(`${label}: a downgradeDocument result is a read-only compatibility view`);
|
|
827
|
+
}
|
|
828
|
+
const version = doc?._v;
|
|
829
|
+
if (targetVersion > 0 && typeof version === "number" && version > targetVersion) {
|
|
830
|
+
throw new Error(
|
|
831
|
+
`${label}: this client supports schema v${targetVersion}, but the document is v${version}`
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
function writableFilter(filter) {
|
|
836
|
+
if (targetVersion < 1) return filter;
|
|
837
|
+
return {
|
|
838
|
+
$and: [
|
|
839
|
+
filter,
|
|
840
|
+
{
|
|
841
|
+
$or: [
|
|
842
|
+
{ _v: { $exists: false } },
|
|
843
|
+
{ _v: { $lte: targetVersion } }
|
|
844
|
+
]
|
|
845
|
+
}
|
|
846
|
+
]
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
function assertSafeUpdate(update) {
|
|
850
|
+
const record = update;
|
|
851
|
+
for (const op of ["$set", "$unset", "$inc", "$push", "$pull"]) {
|
|
852
|
+
const fields = record[op];
|
|
853
|
+
if (!fields) continue;
|
|
854
|
+
for (const field of Object.keys(fields)) {
|
|
855
|
+
if (engineOwned.has(field)) {
|
|
856
|
+
throw new Error(`update cannot modify engine-owned field '${field}'`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
790
861
|
function stamp(doc) {
|
|
791
862
|
if (!stampVersion || doc._v !== void 0) return doc;
|
|
792
863
|
return { ...doc, _v: targetVersion };
|
|
@@ -802,50 +873,86 @@ function applySchema(col, options) {
|
|
|
802
873
|
if (k === "_id") continue;
|
|
803
874
|
if (!deepEqual(migrated[k], original[k])) $set[k] = migrated[k];
|
|
804
875
|
}
|
|
805
|
-
|
|
806
|
-
|
|
876
|
+
if (allowFieldRemoval || retired.size > 0) {
|
|
877
|
+
for (const k of Object.keys(original)) {
|
|
878
|
+
if (k !== "_id" && !(k in migrated) && (allowFieldRemoval || retired.has(k))) $unset[k] = true;
|
|
879
|
+
}
|
|
807
880
|
}
|
|
808
881
|
const update = {};
|
|
809
882
|
if (Object.keys($set).length) update.$set = $set;
|
|
810
883
|
if (Object.keys($unset).length) update.$unset = $unset;
|
|
811
884
|
return Object.keys(update).length ? update : null;
|
|
812
885
|
}
|
|
813
|
-
function
|
|
814
|
-
if (!migrateDocument) return doc;
|
|
886
|
+
function normalizeRead(doc) {
|
|
815
887
|
const fromVersion = typeof doc._v === "number" ? doc._v : 0;
|
|
816
|
-
if (fromVersion
|
|
817
|
-
|
|
888
|
+
if (migrateDocument && fromVersion < targetVersion) {
|
|
889
|
+
const up = { ...migrateDocument(doc, fromVersion), _v: targetVersion };
|
|
890
|
+
return {
|
|
891
|
+
// The migration owns the version transition, while every other
|
|
892
|
+
// engine-owned field continues to come from the stored document.
|
|
893
|
+
value: allowFieldRemoval ? preserveFields(doc, up, false, false) : preserveFields(doc, up, true, false),
|
|
894
|
+
persistable: true
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
if (downgradeDocument && fromVersion > targetVersion) {
|
|
898
|
+
const projected = {
|
|
899
|
+
...downgradeDocument(doc, fromVersion),
|
|
900
|
+
...doc._id !== void 0 ? { _id: doc._id } : {},
|
|
901
|
+
_v: fromVersion
|
|
902
|
+
};
|
|
903
|
+
downcastViews.add(projected);
|
|
904
|
+
return { value: projected, persistable: false };
|
|
905
|
+
}
|
|
906
|
+
return { value: doc, persistable: true };
|
|
818
907
|
}
|
|
819
908
|
function validateRead(doc) {
|
|
820
909
|
if (!validateOnRead || !schema) return doc;
|
|
821
910
|
try {
|
|
822
|
-
|
|
911
|
+
const parsed = schema.parse(doc);
|
|
912
|
+
const fromVersion = typeof doc._v === "number" ? doc._v : 0;
|
|
913
|
+
return preserveFields(doc, parsed, targetVersion > 0 && fromVersion > targetVersion);
|
|
823
914
|
} catch (err) {
|
|
824
915
|
throw new TalaDbValidationError(err, "read");
|
|
825
916
|
}
|
|
826
917
|
}
|
|
827
|
-
async function persistAll(originals,
|
|
918
|
+
async function persistAll(originals, normalized) {
|
|
828
919
|
if (!persistMigrations) return;
|
|
829
920
|
for (let i = 0; i < originals.length; i++) {
|
|
830
921
|
const original = originals[i];
|
|
831
|
-
if (
|
|
832
|
-
const
|
|
922
|
+
if (!normalized[i].persistable) continue;
|
|
923
|
+
const migrated = normalized[i].value;
|
|
924
|
+
if (migrated === original || typeof original._id !== "string") continue;
|
|
925
|
+
const update = diffUpdate(original, migrated);
|
|
833
926
|
if (!update) continue;
|
|
834
927
|
try {
|
|
835
|
-
|
|
928
|
+
const guards = [{ _id: original._id }];
|
|
929
|
+
if (original._v === void 0) guards.push({ _v: { $exists: false } });
|
|
930
|
+
else guards.push({ _v: original._v });
|
|
931
|
+
if (original._changed_at !== void 0) {
|
|
932
|
+
guards.push({ _changed_at: original._changed_at });
|
|
933
|
+
}
|
|
934
|
+
await col.updateOne({ $and: guards }, update);
|
|
836
935
|
} catch {
|
|
837
936
|
}
|
|
838
937
|
}
|
|
839
938
|
}
|
|
840
|
-
const wrapReads = Boolean(migrateDocument) || validateOnRead && Boolean(schema);
|
|
939
|
+
const wrapReads = Boolean(migrateDocument) || Boolean(downgradeDocument) || validateOnRead && Boolean(schema);
|
|
841
940
|
const wrapWrites = Boolean(schema) || stampVersion;
|
|
941
|
+
function pipelinePreservesDocuments(pipeline) {
|
|
942
|
+
return pipeline.every((stage) => !("$group" in stage) && !("$project" in stage));
|
|
943
|
+
}
|
|
944
|
+
function normalizeViewRows(docs) {
|
|
945
|
+
return docs.map((doc) => validateRead(normalizeRead(doc).value));
|
|
946
|
+
}
|
|
842
947
|
return {
|
|
843
948
|
...col,
|
|
844
949
|
insert: wrapWrites ? async (doc) => {
|
|
950
|
+
assertWritableDocument(doc, "insert");
|
|
845
951
|
if (schema) parseWrite(doc, "insert");
|
|
846
952
|
return col.insert(stamp(doc));
|
|
847
953
|
} : col.insert.bind(col),
|
|
848
954
|
insertMany: wrapWrites ? async (docs) => {
|
|
955
|
+
docs.forEach((doc, i) => assertWritableDocument(doc, `insertMany[${i}]`));
|
|
849
956
|
if (schema) docs.forEach((doc, i) => parseWrite(doc, `insertMany[${i}]`));
|
|
850
957
|
return col.insertMany(docs.map(stamp));
|
|
851
958
|
} : col.insertMany.bind(col),
|
|
@@ -854,6 +961,14 @@ function applySchema(col, options) {
|
|
|
854
961
|
// runtime schema check have to be the same seam, or a malformed server
|
|
855
962
|
// response walks straight into a typed collection.
|
|
856
963
|
replaceManyWithIds: wrapWrites ? async (docs, origin) => {
|
|
964
|
+
if (origin !== "remote") {
|
|
965
|
+
docs.forEach((doc, i) => assertWritableDocument(doc, `replaceManyWithIds[${i}]`));
|
|
966
|
+
if (targetVersion > 0) {
|
|
967
|
+
throw new Error(
|
|
968
|
+
"local replaceManyWithIds is disabled on versioned collections; use updateOne/updateMany so schema-version guards are atomic"
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
857
972
|
if (schema) docs.forEach((doc, i) => {
|
|
858
973
|
const { _replica_scope, _remote_rev, ...schemaDoc } = doc;
|
|
859
974
|
void _replica_scope;
|
|
@@ -862,38 +977,75 @@ function applySchema(col, options) {
|
|
|
862
977
|
});
|
|
863
978
|
return col.replaceManyWithIds(docs.map((d) => stampDoc(d)), origin);
|
|
864
979
|
} : col.replaceManyWithIds.bind(col),
|
|
980
|
+
deleteManyWithIds: stampVersion ? async (ids, origin) => {
|
|
981
|
+
if (origin !== "remote") {
|
|
982
|
+
throw new Error(
|
|
983
|
+
"local deleteManyWithIds is disabled on versioned collections; use deleteOne/deleteMany so schema-version guards are atomic"
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
return col.deleteManyWithIds(ids, origin);
|
|
987
|
+
} : col.deleteManyWithIds.bind(col),
|
|
988
|
+
updateOne: wrapWrites ? async (filter, update) => {
|
|
989
|
+
assertSafeUpdate(update);
|
|
990
|
+
return col.updateOne(writableFilter(filter), update);
|
|
991
|
+
} : col.updateOne.bind(col),
|
|
992
|
+
updateMany: wrapWrites ? async (filter, update) => {
|
|
993
|
+
assertSafeUpdate(update);
|
|
994
|
+
return col.updateMany(writableFilter(filter), update);
|
|
995
|
+
} : col.updateMany.bind(col),
|
|
996
|
+
deleteOne: stampVersion ? (filter) => col.deleteOne(writableFilter(filter)) : col.deleteOne.bind(col),
|
|
997
|
+
deleteMany: stampVersion ? (filter) => col.deleteMany(writableFilter(filter)) : col.deleteMany.bind(col),
|
|
865
998
|
find: wrapReads ? async (filter) => {
|
|
866
999
|
const docs = await col.find(filter);
|
|
867
|
-
const
|
|
868
|
-
await persistAll(docs,
|
|
869
|
-
return
|
|
1000
|
+
const normalized = docs.map(normalizeRead);
|
|
1001
|
+
await persistAll(docs, normalized);
|
|
1002
|
+
return normalized.map((n) => validateRead(n.value));
|
|
870
1003
|
} : col.find.bind(col),
|
|
871
1004
|
findOne: wrapReads ? async (filter) => {
|
|
872
1005
|
const doc = await col.findOne(filter);
|
|
873
1006
|
if (doc === null) return null;
|
|
874
|
-
const
|
|
875
|
-
await persistAll([doc], [
|
|
876
|
-
return validateRead(
|
|
1007
|
+
const normalized = normalizeRead(doc);
|
|
1008
|
+
await persistAll([doc], [normalized]);
|
|
1009
|
+
return validateRead(normalized.value);
|
|
877
1010
|
} : col.findOne.bind(col),
|
|
1011
|
+
aggregate: wrapReads ? async (pipeline) => {
|
|
1012
|
+
const docs = await col.aggregate(pipeline);
|
|
1013
|
+
return pipelinePreservesDocuments(pipeline) ? normalizeViewRows(docs) : docs;
|
|
1014
|
+
} : col.aggregate.bind(col),
|
|
878
1015
|
// Live queries feed every @taladb/react hook (useFind, useFindOne,
|
|
879
1016
|
// useQueries). Leaving them unwrapped meant React components received the
|
|
880
1017
|
// un-migrated shape while a direct find() returned the migrated one.
|
|
881
1018
|
subscribe: wrapReads ? (filter, callback, onError) => col.subscribe(
|
|
882
1019
|
filter,
|
|
883
1020
|
(docs) => {
|
|
884
|
-
const
|
|
1021
|
+
const normalized = docs.map(normalizeRead);
|
|
885
1022
|
let out;
|
|
886
1023
|
try {
|
|
887
|
-
out =
|
|
1024
|
+
out = normalized.map((n) => validateRead(n.value));
|
|
888
1025
|
} catch (err) {
|
|
889
1026
|
onError?.(err);
|
|
890
1027
|
return;
|
|
891
1028
|
}
|
|
892
1029
|
callback(out);
|
|
893
|
-
void persistAll(docs,
|
|
1030
|
+
void persistAll(docs, normalized);
|
|
894
1031
|
},
|
|
895
1032
|
onError
|
|
896
|
-
) : col.subscribe.bind(col)
|
|
1033
|
+
) : col.subscribe.bind(col),
|
|
1034
|
+
subscribeAggregate: wrapReads ? (pipeline, callback, onError) => col.subscribeAggregate(
|
|
1035
|
+
pipeline,
|
|
1036
|
+
(docs) => {
|
|
1037
|
+
if (!pipelinePreservesDocuments(pipeline)) {
|
|
1038
|
+
callback(docs);
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
try {
|
|
1042
|
+
callback(normalizeViewRows(docs));
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
onError?.(error);
|
|
1045
|
+
}
|
|
1046
|
+
},
|
|
1047
|
+
onError
|
|
1048
|
+
) : col.subscribeAggregate.bind(col)
|
|
897
1049
|
};
|
|
898
1050
|
}
|
|
899
1051
|
function detectPlatform() {
|
|
@@ -1103,6 +1255,30 @@ async function createBrowserDB(dbName, config, passphrase, migrations) {
|
|
|
1103
1255
|
});
|
|
1104
1256
|
return JSON.parse(json);
|
|
1105
1257
|
},
|
|
1258
|
+
searchText: async (field, query, topK, filter, options) => {
|
|
1259
|
+
const json = await proxy.send("searchText", {
|
|
1260
|
+
collection: name,
|
|
1261
|
+
field,
|
|
1262
|
+
query,
|
|
1263
|
+
topK,
|
|
1264
|
+
filterJson: filter ? JSON.stringify(filter) : "null",
|
|
1265
|
+
optionsJson: options ? JSON.stringify(options) : "null"
|
|
1266
|
+
});
|
|
1267
|
+
return JSON.parse(json);
|
|
1268
|
+
},
|
|
1269
|
+
hybridSearch: async (text, vector, topK, filter, options) => {
|
|
1270
|
+
const json = await proxy.send("hybridSearch", {
|
|
1271
|
+
collection: name,
|
|
1272
|
+
textField: text.textField,
|
|
1273
|
+
text: text.text,
|
|
1274
|
+
vectorField: vector.vectorField,
|
|
1275
|
+
vectorJson: JSON.stringify(vector.vector),
|
|
1276
|
+
topK,
|
|
1277
|
+
filterJson: filter ? JSON.stringify(filter) : "null",
|
|
1278
|
+
optionsJson: options ? JSON.stringify(options) : "null"
|
|
1279
|
+
});
|
|
1280
|
+
return JSON.parse(json);
|
|
1281
|
+
},
|
|
1106
1282
|
subscribe: (filter, callback, onError) => nudgedPoller(
|
|
1107
1283
|
() => proxy.send("find", {
|
|
1108
1284
|
collection: name,
|
|
@@ -1247,6 +1423,12 @@ async function createNodeDB(dbName, config, passphrase, migrations) {
|
|
|
1247
1423
|
const raw = await col.findNearest(field, vector, topK, filter ?? null);
|
|
1248
1424
|
return raw;
|
|
1249
1425
|
},
|
|
1426
|
+
searchText: async (field, query, topK, filter, options) => {
|
|
1427
|
+
return col.searchText(field, query, topK, filter ?? null, options ?? null);
|
|
1428
|
+
},
|
|
1429
|
+
hybridSearch: async (text, vector, topK, filter, options) => {
|
|
1430
|
+
return col.hybridSearch(text.textField, text.text, vector.vectorField, vector.vector, topK, filter ?? null, options ?? null);
|
|
1431
|
+
},
|
|
1250
1432
|
subscribe: (filter, callback, onError) => makePoller(async () => col.find(filter ?? null), callback, onError),
|
|
1251
1433
|
subscribeAggregate: (pipeline, callback, onError) => makePoller(async () => wrapped.aggregate(pipeline), callback, onError)
|
|
1252
1434
|
};
|
|
@@ -1329,6 +1511,18 @@ async function createNativeDB(_dbName, migrations) {
|
|
|
1329
1511
|
const raw = native.findNearest(name, field, vector, topK, filter ?? null);
|
|
1330
1512
|
return raw;
|
|
1331
1513
|
},
|
|
1514
|
+
searchText: async (field, query, topK, filter, options) => {
|
|
1515
|
+
if (!native.searchText) {
|
|
1516
|
+
throw new Error("searchText requires @taladb/react-native \u2265 0.10 \u2014 rebuild the native module");
|
|
1517
|
+
}
|
|
1518
|
+
return native.searchText(name, field, query, topK, filter ?? null, options ?? null);
|
|
1519
|
+
},
|
|
1520
|
+
hybridSearch: async (text, vector, topK, filter, options) => {
|
|
1521
|
+
if (!native.hybridSearch) {
|
|
1522
|
+
throw new Error("hybridSearch requires @taladb/react-native \u2265 0.10 \u2014 rebuild the native module");
|
|
1523
|
+
}
|
|
1524
|
+
return native.hybridSearch(name, text.textField, text.text, vector.vectorField, vector.vector, topK, filter ?? null, options ?? null);
|
|
1525
|
+
},
|
|
1332
1526
|
subscribe: (filter, callback, onError) => makePoller(async () => native.find(name, filter ?? {}), callback, onError),
|
|
1333
1527
|
subscribeAggregate: (pipeline, callback, onError) => makePoller(async () => native.aggregate(name, pipeline), callback, onError)
|
|
1334
1528
|
};
|