@signaldb/core 1.3.1 → 1.5.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.
@@ -41,14 +41,17 @@ interface CollectionEvents<T extends BaseItem, U = T> {
41
41
  'insert': (item: Omit<T, 'id'> & Partial<Pick<T, 'id'>>) => void;
42
42
  'updateOne': (selector: Selector<T>, modifier: Modifier<T>) => void;
43
43
  'updateMany': (selector: Selector<T>, modifier: Modifier<T>) => void;
44
+ 'replaceOne': (selector: Selector<T>, item: Omit<T, 'id'> & Partial<Pick<T, 'id'>>) => void;
44
45
  'removeOne': (selector: Selector<T>) => void;
45
46
  'removeMany': (selector: Selector<T>) => void;
47
+ 'validate': (item: T) => void;
46
48
  '_debug.getItems': (callstack: string, selector: Selector<T> | undefined, measuredTime: number) => void;
47
49
  '_debug.find': <O extends FindOptions<T>>(callstack: string, selector: Selector<T> | undefined, options: O | undefined, cursor: Cursor<T, U>) => void;
48
50
  '_debug.findOne': <O extends FindOptions<T>>(callstack: string, selector: Selector<T>, options: O | undefined, item: U | undefined) => void;
49
51
  '_debug.insert': (callstack: string, item: Omit<T, 'id'> & Partial<Pick<T, 'id'>>) => void;
50
52
  '_debug.updateOne': (callstack: string, selector: Selector<T>, modifier: Modifier<T>) => void;
51
53
  '_debug.updateMany': (callstack: string, selector: Selector<T>, modifier: Modifier<T>) => void;
54
+ '_debug.replaceOne': (callstack: string, selector: Selector<T>, item: Omit<T, 'id'> & Partial<Pick<T, 'id'>>) => void;
52
55
  '_debug.removeOne': (callstack: string, selector: Selector<T>) => void;
53
56
  '_debug.removeMany': (callstack: string, selector: Selector<T>) => void;
54
57
  }
@@ -229,18 +232,38 @@ export default class Collection<T extends BaseItem<I> = BaseItem, I = any, U = T
229
232
  * Updates a single item in the collection that matches the given selector.
230
233
  * @param selector - The criteria to select the item to update.
231
234
  * @param modifier - The modifications to apply to the item.
235
+ * @param [options] - Optional settings for the update operation.
236
+ * @param [options.upsert] - If `true`, creates a new item if no item matches the selector.
232
237
  * @returns The number of items updated (0 or 1).
233
238
  * @throws {Error} If the collection is disposed or invalid arguments are provided.
234
239
  */
235
- updateOne(selector: Selector<T>, modifier: Modifier<T>): 0 | 1;
240
+ updateOne(selector: Selector<T>, modifier: Modifier<T>, options?: {
241
+ upsert?: boolean;
242
+ }): 0 | 1;
236
243
  /**
237
244
  * Updates multiple items in the collection that match the given selector.
238
245
  * @param selector - The criteria to select the items to update.
239
246
  * @param modifier - The modifications to apply to the items.
247
+ * @param [options] - Optional settings for the update operation.
248
+ * @param [options.upsert] - If `true`, creates new items if no items match the selector.
240
249
  * @returns The number of items updated.
241
250
  * @throws {Error} If the collection is disposed or invalid arguments are provided.
242
251
  */
