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
|
@@ -659,18 +659,52 @@ function deepEqual(a, b) {
|
|
|
659
659
|
);
|
|
660
660
|
}
|
|
661
661
|
function applySchema(col, options) {
|
|
662
|
-
const {
|
|
662
|
+
const {
|
|
663
|
+
schema,
|
|
664
|
+
validateOnRead = false,
|
|
665
|
+
migrateDocument,
|
|
666
|
+
downgradeDocument,
|
|
667
|
+
syncSchema,
|
|
668
|
+
persistMigrations = false,
|
|
669
|
+
allowFieldRemoval = false,
|
|
670
|
+
retiredFields = []
|
|
671
|
+
} = options;
|
|
663
672
|
const targetVersion = syncSchema?.version ?? 0;
|
|
664
673
|
if (migrateDocument && targetVersion < 1) {
|
|
665
674
|
throw new Error("CollectionOptions.migrateDocument requires syncSchema.version (the migration target)");
|
|
666
675
|
}
|
|
676
|
+
if (downgradeDocument && targetVersion < 1) {
|
|
677
|
+
throw new Error("CollectionOptions.downgradeDocument requires syncSchema.version (the shape this build reads)");
|
|
678
|
+
}
|
|
667
679
|
if (syncSchema && targetVersion < 1 && (syncSchema.renames || syncSchema.defaults)) {
|
|
668
680
|
throw new Error(
|
|
669
681
|
"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"
|
|
670
682
|
);
|
|
671
683
|
}
|
|
672
684
|
const stampVersion = targetVersion > 0;
|
|
673
|
-
if (!schema && !migrateDocument && !stampVersion) return col;
|
|
685
|
+
if (!schema && !migrateDocument && !downgradeDocument && !stampVersion) return col;
|
|
686
|
+
const retired = new Set(retiredFields);
|
|
687
|
+
const engineOwned = /* @__PURE__ */ new Set([
|
|
688
|
+
"_id",
|
|
689
|
+
"_v",
|
|
690
|
+
"_changed_at",
|
|
691
|
+
"_remote",
|
|
692
|
+
"_remote_rev",
|
|
693
|
+
"_replica_scope"
|
|
694
|
+
]);
|
|
695
|
+
const downcastViews = /* @__PURE__ */ new WeakSet();
|
|
696
|
+
function preserveFields(original, next, preserveUnknown, preserveVersion = true) {
|
|
697
|
+
let out = null;
|
|
698
|
+
for (const k of Object.keys(original)) {
|
|
699
|
+
const ownedField = engineOwned.has(k) && (k !== "_v" || preserveVersion);
|
|
700
|
+
const mustRestore = ownedField || preserveUnknown && !retired.has(k) && !(k in next);
|
|
701
|
+
if (!mustRestore) continue;
|
|
702
|
+
if (!ownedField && k in next) continue;
|
|
703
|
+
out ?? (out = { ...next });
|
|
704
|
+
out[k] = original[k];
|
|
705
|
+
}
|
|
706
|
+
return out ?? next;
|
|
707
|
+
}
|
|
674
708
|
function parseWrite(doc, label) {
|
|
675
709
|
try {
|
|
676
710
|
return schema.parse(doc);
|
|
@@ -678,6 +712,43 @@ function applySchema(col, options) {
|
|
|
678
712
|
throw new TalaDbValidationError(err, label);
|
|
679
713
|
}
|
|
680
714
|
}
|
|
715
|
+
function assertWritableDocument(doc, label) {
|
|
716
|
+
if (doc && typeof doc === "object" && downcastViews.has(doc)) {
|
|
717
|
+
throw new Error(`${label}: a downgradeDocument result is a read-only compatibility view`);
|
|
718
|
+
}
|
|
719
|
+
const version = doc?._v;
|
|
720
|
+
if (targetVersion > 0 && typeof version === "number" && version > targetVersion) {
|
|
721
|
+
throw new Error(
|
|
722
|
+
`${label}: this client supports schema v${targetVersion}, but the document is v${version}`
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
function writableFilter(filter) {
|
|
727
|
+
if (targetVersion < 1) return filter;
|
|
728
|
+
return {
|
|
729
|
+
$and: [
|
|
730
|
+
filter,
|
|
731
|
+
{
|
|
732
|
+
$or: [
|
|
733
|
+
{ _v: { $exists: false } },
|
|
734
|
+
{ _v: { $lte: targetVersion } }
|
|
735
|
+
]
|
|
736
|
+
}
|
|
737
|
+
]
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
function assertSafeUpdate(update) {
|
|
741
|
+
const record = update;
|
|
742
|
+
for (const op of ["$set", "$unset", "$inc", "$push", "$pull"]) {
|
|
743
|
+
const fields = record[op];
|
|
744
|
+
if (!fields) continue;
|
|
745
|
+
for (const field of Object.keys(fields)) {
|
|
746
|
+
if (engineOwned.has(field)) {
|
|
747
|
+
throw new Error(`update cannot modify engine-owned field '${field}'`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
681
752
|
function stamp(doc) {
|
|
682
753
|
if (!stampVersion || doc._v !== void 0) return doc;
|
|
683
754
|
return { ...doc, _v: targetVersion };
|
|
@@ -693,50 +764,86 @@ function applySchema(col, options) {
|
|
|
693
764
|
if (k === "_id") continue;
|
|
694
765
|
if (!deepEqual(migrated[k], original[k])) $set[k] = migrated[k];
|
|
695
766
|
}
|
|
696
|
-
|
|
697
|
-
|
|
767
|
+
if (allowFieldRemoval || retired.size > 0) {
|
|
768
|
+
for (const k of Object.keys(original)) {
|
|
769
|
+
if (k !== "_id" && !(k in migrated) && (allowFieldRemoval || retired.has(k))) $unset[k] = true;
|
|
770
|
+
}
|
|
698
771
|
}
|
|
699
772
|
const update = {};
|
|
700
773
|
if (Object.keys($set).length) update.$set = $set;
|
|
701
774
|
if (Object.keys($unset).length) update.$unset = $unset;
|
|
702
775
|
return Object.keys(update).length ? update : null;
|
|
703
776
|
}
|
|
704
|
-
function
|
|
705
|
-
if (!migrateDocument) return doc;
|
|
777
|
+
function normalizeRead(doc) {
|
|
706
778
|
const fromVersion = typeof doc._v === "number" ? doc._v : 0;
|
|
707
|
-
if (fromVersion
|
|
708
|
-
|
|
779
|
+
if (migrateDocument && fromVersion < targetVersion) {
|
|
780
|
+
const up = { ...migrateDocument(doc, fromVersion), _v: targetVersion };
|
|
781
|
+
return {
|
|
782
|
+
// The migration owns the version transition, while every other
|
|
783
|
+
// engine-owned field continues to come from the stored document.
|
|
784
|
+
value: allowFieldRemoval ? preserveFields(doc, up, false, false) : preserveFields(doc, up, true, false),
|
|
785
|
+
persistable: true
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
if (downgradeDocument && fromVersion > targetVersion) {
|
|
789
|
+
const projected = {
|
|
790
|
+
...downgradeDocument(doc, fromVersion),
|
|
791
|
+
...doc._id !== void 0 ? { _id: doc._id } : {},
|
|
792
|
+
_v: fromVersion
|
|
793
|
+
};
|
|
794
|
+
downcastViews.add(projected);
|
|
795
|
+
return { value: projected, persistable: false };
|
|
796
|
+
}
|
|
797
|
+
return { value: doc, persistable: true };
|
|
709
798
|
}
|
|
710
799
|
function validateRead(doc) {
|
|
711
800
|
if (!validateOnRead || !schema) return doc;
|
|
712
801
|
try {
|
|
713
|
-
|
|
802
|
+
const parsed = schema.parse(doc);
|
|
803
|
+
const fromVersion = typeof doc._v === "number" ? doc._v : 0;
|
|
804
|
+
return preserveFields(doc, parsed, targetVersion > 0 && fromVersion > targetVersion);
|
|
714
805
|
} catch (err) {
|
|
715
806
|
throw new TalaDbValidationError(err, "read");
|
|
716
807
|
}
|
|
717
808
|
}
|
|
718
|
-
async function persistAll(originals,
|
|
809
|
+
async function persistAll(originals, normalized) {
|
|
719
810
|
if (!persistMigrations) return;
|
|
720
811
|
for (let i = 0; i < originals.length; i++) {
|
|
721
812
|
const original = originals[i];
|
|
722
|
-
if (
|
|
723
|
-
const
|
|
813
|
+
if (!normalized[i].persistable) continue;
|
|
814
|
+
const migrated = normalized[i].value;
|
|
815
|
+
if (migrated === original || typeof original._id !== "string") continue;
|
|
816
|
+
const update = diffUpdate(original, migrated);
|
|
724
817
|
if (!update) continue;
|
|
725
818
|
try {
|
|
726
|
-
|
|
819
|
+
const guards = [{ _id: original._id }];
|
|
820
|
+
if (original._v === void 0) guards.push({ _v: { $exists: false } });
|
|
821
|
+
else guards.push({ _v: original._v });
|
|
822
|
+
if (original._changed_at !== void 0) {
|
|
823
|
+
guards.push({ _changed_at: original._changed_at });
|
|
824
|
+
}
|
|
825
|
+
await col.updateOne({ $and: guards }, update);
|
|
727
826
|
} catch {
|
|
728
827
|
}
|
|
729
828
|
}
|
|
730
829
|
}
|
|
731
|
-
const wrapReads = Boolean(migrateDocument) || validateOnRead && Boolean(schema);
|
|
830
|
+
const wrapReads = Boolean(migrateDocument) || Boolean(downgradeDocument) || validateOnRead && Boolean(schema);
|
|
732
831
|
const wrapWrites = Boolean(schema) || stampVersion;
|
|
832
|
+
function pipelinePreservesDocuments(pipeline) {
|
|
833
|
+
return pipeline.every((stage) => !("$group" in stage) && !("$project" in stage));
|
|
834
|
+
}
|
|
835
|
+
function normalizeViewRows(docs) {
|
|
836
|
+
return docs.map((doc) => validateRead(normalizeRead(doc).value));
|
|
837
|
+
}
|
|
733
838
|
return {
|
|
734
839
|
...col,
|
|
735
840
|
insert: wrapWrites ? async (doc) => {
|
|
841
|
+
assertWritableDocument(doc, "insert");
|
|
736
842
|
if (schema) parseWrite(doc, "insert");
|
|
737
843
|
return col.insert(stamp(doc));
|
|
738
844
|
} : col.insert.bind(col),
|
|
739
845
|
insertMany: wrapWrites ? async (docs) => {
|
|
846
|
+
docs.forEach((doc, i) => assertWritableDocument(doc, `insertMany[${i}]`));
|
|
740
847
|
if (schema) docs.forEach((doc, i) => parseWrite(doc, `insertMany[${i}]`));
|
|
741
848
|
return col.insertMany(docs.map(stamp));
|
|
742
849
|
} : col.insertMany.bind(col),
|
|
@@ -745,6 +852,14 @@ function applySchema(col, options) {
|
|
|
745
852
|
// runtime schema check have to be the same seam, or a malformed server
|
|
746
853
|
// response walks straight into a typed collection.
|
|
747
854
|
replaceManyWithIds: wrapWrites ? async (docs, origin) => {
|
|
855
|
+
if (origin !== "remote") {
|
|
856
|
+
docs.forEach((doc, i) => assertWritableDocument(doc, `replaceManyWithIds[${i}]`));
|
|
857
|
+
if (targetVersion > 0) {
|
|
858
|
+
throw new Error(
|
|
859
|
+
"local replaceManyWithIds is disabled on versioned collections; use updateOne/updateMany so schema-version guards are atomic"
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
748
863
|
if (schema) docs.forEach((doc, i) => {
|
|
749
864
|
const { _replica_scope, _remote_rev, ...schemaDoc } = doc;
|
|
750
865
|
void _replica_scope;
|
|
@@ -753,38 +868,75 @@ function applySchema(col, options) {
|
|
|
753
868
|
});
|
|
754
869
|
return col.replaceManyWithIds(docs.map((d) => stampDoc(d)), origin);
|
|
755
870
|
} : col.replaceManyWithIds.bind(col),
|
|
871
|
+
deleteManyWithIds: stampVersion ? async (ids, origin) => {
|
|
872
|
+
if (origin !== "remote") {
|
|
873
|
+
throw new Error(
|
|
874
|
+
"local deleteManyWithIds is disabled on versioned collections; use deleteOne/deleteMany so schema-version guards are atomic"
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
return col.deleteManyWithIds(ids, origin);
|
|
878
|
+
} : col.deleteManyWithIds.bind(col),
|
|
879
|
+
updateOne: wrapWrites ? async (filter, update) => {
|
|
880
|
+
assertSafeUpdate(update);
|
|
881
|
+
return col.updateOne(writableFilter(filter), update);
|
|
882
|
+
} : col.updateOne.bind(col),
|
|
883
|
+
updateMany: wrapWrites ? async (filter, update) => {
|
|
884
|
+
assertSafeUpdate(update);
|
|
885
|
+
return col.updateMany(writableFilter(filter), update);
|
|
886
|
+
} : col.updateMany.bind(col),
|
|
887
|
+
deleteOne: stampVersion ? (filter) => col.deleteOne(writableFilter(filter)) : col.deleteOne.bind(col),
|
|
888
|
+
deleteMany: stampVersion ? (filter) => col.deleteMany(writableFilter(filter)) : col.deleteMany.bind(col),
|
|
756
889
|
find: wrapReads ? async (filter) => {
|
|
757
890
|
const docs = await col.find(filter);
|
|
758
|
-
const
|
|
759
|
-
await persistAll(docs,
|
|
760
|
-
return
|
|
891
|
+
const normalized = docs.map(normalizeRead);
|
|
892
|
+
await persistAll(docs, normalized);
|
|
893
|
+
return normalized.map((n) => validateRead(n.value));
|
|
761
894
|
} : col.find.bind(col),
|
|
762
895
|
findOne: wrapReads ? async (filter) => {
|
|
763
896
|
const doc = await col.findOne(filter);
|
|
764
897
|
if (doc === null) return null;
|
|
765
|
-
const
|
|
766
|
-
await persistAll([doc], [
|
|
767
|
-
return validateRead(
|
|
898
|
+
const normalized = normalizeRead(doc);
|
|
899
|
+
await persistAll([doc], [normalized]);
|
|
900
|
+
return validateRead(normalized.value);
|
|
768
901
|
} : col.findOne.bind(col),
|
|
902
|
+
aggregate: wrapReads ? async (pipeline) => {
|
|
903
|
+
const docs = await col.aggregate(pipeline);
|
|
904
|
+
return pipelinePreservesDocuments(pipeline) ? normalizeViewRows(docs) : docs;
|
|
905
|
+
} : col.aggregate.bind(col),
|
|
769
906
|
// Live queries feed every @taladb/react hook (useFind, useFindOne,
|
|
770
907
|
// useQueries). Leaving them unwrapped meant React components received the
|
|
771
908
|
// un-migrated shape while a direct find() returned the migrated one.
|
|
772
909
|
subscribe: wrapReads ? (filter, callback, onError) => col.subscribe(
|
|
773
910
|
filter,
|
|
774
911
|
(docs) => {
|
|
775
|
-
const
|
|
912
|
+
const normalized = docs.map(normalizeRead);
|
|
776
913
|
let out;
|
|
777
914
|
try {
|
|
778
|
-
out =
|
|
915
|
+
out = normalized.map((n) => validateRead(n.value));
|
|
779
916
|
} catch (err) {
|
|
780
917
|
onError?.(err);
|
|
781
918
|
return;
|
|
782
919
|
}
|
|
783
920
|
callback(out);
|
|
784
|
-
void persistAll(docs,
|
|
921
|
+
void persistAll(docs, normalized);
|
|
785
922
|
},
|
|
786
923
|
onError
|
|
787
|
-
) : col.subscribe.bind(col)
|
|
924
|
+
) : col.subscribe.bind(col),
|
|
925
|
+
subscribeAggregate: wrapReads ? (pipeline, callback, onError) => col.subscribeAggregate(
|
|
926
|
+
pipeline,
|
|
927
|
+
(docs) => {
|
|
928
|
+
if (!pipelinePreservesDocuments(pipeline)) {
|
|
929
|
+
callback(docs);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
try {
|
|
933
|
+
callback(normalizeViewRows(docs));
|
|
934
|
+
} catch (error) {
|
|
935
|
+
onError?.(error);
|
|
936
|
+
}
|
|
937
|
+
},
|
|
938
|
+
onError
|
|
939
|
+
) : col.subscribeAggregate.bind(col)
|
|
788
940
|
};
|
|
789
941
|
}
|
|
790
942
|
function detectPlatform() {
|
|
@@ -994,6 +1146,30 @@ async function createBrowserDB(dbName, config, passphrase, migrations) {
|
|
|
994
1146
|
});
|
|
995
1147
|
return JSON.parse(json);
|
|
996
1148
|
},
|
|
1149
|
+
searchText: async (field, query, topK, filter, options) => {
|
|
1150
|
+
const json = await proxy.send("searchText", {
|
|
1151
|
+
collection: name,
|
|
1152
|
+
field,
|
|
1153
|
+
query,
|
|
1154
|
+
topK,
|
|
1155
|
+
filterJson: filter ? JSON.stringify(filter) : "null",
|
|
1156
|
+
optionsJson: options ? JSON.stringify(options) : "null"
|
|
1157
|
+
});
|
|
1158
|
+
return JSON.parse(json);
|
|
1159
|
+
},
|
|
1160
|
+
hybridSearch: async (text, vector, topK, filter, options) => {
|
|
1161
|
+
const json = await proxy.send("hybridSearch", {
|
|
1162
|
+
collection: name,
|
|
1163
|
+
textField: text.textField,
|
|
1164
|
+
text: text.text,
|
|
1165
|
+
vectorField: vector.vectorField,
|
|
1166
|
+
vectorJson: JSON.stringify(vector.vector),
|
|
1167
|
+
topK,
|
|
1168
|
+
filterJson: filter ? JSON.stringify(filter) : "null",
|
|
1169
|
+
optionsJson: options ? JSON.stringify(options) : "null"
|
|
1170
|
+
});
|
|
1171
|
+
return JSON.parse(json);
|
|
1172
|
+
},
|
|
997
1173
|
subscribe: (filter, callback, onError) => nudgedPoller(
|
|
998
1174
|
() => proxy.send("find", {
|
|
999
1175
|
collection: name,
|
|
@@ -1138,6 +1314,12 @@ async function createNodeDB(dbName, config, passphrase, migrations) {
|
|
|
1138
1314
|
const raw = await col.findNearest(field, vector, topK, filter ?? null);
|
|
1139
1315
|
return raw;
|
|
1140
1316
|
},
|
|
1317
|
+
searchText: async (field, query, topK, filter, options) => {
|
|
1318
|
+
return col.searchText(field, query, topK, filter ?? null, options ?? null);
|
|
1319
|
+
},
|
|
1320
|
+
hybridSearch: async (text, vector, topK, filter, options) => {
|
|
1321
|
+
return col.hybridSearch(text.textField, text.text, vector.vectorField, vector.vector, topK, filter ?? null, options ?? null);
|
|
1322
|
+
},
|
|
1141
1323
|
subscribe: (filter, callback, onError) => makePoller(async () => col.find(filter ?? null), callback, onError),
|
|
1142
1324
|
subscribeAggregate: (pipeline, callback, onError) => makePoller(async () => wrapped.aggregate(pipeline), callback, onError)
|
|
1143
1325
|
};
|
|
@@ -1220,6 +1402,18 @@ async function createNativeDB(_dbName, migrations) {
|
|
|
1220
1402
|
const raw = native.findNearest(name, field, vector, topK, filter ?? null);
|
|
1221
1403
|
return raw;
|
|
1222
1404
|
},
|
|
1405
|
+
searchText: async (field, query, topK, filter, options) => {
|
|
1406
|
+
if (!native.searchText) {
|
|
1407
|
+
throw new Error("searchText requires @taladb/react-native \u2265 0.10 \u2014 rebuild the native module");
|
|
1408
|
+
}
|
|
1409
|
+
return native.searchText(name, field, query, topK, filter ?? null, options ?? null);
|
|
1410
|
+
},
|
|
1411
|
+
hybridSearch: async (text, vector, topK, filter, options) => {
|
|
1412
|
+
if (!native.hybridSearch) {
|
|
1413
|
+
throw new Error("hybridSearch requires @taladb/react-native \u2265 0.10 \u2014 rebuild the native module");
|
|
1414
|
+
}
|
|
1415
|
+
return native.hybridSearch(name, text.textField, text.text, vector.vectorField, vector.vector, topK, filter ?? null, options ?? null);
|
|
1416
|
+
},
|
|
1223
1417
|
subscribe: (filter, callback, onError) => makePoller(async () => native.find(name, filter ?? {}), callback, onError),
|
|
1224
1418
|
subscribeAggregate: (pipeline, callback, onError) => makePoller(async () => native.aggregate(name, pipeline), callback, onError)
|
|
1225
1419
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taladb",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "The embedded vector database for on-device AI — documents, similarity search, and offline sync for browser, React Native, and Node.js",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -36,9 +36,14 @@
|
|
|
36
36
|
"embeddings",
|
|
37
37
|
"semantic-search",
|
|
38
38
|
"ai",
|
|
39
|
-
"
|
|
39
|
+
"rag",
|
|
40
|
+
"vector-database",
|
|
41
|
+
"embedded-database",
|
|
42
|
+
"on-device-ai",
|
|
43
|
+
"knn",
|
|
44
|
+
"hnsw"
|
|
40
45
|
],
|
|
41
|
-
"license": "
|
|
46
|
+
"license": "Apache-2.0",
|
|
42
47
|
"repository": {
|
|
43
48
|
"type": "git",
|
|
44
49
|
"url": "https://github.com/thinkgrid-labs/taladb.git",
|