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/dist/index.mjs CHANGED
@@ -716,18 +716,52 @@ function deepEqual(a, b) {
716
716
  );
717
717
  }
718
718
  function applySchema(col, options) {
719
- const { schema, validateOnRead = false, migrateDocument, syncSchema, persistMigrations = false } = options;
719
+ const {
720
+ schema,
721
+ validateOnRead = false,
722
+ migrateDocument,
723
+ downgradeDocument,
724
+ syncSchema,
725
+ persistMigrations = false,
726
+ allowFieldRemoval = false,
727
+ retiredFields = []
728
+ } = options;
720
729
  const targetVersion = syncSchema?.version ?? 0;
721
730
  if (migrateDocument && targetVersion < 1) {
722
731
  throw new Error("CollectionOptions.migrateDocument requires syncSchema.version (the migration target)");
723
732
  }
733
+ if (downgradeDocument && targetVersion < 1) {
734
+ throw new Error("CollectionOptions.downgradeDocument requires syncSchema.version (the shape this build reads)");
735
+ }
724
736
  if (syncSchema && targetVersion < 1 && (syncSchema.renames || syncSchema.defaults)) {
725
737
  throw new Error(
726
738
  "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"
727
739
  );
728
740
  }
729
741
  const stampVersion = targetVersion > 0;
730
- if (!schema && !migrateDocument && !stampVersion) return col;
742
+ if (!schema && !migrateDocument && !downgradeDocument && !stampVersion) return col;
743
+ const retired = new Set(retiredFields);
744
+ const engineOwned = /* @__PURE__ */ new Set([
745
+ "_id",
746
+ "_v",
747
+ "_changed_at",
748
+ "_remote",
749
+ "_remote_rev",
750
+ "_replica_scope"
751
+ ]);
752
+ const downcastViews = /* @__PURE__ */ new WeakSet();
753
+ function preserveFields(original, next, preserveUnknown, preserveVersion = true) {
754
+ let out = null;
755
+ for (const k of Object.keys(original)) {
756
+ const ownedField = engineOwned.has(k) && (k !== "_v" || preserveVersion);
757
+ const mustRestore = ownedField || preserveUnknown && !retired.has(k) && !(k in next);
758
+ if (!mustRestore) continue;
759
+ if (!ownedField && k in next) continue;
760
+ out ?? (out = { ...next });
761
+ out[k] = original[k];
762
+ }
763
+ return out ?? next;
764
+ }
731
765
  function parseWrite(doc, label) {
732
766
  try {
733
767
  return schema.parse(doc);
@@ -735,6 +769,43 @@ function applySchema(col, options) {
735
769
  throw new TalaDbValidationError(err, label);
736
770
  }
737
771
  }
772
+ function assertWritableDocument(doc, label) {
773
+ if (doc && typeof doc === "object" && downcastViews.has(doc)) {
774
+ throw new Error(`${label}: a downgradeDocument result is a read-only compatibility view`);
775
+ }
776
+ const version = doc?._v;
777
+ if (targetVersion > 0 && typeof version === "number" && version > targetVersion) {
778
+ throw new Error(
779
+ `${label}: this client supports schema v${targetVersion}, but the document is v${version}`
780
+ );
781
+ }
782
+ }
783
+ function writableFilter(filter) {
784
+ if (targetVersion < 1) return filter;
785
+ return {
786
+ $and: [
787
+ filter,
788
+ {
789
+ $or: [
790
+ { _v: { $exists: false } },
791
+ { _v: { $lte: targetVersion } }
792
+ ]
793
+ }
794
+ ]
795
+ };
796
+ }
797
+ function assertSafeUpdate(update) {
798
+ const record = update;
799
+ for (const op of ["$set", "$unset", "$inc", "$push", "$pull"]) {
800
+ const fields = record[op];
801
+ if (!fields) continue;
802
+ for (const field of Object.keys(fields)) {
803
+ if (engineOwned.has(field)) {
804
+ throw new Error(`update cannot modify engine-owned field '${field}'`);
805
+ }
806
+ }
807
+ }
808
+ }
738
809
  function stamp(doc) {
739
810
  if (!stampVersion || doc._v !== void 0) return doc;
740
811
  return { ...doc, _v: targetVersion };
@@ -750,50 +821,86 @@ function applySchema(col, options) {
750
821
  if (k === "_id") continue;
751
822
  if (!deepEqual(migrated[k], original[k])) $set[k] = migrated[k];
752
823
  }
753
- for (const k of Object.keys(original)) {
754
- if (k !== "_id" && !(k in migrated)) $unset[k] = true;
824
+ if (allowFieldRemoval || retired.size > 0) {
825
+ for (const k of Object.keys(original)) {
826
+ if (k !== "_id" && !(k in migrated) && (allowFieldRemoval || retired.has(k))) $unset[k] = true;
827
+ }
755
828
  }
756
829
  const update = {};
757
830
  if (Object.keys($set).length) update.$set = $set;
758
831
  if (Object.keys($unset).length) update.$unset = $unset;
759
832
  return Object.keys(update).length ? update : null;
760
833
  }
761
- function migrateRead(doc) {
762
- if (!migrateDocument) return doc;
834
+ function normalizeRead(doc) {
763
835
  const fromVersion = typeof doc._v === "number" ? doc._v : 0;
764
- if (fromVersion >= targetVersion) return doc;
765
- return { ...migrateDocument(doc, fromVersion), _v: targetVersion };
836
+ if (migrateDocument && fromVersion < targetVersion) {
837
+ const up = { ...migrateDocument(doc, fromVersion), _v: targetVersion };
838
+ return {
839
+ // The migration owns the version transition, while every other
840
+ // engine-owned field continues to come from the stored document.
841
+ value: allowFieldRemoval ? preserveFields(doc, up, false, false) : preserveFields(doc, up, true, false),
842
+ persistable: true
843
+ };
844
+ }
845
+ if (downgradeDocument && fromVersion > targetVersion) {
846
+ const projected = {
847
+ ...downgradeDocument(doc, fromVersion),
848
+ ...doc._id !== void 0 ? { _id: doc._id } : {},
849
+ _v: fromVersion
850
+ };
851
+ downcastViews.add(projected);
852
+ return { value: projected, persistable: false };
853
+ }
854
+ return { value: doc, persistable: true };
766
855
  }
767
856
  function validateRead(doc) {
768
857
  if (!validateOnRead || !schema) return doc;
769
858
  try {
770
- return schema.parse(doc);
859
+ const parsed = schema.parse(doc);
860
+ const fromVersion = typeof doc._v === "number" ? doc._v : 0;
861
+ return preserveFields(doc, parsed, targetVersion > 0 && fromVersion > targetVersion);
771
862
  } catch (err) {
772
863
  throw new TalaDbValidationError(err, "read");
773
864
  }
774
865
  }
775
- async function persistAll(originals, migrated) {
866
+ async function persistAll(originals, normalized) {
776
867
  if (!persistMigrations) return;
777
868
  for (let i = 0; i < originals.length; i++) {
778
869
  const original = originals[i];
779
- if (migrated[i] === original || typeof original._id !== "string") continue;
780
- const update = diffUpdate(original, migrated[i]);
870
+ if (!normalized[i].persistable) continue;
871
+ const migrated = normalized[i].value;
872
+ if (migrated === original || typeof original._id !== "string") continue;
873
+ const update = diffUpdate(original, migrated);
781
874
  if (!update) continue;
782
875
  try {
783
- await col.updateOne({ _id: original._id }, update);
876
+ const guards = [{ _id: original._id }];
877
+ if (original._v === void 0) guards.push({ _v: { $exists: false } });
878
+ else guards.push({ _v: original._v });
879
+ if (original._changed_at !== void 0) {
880
+ guards.push({ _changed_at: original._changed_at });
881
+ }
882
+ await col.updateOne({ $and: guards }, update);
784
883
  } catch {
785
884
  }
786
885
  }
787
886
  }
788
- const wrapReads = Boolean(migrateDocument) || validateOnRead && Boolean(schema);
887
+ const wrapReads = Boolean(migrateDocument) || Boolean(downgradeDocument) || validateOnRead && Boolean(schema);
789
888
  const wrapWrites = Boolean(schema) || stampVersion;
889
+ function pipelinePreservesDocuments(pipeline) {
890
+ return pipeline.every((stage) => !("$group" in stage) && !("$project" in stage));
891
+ }
892
+ function normalizeViewRows(docs) {
893
+ return docs.map((doc) => validateRead(normalizeRead(doc).value));
894
+ }
790
895
  return {
791
896
  ...col,
792
897
  insert: wrapWrites ? async (doc) => {
898
+ assertWritableDocument(doc, "insert");
793
899
  if (schema) parseWrite(doc, "insert");
794
900
  return col.insert(stamp(doc));
795
901
  } : col.insert.bind(col),
796
902
  insertMany: wrapWrites ? async (docs) => {
903
+ docs.forEach((doc, i) => assertWritableDocument(doc, `insertMany[${i}]`));
797
904
  if (schema) docs.forEach((doc, i) => parseWrite(doc, `insertMany[${i}]`));
798
905
  return col.insertMany(docs.map(stamp));
799
906
  } : col.insertMany.bind(col),
@@ -802,6 +909,14 @@ function applySchema(col, options) {
802
909
  // runtime schema check have to be the same seam, or a malformed server
803
910
  // response walks straight into a typed collection.
804
911
  replaceManyWithIds: wrapWrites ? async (docs, origin) => {
912
+ if (origin !== "remote") {
913
+ docs.forEach((doc, i) => assertWritableDocument(doc, `replaceManyWithIds[${i}]`));
914
+ if (targetVersion > 0) {
915
+ throw new Error(
916
+ "local replaceManyWithIds is disabled on versioned collections; use updateOne/updateMany so schema-version guards are atomic"
917
+ );
918
+ }
919
+ }
805
920
  if (schema) docs.forEach((doc, i) => {
806
921
  const { _replica_scope, _remote_rev, ...schemaDoc } = doc;
807
922
  void _replica_scope;
@@ -810,38 +925,75 @@ function applySchema(col, options) {
810
925
  });
811
926
  return col.replaceManyWithIds(docs.map((d) => stampDoc(d)), origin);
812
927
  } : col.replaceManyWithIds.bind(col),
928
+ deleteManyWithIds: stampVersion ? async (ids, origin) => {
929
+ if (origin !== "remote") {
930
+ throw new Error(
931
+ "local deleteManyWithIds is disabled on versioned collections; use deleteOne/deleteMany so schema-version guards are atomic"
932
+ );
933
+ }
934
+ return col.deleteManyWithIds(ids, origin);
935
+ } : col.deleteManyWithIds.bind(col),
936
+ updateOne: wrapWrites ? async (filter, update) => {
937
+ assertSafeUpdate(update);
938
+ return col.updateOne(writableFilter(filter), update);
939
+ } : col.updateOne.bind(col),
940
+ updateMany: wrapWrites ? async (filter, update) => {
941
+ assertSafeUpdate(update);
942
+ return col.updateMany(writableFilter(filter), update);
943
+ } : col.updateMany.bind(col),
944
+ deleteOne: stampVersion ? (filter) => col.deleteOne(writableFilter(filter)) : col.deleteOne.bind(col),
945
+ deleteMany: stampVersion ? (filter) => col.deleteMany(writableFilter(filter)) : col.deleteMany.bind(col),
813
946
  find: wrapReads ? async (filter) => {
814
947
  const docs = await col.find(filter);
815
- const migrated = docs.map(migrateRead);
816
- await persistAll(docs, migrated);
817
- return migrated.map(validateRead);
948
+ const normalized = docs.map(normalizeRead);
949
+ await persistAll(docs, normalized);
950
+ return normalized.map((n) => validateRead(n.value));
818
951
  } : col.find.bind(col),
819
952
  findOne: wrapReads ? async (filter) => {
820
953
  const doc = await col.findOne(filter);
821
954
  if (doc === null) return null;
822
- const migrated = migrateRead(doc);
823
- await persistAll([doc], [migrated]);
824
- return validateRead(migrated);
955
+ const normalized = normalizeRead(doc);
956
+ await persistAll([doc], [normalized]);
957
+ return validateRead(normalized.value);
825
958
  } : col.findOne.bind(col),
959
+ aggregate: wrapReads ? async (pipeline) => {
960
+ const docs = await col.aggregate(pipeline);
961
+ return pipelinePreservesDocuments(pipeline) ? normalizeViewRows(docs) : docs;
962
+ } : col.aggregate.bind(col),
826
963
  // Live queries feed every @taladb/react hook (useFind, useFindOne,
827
964
  // useQueries). Leaving them unwrapped meant React components received the
828
965
  // un-migrated shape while a direct find() returned the migrated one.
829
966
  subscribe: wrapReads ? (filter, callback, onError) => col.subscribe(
830
967
  filter,
831
968
  (docs) => {
832
- const migrated = docs.map(migrateRead);
969
+ const normalized = docs.map(normalizeRead);
833
970
  let out;
834
971
  try {
835
- out = migrated.map(validateRead);
972
+ out = normalized.map((n) => validateRead(n.value));
836
973
  } catch (err) {
837
974
  onError?.(err);
838
975
  return;
839
976
  }
840
977
  callback(out);
841
- void persistAll(docs, migrated);
978
+ void persistAll(docs, normalized);
842
979
  },
843
980
  onError
844
- ) : col.subscribe.bind(col)
981
+ ) : col.subscribe.bind(col),
982
+ subscribeAggregate: wrapReads ? (pipeline, callback, onError) => col.subscribeAggregate(
983
+ pipeline,
984
+ (docs) => {
985
+ if (!pipelinePreservesDocuments(pipeline)) {
986
+ callback(docs);
987
+ return;
988
+ }
989
+ try {
990
+ callback(normalizeViewRows(docs));
991
+ } catch (error) {
992
+ onError?.(error);
993
+ }
994
+ },
995
+ onError
996
+ ) : col.subscribeAggregate.bind(col)
845
997
  };
846
998
  }
847
999
  function detectPlatform() {
@@ -1051,6 +1203,30 @@ async function createBrowserDB(dbName, config, passphrase, migrations) {
1051
1203
  });
1052
1204
  return JSON.parse(json);
1053
1205
  },
1206
+ searchText: async (field, query, topK, filter, options) => {
1207
+ const json = await proxy.send("searchText", {
1208
+ collection: name,
1209
+ field,
1210
+ query,
1211
+ topK,
1212
+ filterJson: filter ? JSON.stringify(filter) : "null",
1213
+ optionsJson: options ? JSON.stringify(options) : "null"
1214
+ });
1215
+ return JSON.parse(json);
1216
+ },
1217
+ hybridSearch: async (text, vector, topK, filter, options) => {
1218
+ const json = await proxy.send("hybridSearch", {
1219
+ collection: name,
1220
+ textField: text.textField,
1221
+ text: text.text,
1222
+ vectorField: vector.vectorField,
1223
+ vectorJson: JSON.stringify(vector.vector),
1224
+ topK,
1225
+ filterJson: filter ? JSON.stringify(filter) : "null",
1226
+ optionsJson: options ? JSON.stringify(options) : "null"
1227
+ });
1228
+ return JSON.parse(json);
1229
+ },
1054
1230
  subscribe: (filter, callback, onError) => nudgedPoller(
1055
1231
  () => proxy.send("find", {
1056
1232
  collection: name,
@@ -1195,6 +1371,12 @@ async function createNodeDB(dbName, config, passphrase, migrations) {
1195
1371
  const raw = await col.findNearest(field, vector, topK, filter ?? null);
1196
1372
  return raw;
1197
1373
  },
1374
+ searchText: async (field, query, topK, filter, options) => {
1375
+ return col.searchText(field, query, topK, filter ?? null, options ?? null);
1376
+ },
1377
+ hybridSearch: async (text, vector, topK, filter, options) => {
1378
+ return col.hybridSearch(text.textField, text.text, vector.vectorField, vector.vector, topK, filter ?? null, options ?? null);
1379
+ },
1198
1380
  subscribe: (filter, callback, onError) => makePoller(async () => col.find(filter ?? null), callback, onError),
1199
1381
  subscribeAggregate: (pipeline, callback, onError) => makePoller(async () => wrapped.aggregate(pipeline), callback, onError)
1200
1382
  };
@@ -1277,6 +1459,18 @@ async function createNativeDB(_dbName, migrations) {
1277
1459
  const raw = native.findNearest(name, field, vector, topK, filter ?? null);
1278
1460
  return raw;
1279
1461
  },
1462
+ searchText: async (field, query, topK, filter, options) => {
1463
+ if (!native.searchText) {
1464
+ throw new Error("searchText requires @taladb/react-native \u2265 0.10 \u2014 rebuild the native module");
1465
+ }
1466
+ return native.searchText(name, field, query, topK, filter ?? null, options ?? null);
1467
+ },
1468
+ hybridSearch: async (text, vector, topK, filter, options) => {
1469
+ if (!native.hybridSearch) {
1470
+ throw new Error("hybridSearch requires @taladb/react-native \u2265 0.10 \u2014 rebuild the native module");
1471
+ }
1472
+ return native.hybridSearch(name, text.textField, text.text, vector.vectorField, vector.vector, topK, filter ?? null, options ?? null);
1473
+ },
1280
1474
  subscribe: (filter, callback, onError) => makePoller(async () => native.find(name, filter ?? {}), callback, onError),
1281
1475
  subscribeAggregate: (pipeline, callback, onError) => makePoller(async () => native.aggregate(name, pipeline), callback, onError)
1282
1476
  };