243
- updateMany(selector: Selector<T>, modifier: Modifier<T>): number;
252
+ updateMany(selector: Selector<T>, modifier: Modifier<T>, options?: {
253
+ upsert?: boolean;
254
+ }): number;
255
+ /**
256
+ * Replaces a single item in the collection that matches the given selector.
257
+ * @param selector - The criteria to select the item to replace.
258
+ * @param replacement - The item to replace the selected item with.
259
+ * @param [options] - Optional settings for the replace operation.
260
+ * @param [options.upsert] - If `true`, creates a new item if no item matches the selector.
261
+ * @returns The number of items replaced (0 or 1).
262
+ * @throws {Error} If the collection is disposed or invalid arguments are provided.
263
+ */
264
+ replaceOne(selector: Selector<T>, replacement: Omit<T, 'id'> & Partial<Pick<T, 'id'>>, options?: {
265
+ upsert?: boolean;
266
+ }): 0 | 1;
244
267
  /**
245
268
  * Removes a single item from the collection that matches the given selector.
246
269
  * @param selector - The criteria to select the item to remove.
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  const mingo = require("mingo");
3
3
  function modify(item, modifier) {
4
+ const hasOperators = Object.keys(modifier).some((key) => key.startsWith("$"));
5
+ if (!hasOperators)
6
+ return modifier;
4
7
  const clonedItem = { ...item };
5
8
  mingo.update(clonedItem, modifier);
6
9
  return clonedItem;
@@ -530,6 +530,7 @@ const _Collection = class _Collection extends EventEmitter {
530
530
  if (!item)
531
531
  throw new Error("Invalid item");
532
532
  const newItem = { id: randomId(), ...item };
533
+ this.emit("validate", newItem);
533
534
  if (this.idIndex.has(serializeValue(newItem.id)))
534
535
  throw new Error("Item with same id already exists");
535
536
  this.memory().push(newItem);
@@ -567,61 +568,143 @@ const _Collection = class _Collection extends EventEmitter {
567
568
  * Updates a single item in the collection that matches the given selector.
568
569
  * @param selector - The criteria to select the item to update.
569
570
  * @param modifier - The modifications to apply to the item.
571
+ * @param [options] - Optional settings for the update operation.
572
+ * @param [options.upsert] - If `true`, creates a new item if no item matches the selector.
570
573
  * @returns The number of items updated (0 or 1).
571
574
  * @throws {Error} If the collection is disposed or invalid arguments are provided.
572
575
  */
