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.
@@ -659,18 +659,52 @@ function deepEqual(a, b) {
659
659
  );
660
660
  }
661
661
  function applySchema(col, options) {
662
- const { schema, validateOnRead = false, migrateDocument, syncSchema, persistMigrations = false } = options;
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
- for (const k of Object.keys(original)) {
697
- if (k !== "_id" && !(k in migrated)) $unset[k] = true;
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 migrateRead(doc) {
705
- if (!migrateDocument) return doc;
777
+ function normalizeRead(doc) {
706
778
  const fromVersion = typeof doc._v === "number" ? doc._v : 0;
707
- if (fromVersion >= targetVersion) return doc;
708
- return { ...migrateDocument(doc, fromVersion), _v: targetVersion };
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
- return schema.parse(doc);
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, migrated) {
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 (migrated[i] === original || typeof original._id !== "string") continue;
723
- const update = diffUpdate(original, migrated[i]);
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
- await col.updateOne({ _id: original._id }, update);
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 migrated = docs.map(migrateRead);
759
- await persistAll(docs, migrated);
760
- return migrated.map(validateRead);
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 migrated = migrateRead(doc);
766
- await persistAll([doc], [migrated]);
767
- return validateRead(migrated);
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 migrated = docs.map(migrateRead);
912
+ const normalized = docs.map(normalizeRead);
776
913
  let out;
777
914
  try {
778
- out = migrated.map(validateRead);
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, migrated);
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.9.4",
4
- "description": "Local-first document and vector database for React, React Native, and Node.js",
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
- "hybrid-search"
39
+ "rag",
40
+ "vector-database",
41
+ "embedded-database",
42
+ "on-device-ai",
43
+ "knn",
44
+ "hnsw"
40
45
  ],
41
- "license": "MIT",
46
+ "license": "Apache-2.0",
42
47
  "repository": {
43
48
  "type": "git",
44
49
  "url": "https://github.com/thinkgrid-labs/taladb.git",