573
- updateOne(selector, modifier) {
576
+ updateOne(selector, modifier, options) {
574
577
  if (this.isDisposed)
575
578
  throw new Error("Collection is disposed");
576
579
  if (!selector)
577
580
  throw new Error("Invalid selector");
578
581
  if (!modifier)
579
582
  throw new Error("Invalid modifier");
583
+ const { $setOnInsert, ...restModifier } = modifier;
580
584
  const { item, index } = this.getItemAndIndex(selector);
581
- if (item == null)
582
- return 0;
583
- const modifiedItem = modify(deepClone.default(item), modifier);
584
- const existingItem = this.findOne({ id: modifiedItem.id }, { reactive: false });
585
- if (!isEqual(existingItem, { ...existingItem, id: modifiedItem.id }))
586
- throw new Error("Item with same id already exists");
587
- this.memory().splice(index, 1, modifiedItem);
588
- this.rebuildIndices();
589
- this.emit("changed", modifiedItem, modifier);
585
+ if (item == null) {
586
+ if (options == null ? void 0 : options.upsert) {
587
+ const newItem = modify({}, {
588
+ ...restModifier,
589
+ $set: {
590
+ ...$setOnInsert,
591
+ ...restModifier.$set
592
+ }
593
+ });
594
+ if (newItem.id != null && this.getItemAndIndex({ id: newItem.id }).item != null) {
595
+ throw new Error("Item with same id already exists");
596
+ }
597
+ this.insert(newItem);
598
+ }
599
+ } else {
600
+ const modifiedItem = modify(deepClone.default(item), restModifier);
601
+ if (item.id !== modifiedItem.id && this.getItemAndIndex({ id: modifiedItem.id }).item != null) {
602
+ throw new Error("Item with same id already exists");
603
+ }
604
+ this.emit("validate", modifiedItem);
605
+ this.memory().splice(index, 1, modifiedItem);
606
+ this.rebuildIndices();
607
+ this.emit("changed", modifiedItem, restModifier);
608
+ }
590
609
  this.emit("updateOne", selector, modifier);
591
610
  this.executeInDebugMode((callstack) => this.emit("_debug.updateOne", callstack, selector, modifier));
611
+ if (item == null && !(options == null ? void 0 : options.upsert))
612
+ return 0;
592
613
  return 1;
593
614
  }
594
615
  /**
595
616
  * Updates multiple items in the collection that match the given selector.
596
617
  * @param selector - The criteria to select the items to update.
597
618
  * @param modifier - The modifications to apply to the items.
619
+ * @param [options] - Optional settings for the update operation.
620
+ * @param [options.upsert] - If `true`, creates new items if no items match the selector.
598
621
  * @returns The number of items updated.
599
622
  * @throws {Error} If the collection is disposed or invalid arguments are provided.
600
623
  */
601
- updateMany(selector, modifier) {
624
+ updateMany(selector, modifier, options) {
602
625
  if (this.isDisposed)
603
626
  throw new Error("Collection is disposed");
604
627
  if (!selector)
605
628
  throw new Error("Invalid selector");
606
629
  if (!modifier)
607
630
  throw new Error("Invalid modifier");
631
+ const { $setOnInsert, ...restModifier } = modifier;
608
632
  const items = this.getItems(selector);
609
- const modifiedItems = [];
610
- items.forEach((item) => {
633
+ if (items.length === 0 && (options == null ? void 0 : options.upsert)) {
634
+ const newItem = modify({}, {
635
+ ...restModifier,
636
+ $set: {
637
+ ...$setOnInsert,
638
+ ...restModifier.$set
639
+ }
640
+ });
641
+ if (newItem.id != null && this.getItemAndIndex({ id: newItem.id }).item != null) {
642
+ throw new Error("Item with same id already exists");
643
+ }
644
+ this.insert(newItem);
645
+ }
646
+ const changes = items.map((item) => {
611
647
  const { index } = this.getItemAndIndex({ id: item.id });
612
648
  if (index === -1)
613
- throw new Error("Cannot resolve index for item");
614
- const modifiedItem = modify(deepClone.default(item), modifier);
615
- this.memory().splice(index, 1, modifiedItem);
616
- modifiedItems.push(modifiedItem);
649
+ throw new Error(`Cannot resolve index for item with id '${item.id}'`);
650
+ const modifiedItem = modify(deepClone.default(item), restModifier);
651
+ if (item.id !== modifiedItem.id && this.getItemAndIndex({ id: modifiedItem.id }).item != null) {
652
+ throw new Error(`Item with same id '${modifiedItem.id}' already exists`);
653
+ }
654
+ this.emit("validate", modifiedItem);
655
+ return {
656
+ item: modifiedItem,
657
+ index
658
+ };
659
+ });
660
+ changes.forEach(({ item, index }) => {
661
+ this.memory().splice(index, 1, item);
617
662
  });
618
663
  this.rebuildIndices();
619
- modifiedItems.forEach((modifiedItem) => {
620
- this.emit("changed", modifiedItem, modifier);
664
+ changes.forEach(({ item }) => {
665
+ this.emit("changed", item, restModifier);
621
666
  });
622
667
  this.emit("updateMany", selector, modifier);
623
668
  this.executeInDebugMode((callstack) => this.emit("_debug.updateMany", callstack, selector, modifier));
624
- return modifiedItems.length;
669
+ return changes.length === 0 && (options == null ? void 0 : options.upsert) ? 1 : changes.length;
670
+ }
671
+ /**
672
+ * Replaces a single item in the collection that matches the given selector.
673
+ * @param selector - The criteria to select the item to replace.
674
+ * @param replacement - The item to replace the selected item with.
675
+ * @param [options] - Optional settings for the replace operation.
676
+ * @param [options.upsert] - If `true`, creates a new item if no item matches the selector.
677
+ * @returns The number of items replaced (0 or 1).
678
+ * @throws {Error} If the collection is disposed or invalid arguments are provided.
679
+ */
680
+ replaceOne(selector, replacement, options) {
681
+ if (this.isDisposed)
682
+ throw new Error("Collection is disposed");
683
+ if (!selector)
684
+ throw new Error("Invalid selector");
685
+ const { item, index } = this.getItemAndIndex(selector);
686
+ if (item == null) {
687
+ if (options == null ? void 0 : options.upsert) {
688
+ if (replacement.id != null && this.getItemAndIndex({ id: replacement.id }).item != null) {
689
+ throw new Error("Item with same id already exists");
690
+ }
691
+ this.insert(replacement);
692
+ }
693
+ } else {
694
+ if (item.id !== replacement.id && this.getItemAndIndex({ id: replacement.id }).item != null) {
695
+ throw new Error("Item with same id already exists");
696
+ }
697
+ const modifiedItem = { id: item.id, ...replacement };
698
+ this.emit("validate", modifiedItem);
699
+ this.memory().splice(index, 1, modifiedItem);
700
+ this.rebuildIndices();
701
+ this.emit("changed", modifiedItem, replacement);
702
+ }
703
+ this.emit("replaceOne", selector, replacement);
704
+ this.executeInDebugMode((callstack) => this.emit("_debug.replaceOne", callstack, selector, replacement));
705
+ if (item == null && !(options == null ? void 0 : options.upsert))
706
+ return 0;
707
+ return 1;
625
708
  }
626
709
  /**
627
710
  * Removes a single item from the collection that matches the given selector.
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export type { default as MemoryAdapter } from './types/MemoryAdapter';
3
3
  export type { default as PersistenceAdapter, Changeset, LoadResponse, } from './types/PersistenceAdapter';
4
4
  export type { default as Selector } from './types/Selector';
5
5
  export type { default as Modifier } from './types/Modifier';
6
- export type { BaseItem, ObserveCallbacks, CursorOptions, Transform, SortSpecifier, FieldSpecifier, FindOptions, } from './Collection';
6
+ export type { BaseItem, ObserveCallbacks, CursorOptions, Transform, SortSpecifier, FieldSpecifier, FindOptions, CollectionOptions, } from './Collection';
7
7
  export { default as Collection, createIndex } from './Collection';
8
8
  export { default as AutoFetchCollection } from './AutoFetchCollection';
9
9
  export { default as combinePersistenceAdapters } from './persistence/combinePersistenceAdapters';
package/dist/index10.mjs CHANGED
@@ -1,5 +1,8 @@
1
1
  import { update } from "mingo";
2
2
  function modify(item, modifier) {
3
+ const hasOperators = Object.keys(modifier).some((key) => key.startsWith("$"));
4
+ if (!hasOperators)
5
+ return modifier;
3
6
  const clonedItem = { ...item };
4
7
  update(clonedItem, modifier);
5
8
  return clonedItem;
package/dist/index2.mjs CHANGED
@@ -529,6 +529,7 @@ const _Collection = class _Collection extends EventEmitter {
529
529
  if (!item)
530
530
  throw new Error("Invalid item");
531
531
  const newItem = { id: randomId(), ...item };
532
+ this.emit("validate", newItem);
532
533
  if (this.idIndex.has(serializeValue(newItem.id)))
533
534
  throw new Error("Item with same id already exists");
534
535
  this.memory().push(newItem);
@@ -566,61 +567,143 @@ const _Collection = class _Collection extends EventEmitter {
566
567
  * Updates a single item in the collection that matches the given selector.
567
568
  * @param selector - The criteria to select the item to update.
568
569
  * @param modifier - The modifications to apply to the item.
570
+ * @param [options] - Optional settings for the update operation.
571
+ * @param [options.upsert] - If `true`, creates a new item if no item matches the selector.
569
572
  * @returns The number of items updated (0 or 1).
570
573
  * @throws {Error} If the collection is disposed or invalid arguments are provided.
571
574
  */
572
- updateOne(selector, modifier) {
575
+ updateOne(selector, modifier, options) {
573
576
  if (this.isDisposed)
574
577
  throw new Error("Collection is disposed");
575
578
  if (!selector)
576
579
  throw new Error("Invalid selector");
577
580
  if (!modifier)
578
581
  throw new Error("Invalid modifier");
582
+ const { $setOnInsert, ...restModifier } = modifier;
579
583
  const { item, index } = this.getItemAndIndex(selector);
580
- if (item == null)
581
- return 0;
582
- const modifiedItem = modify(deepClone(item), modifier);
583
- const existingItem = this.findOne({ id: modifiedItem.id }, { reactive: false });
584
- if (!isEqual(existingItem, { ...existingItem, id: modifiedItem.id }))
585
- throw new Error("Item with same id already exists");
586
- this.memory().splice(index, 1, modifiedItem);
587
- this.rebuildIndices();
588
- this.emit("changed", modifiedItem, modifier);
584
+ if (item == null) {
585
+ if (options == null ? void 0 : options.upsert) {
586
+ const newItem = modify({}, {
587
+ ...restModifier,
588
+ $set: {
589
+ ...$setOnInsert,
590
+ ...restModifier.$set
591
+ }
592
+ });
593
+ if (newItem.id != null && this.getItemAndIndex({ id: newItem.id }).item != null) {
594
+ throw new Error("Item with same id already exists");
595
+ }
596
+ this.insert(newItem);
597
+ }
598
+ } else {
599
+ const modifiedItem = modify(deepClone(item), restModifier);
600
+ if (item.id !== modifiedItem.id && this.getItemAndIndex({ id: modifiedItem.id }).item != null) {
601
+ throw new Error("Item with same id already exists");
602
+ }
603
+ this.emit("validate", modifiedItem);
604
+ this.memory().splice(index, 1, modifiedItem);
605
+ this.rebuildIndices();
606
+ this.emit("changed", modifiedItem, restModifier);
607
+ }
589
608
  this.emit("updateOne", selector, modifier);
590
609
  this.executeInDebugMode((callstack) => this.emit("_debug.updateOne", callstack, selector, modifier));
610
+ if (item == null && !(options == null ? void 0 : options.upsert))
611
+ return 0;
591
612
  return 1;
592
613
  }
593
614
  /**
594
615
  * Updates multiple items in the collection that match the given selector.
595
616
  * @param selector - The criteria to select the items to update.
596
617
  * @param modifier - The modifications to apply to the items.
618
+ * @param [options] - Optional settings for the update operation.
619
+ * @param [options.upsert] - If `true`, creates new items if no items match the selector.
597
620
  * @returns The number of items updated.
598
621
  * @throws {Error} If the collection is disposed or invalid arguments are provided.
599
622
  */
600
- updateMany(selector, modifier) {
623
+ updateMany(selector, modifier, options) {
601
624
  if (this.isDisposed)
602
625
  throw new Error("Collection is disposed");
603
626
  if (!selector)
604
627
  throw new Error("Invalid selector");
605
628
  if (!modifier)
606
629
  throw new Error("Invalid modifier");
630
+ const { $setOnInsert, ...restModifier } = modifier;
607
631
  const items = this.getItems(selector);
608
- const modifiedItems = [];
609
- items.forEach((item) => {
632
+ if (items.length === 0 && (options == null ? void 0 : options.upsert)) {
633
+ const newItem = modify({}, {
634
+ ...restModifier,
635
+ $set: {
636
+ ...$setOnInsert,
637
+ ...restModifier.$set
638
+ }
639
+ });
640
+ if (newItem.id != null && this.getItemAndIndex({ id: newItem.id }).item != null) {
641
+ throw new Error("Item with same id already exists");
642
+ }
643
+ this.insert(newItem);
644
+ }
645
+ const changes = items.map((item) => {
610
646
  const { index } = this.getItemAndIndex({ id: item.id });
611
647
  if (index === -1)
612
- throw new Error("Cannot resolve index for item");
613
- const modifiedItem = modify(deepClone(item), modifier);
614
- this.memory().splice(index, 1, modifiedItem);
615
- modifiedItems.push(modifiedItem);
648
+ throw new Error(`Cannot resolve index for item with id '${item.id}'`);
649
+ const modifiedItem = modify(deepClone(item), restModifier);
650
+ if (item.id !== modifiedItem.id && this.getItemAndIndex({ id: modifiedItem.id }).item != null) {
651
+ throw new Error(`Item with same id '${modifiedItem.id}' already exists`);
652
+ }
653
+ this.emit("validate", modifiedItem);
654
+ return {
655
+ item: modifiedItem,
656
+ index
657
+ };
658
+ });
659
+ changes.forEach(({ item, index }) => {
660
+ this.memory().splice(index, 1, item);
616
661
  });
617
662
  this.rebuildIndices();
618
- modifiedItems.forEach((modifiedItem) => {
619
- this.emit("changed", modifiedItem, modifier);
663
+ changes.forEach(({ item }) => {
664
+ this.emit("changed", item, restModifier);
620
665
  });
621
666
  this.emit("updateMany", selector, modifier);
622
667
  this.executeInDebugMode((callstack) => this.emit("_debug.updateMany", callstack, selector, modifier));
623
- return modifiedItems.length;
668
+ return changes.length === 0 && (options == null ? void 0 : options.upsert) ? 1 : changes.length;
669
+ }
670
+ /**
671
+ * Replaces a single item in the collection that matches the given selector.
672
+ * @param selector - The criteria to select the item to replace.
673
+ * @param replacement - The item to replace the selected item with.
674
+ * @param [options] - Optional settings for the replace operation.
675
+ * @param [options.upsert] - If `true`, creates a new item if no item matches the selector.
676
+ * @returns The number of items replaced (0 or 1).
677
+ * @throws {Error} If the collection is disposed or invalid arguments are provided.
678
+ */
679
+ replaceOne(selector, replacement, options) {
680
+ if (this.isDisposed)
681
+ throw new Error("Collection is disposed");
682
+ if (!selector)
683
+ throw new Error("Invalid selector");
684
+ const { item, index } = this.getItemAndIndex(selector);
685
+ if (item == null) {
686
+ if (options == null ? void 0 : options.upsert) {
687
+ if (replacement.id != null && this.getItemAndIndex({ id: replacement.id }).item != null) {
688
+ throw new Error("Item with same id already exists");
689
+ }
690
+ this.insert(replacement);
691
+ }
692
+ } else {
693
+ if (item.id !== replacement.id && this.getItemAndIndex({ id: replacement.id }).item != null) {
694
+ throw new Error("Item with same id already exists");
695
+ }
696
+ const modifiedItem = { id: item.id, ...replacement };
697
+ this.emit("validate", modifiedItem);
698
+ this.memory().splice(index, 1, modifiedItem);
699
+ this.rebuildIndices();
700
+ this.emit("changed", modifiedItem, replacement);
701
+ }
702
+ this.emit("replaceOne", selector, replacement);
703
+ this.executeInDebugMode((callstack) => this.emit("_debug.replaceOne", callstack, selector, replacement));
704
+ if (item == null && !(options == null ? void 0 : options.upsert))
705
+ return 0;
706
+ return 1;
624
707
  }
625
708
  /**
626
709
  * Removes a single item from the collection that matches the given selector.
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@signaldb/core",
3
- "version": "1.3.1",
3
+ "version": "1.5.0",
4
4
  "description": "SignalDB is a client-side database that provides a simple MongoDB-like interface to the data with first-class typescript support to achieve an optimistic UI. Data persistence can be achieved by using storage providers that store the data through a JSON interface to places such as localStorage.",
5
5
  "scripts": {
6
- "build": "rimraf dist && vite build"
6
+ "build": "rimraf dist && vite build",
7
+ "test": "vitest"
7
8
  },
8
9
  "repository": {
9
10
  "type": "git",
@@ -54,5 +55,8 @@
54
55
  "dependencies": {
55
56
  "fast-sort": "^3.4.1",
56
57
  "mingo": "^6.5.1"
58
+ },
59
+ "devDependencies": {
60
+ "zod": "^3.24.2"
57
61
  }
58
62
  }