@sincpro/mobile 0.1.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 +74 -0
- package/dist/adapters/Bluetooth.adapter.d.ts +14 -0
- package/dist/adapters/Bluetooth.adapter.js +100 -0
- package/dist/adapters/Geo.adapter.d.ts +8 -0
- package/dist/adapters/Geo.adapter.js +30 -0
- package/dist/adapters/JsonSerializer.adapter.d.ts +28 -0
- package/dist/adapters/JsonSerializer.adapter.js +47 -0
- package/dist/adapters/Network.adapter.d.ts +4 -0
- package/dist/adapters/Network.adapter.js +13 -0
- package/dist/adapters/ReceiptExporter.adapter.d.ts +19 -0
- package/dist/adapters/ReceiptExporter.adapter.js +142 -0
- package/dist/adapters/repositories/database_table.repository.d.ts +13 -0
- package/dist/adapters/repositories/database_table.repository.js +29 -0
- package/dist/adapters/repositories/domain_event.repository.d.ts +32 -0
- package/dist/adapters/repositories/domain_event.repository.js +147 -0
- package/dist/adapters/repositories/domain_event_dead_letter.repository.d.ts +23 -0
- package/dist/adapters/repositories/domain_event_dead_letter.repository.js +81 -0
- package/dist/adapters/repositories/setting.repository.d.ts +10 -0
- package/dist/adapters/repositories/setting.repository.js +21 -0
- package/dist/adapters/webview.adapter.d.ts +10 -0
- package/dist/adapters/webview.adapter.js +166 -0
- package/dist/domain/connectivity/bluetooth.d.ts +19 -0
- package/dist/domain/connectivity/bluetooth.js +1 -0
- package/dist/domain/connectivity/events.d.ts +17 -0
- package/dist/domain/connectivity/events.js +17 -0
- package/dist/domain/connectivity/geo.d.ts +4 -0
- package/dist/domain/connectivity/geo.js +1 -0
- package/dist/domain/connectivity/index.d.ts +3 -0
- package/dist/domain/connectivity/index.js +3 -0
- package/dist/domain/connectivity/network.d.ts +7 -0
- package/dist/domain/connectivity/network.js +1 -0
- package/dist/domain/database/database.d.ts +16 -0
- package/dist/domain/database/database.js +1 -0
- package/dist/domain/database/index.d.ts +2 -0
- package/dist/domain/database/index.js +2 -0
- package/dist/domain/database/repository.d.ts +19 -0
- package/dist/domain/database/repository.js +1 -0
- package/dist/domain/entity/entity.d.ts +109 -0
- package/dist/domain/entity/entity.js +246 -0
- package/dist/domain/entity/entity_collection.d.ts +396 -0
- package/dist/domain/entity/entity_collection.js +824 -0
- package/dist/domain/entity/index.d.ts +3 -0
- package/dist/domain/entity/index.js +3 -0
- package/dist/domain/entity/value_object.d.ts +12 -0
- package/dist/domain/entity/value_object.js +39 -0
- package/dist/domain/event_sourcing/domain_event.d.ts +58 -0
- package/dist/domain/event_sourcing/domain_event.js +164 -0
- package/dist/domain/event_sourcing/event.d.ts +25 -0
- package/dist/domain/event_sourcing/event.js +25 -0
- package/dist/domain/event_sourcing/event_handler.d.ts +7 -0
- package/dist/domain/event_sourcing/event_handler.js +13 -0
- package/dist/domain/event_sourcing/index.d.ts +2 -0
- package/dist/domain/event_sourcing/index.js +2 -0
- package/dist/domain/events.d.ts +33 -0
- package/dist/domain/events.js +32 -0
- package/dist/domain/icon.d.ts +1 -0
- package/dist/domain/icon.js +1 -0
- package/dist/domain/index.d.ts +9 -0
- package/dist/domain/index.js +9 -0
- package/dist/domain/print/driver_registry.d.ts +4 -0
- package/dist/domain/print/driver_registry.js +29 -0
- package/dist/domain/print/events.d.ts +13 -0
- package/dist/domain/print/events.js +13 -0
- package/dist/domain/print/index.d.ts +2 -0
- package/dist/domain/print/index.js +1 -0
- package/dist/domain/print/printer.d.ts +35 -0
- package/dist/domain/print/printer.js +1 -0
- package/dist/domain/receipt.d.ts +31 -0
- package/dist/domain/receipt.js +1 -0
- package/dist/domain/repositories.d.ts +6 -0
- package/dist/domain/repositories.js +7 -0
- package/dist/domain/settings.d.ts +7 -0
- package/dist/domain/settings.js +1 -0
- package/dist/domain/webview/events.d.ts +11 -0
- package/dist/domain/webview/events.js +11 -0
- package/dist/domain/webview/index.d.ts +1 -0
- package/dist/domain/webview/index.js +1 -0
- package/dist/domain/webview/webview.d.ts +57 -0
- package/dist/domain/webview/webview.js +9 -0
- package/dist/entrypoints/cron/Cron.d.ts +13 -0
- package/dist/entrypoints/cron/Cron.js +57 -0
- package/dist/entrypoints/cron/checkNetworkStatus.cron.d.ts +3 -0
- package/dist/entrypoints/cron/checkNetworkStatus.cron.js +10 -0
- package/dist/entrypoints/db/index.d.ts +2 -0
- package/dist/entrypoints/db/index.js +2 -0
- package/dist/entrypoints/db/migrations.d.ts +10 -0
- package/dist/entrypoints/db/migrations.js +115 -0
- package/dist/entrypoints/db/repositories.d.ts +8 -0
- package/dist/entrypoints/db/repositories.js +34 -0
- package/dist/entrypoints/queue/QueueProcessor.d.ts +12 -0
- package/dist/entrypoints/queue/QueueProcessor.js +75 -0
- package/dist/entrypoints/queue/activateDomain.subscriber.d.ts +7 -0
- package/dist/entrypoints/queue/activateDomain.subscriber.js +15 -0
- package/dist/entrypoints/queue/newAppSettings.handler.d.ts +7 -0
- package/dist/entrypoints/queue/newAppSettings.handler.js +17 -0
- package/dist/entrypoints/queue/printImage.subscriber.d.ts +7 -0
- package/dist/entrypoints/queue/printImage.subscriber.js +14 -0
- package/dist/entrypoints/queue/processWebViewMessage.subscriber.d.ts +7 -0
- package/dist/entrypoints/queue/processWebViewMessage.subscriber.js +23 -0
- package/dist/entrypoints/ui/AppShell.d.ts +15 -0
- package/dist/entrypoints/ui/AppShell.js +58 -0
- package/dist/entrypoints/ui/common_provider.d.ts +35 -0
- package/dist/entrypoints/ui/common_provider.js +244 -0
- package/dist/entrypoints/ui/domain_switcher.d.ts +19 -0
- package/dist/entrypoints/ui/domain_switcher.js +22 -0
- package/dist/entrypoints/ui/theme.d.ts +15 -0
- package/dist/entrypoints/ui/theme.js +16 -0
- package/dist/exceptions.d.ts +22 -0
- package/dist/exceptions.js +60 -0
- package/dist/framework/base_module.d.ts +14 -0
- package/dist/framework/base_module.js +40 -0
- package/dist/framework/createApp.d.ts +6 -0
- package/dist/framework/createApp.js +9 -0
- package/dist/framework/domain_module.d.ts +13 -0
- package/dist/framework/domain_module.js +18 -0
- package/dist/framework/kernel.d.ts +18 -0
- package/dist/framework/kernel.js +60 -0
- package/dist/framework/orchestrator.d.ts +29 -0
- package/dist/framework/orchestrator.js +118 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +11 -0
- package/dist/infrastructure/database/connector.d.ts +13 -0
- package/dist/infrastructure/database/connector.js +165 -0
- package/dist/infrastructure/database/index.d.ts +2 -0
- package/dist/infrastructure/database/index.js +2 -0
- package/dist/infrastructure/database/mapped.d.ts +128 -0
- package/dist/infrastructure/database/mapped.js +174 -0
- package/dist/infrastructure/database/utils.d.ts +10 -0
- package/dist/infrastructure/database/utils.js +35 -0
- package/dist/infrastructure/logger.d.ts +45 -0
- package/dist/infrastructure/logger.js +125 -0
- package/dist/infrastructure/ui/ToastHost.d.ts +1 -0
- package/dist/infrastructure/ui/ToastHost.js +19 -0
- package/dist/infrastructure/ui/UIEventBus.d.ts +12 -0
- package/dist/infrastructure/ui/UIEventBus.js +65 -0
- package/dist/infrastructure/ui/errorHandler.d.ts +1 -0
- package/dist/infrastructure/ui/errorHandler.js +20 -0
- package/dist/infrastructure/ui/events.d.ts +1 -0
- package/dist/infrastructure/ui/events.js +1 -0
- package/dist/infrastructure/workers/CronWorker.d.ts +42 -0
- package/dist/infrastructure/workers/CronWorker.js +143 -0
- package/dist/infrastructure/workers/EventBus.d.ts +67 -0
- package/dist/infrastructure/workers/EventBus.js +279 -0
- package/dist/infrastructure/workers/index.d.ts +2 -0
- package/dist/infrastructure/workers/index.js +2 -0
- package/dist/services/bluetooth.service.d.ts +14 -0
- package/dist/services/bluetooth.service.js +72 -0
- package/dist/services/database_table.service.d.ts +8 -0
- package/dist/services/database_table.service.js +19 -0
- package/dist/services/dead_letter_queue.service.d.ts +38 -0
- package/dist/services/dead_letter_queue.service.js +99 -0
- package/dist/services/event.service.d.ts +7 -0
- package/dist/services/event.service.js +24 -0
- package/dist/services/network.service.d.ts +7 -0
- package/dist/services/network.service.js +43 -0
- package/dist/services/printer.service.d.ts +25 -0
- package/dist/services/printer.service.js +220 -0
- package/dist/services/webview.service.d.ts +17 -0
- package/dist/services/webview.service.js +59 -0
- package/dist/tools/utils/Initials.d.ts +1 -0
- package/dist/tools/utils/Initials.js +12 -0
- package/dist/tools/utils/collections.d.ts +120 -0
- package/dist/tools/utils/collections.js +158 -0
- package/dist/tools/utils/date.d.ts +70 -0
- package/dist/tools/utils/date.js +126 -0
- package/dist/tools/utils/maps.d.ts +4 -0
- package/dist/tools/utils/maps.js +20 -0
- package/dist/tools/utils/monetary.d.ts +3 -0
- package/dist/tools/utils/monetary.js +31 -0
- package/dist/tools/utils/quantity.d.ts +11 -0
- package/dist/tools/utils/quantity.js +44 -0
- package/dist/tools/utils/searchTools.d.ts +39 -0
- package/dist/tools/utils/searchTools.js +56 -0
- package/dist/tools/utils/serializer.d.ts +2 -0
- package/dist/tools/utils/serializer.js +44 -0
- package/dist/ui/components/atoms/DebugBanner.d.ts +2 -0
- package/dist/ui/components/atoms/DebugBanner.js +15 -0
- package/dist/ui/components/atoms/index.d.ts +1 -0
- package/dist/ui/components/atoms/index.js +1 -0
- package/dist/ui/components/molecules/BluetoothDeviceSelectorModal.d.ts +8 -0
- package/dist/ui/components/molecules/BluetoothDeviceSelectorModal.js +61 -0
- package/dist/ui/components/molecules/DeadLetterErrorBlock.d.ts +8 -0
- package/dist/ui/components/molecules/DeadLetterErrorBlock.js +14 -0
- package/dist/ui/components/molecules/EventTimelineItem.d.ts +7 -0
- package/dist/ui/components/molecules/EventTimelineItem.js +65 -0
- package/dist/ui/components/molecules/ProcessToast.d.ts +5 -0
- package/dist/ui/components/molecules/ProcessToast.js +51 -0
- package/dist/ui/components/molecules/ProcessToastProvider.d.ts +4 -0
- package/dist/ui/components/molecules/ProcessToastProvider.js +5 -0
- package/dist/ui/components/molecules/index.d.ts +5 -0
- package/dist/ui/components/molecules/index.js +5 -0
- package/dist/ui/components/organisms/BluetoothPrinterSelector.d.ts +7 -0
- package/dist/ui/components/organisms/BluetoothPrinterSelector.js +92 -0
- package/dist/ui/components/organisms/DatabaseInfoRow.d.ts +11 -0
- package/dist/ui/components/organisms/DatabaseInfoRow.js +8 -0
- package/dist/ui/components/organisms/DeadLetterQueueRow.d.ts +7 -0
- package/dist/ui/components/organisms/DeadLetterQueueRow.js +72 -0
- package/dist/ui/components/organisms/EventRow.d.ts +7 -0
- package/dist/ui/components/organisms/EventRow.js +90 -0
- package/dist/ui/components/organisms/InjectableWebView.d.ts +35 -0
- package/dist/ui/components/organisms/InjectableWebView.js +169 -0
- package/dist/ui/components/organisms/Receipt.d.ts +11 -0
- package/dist/ui/components/organisms/Receipt.js +207 -0
- package/dist/ui/components/organisms/TableInfoInfoRow.d.ts +9 -0
- package/dist/ui/components/organisms/TableInfoInfoRow.js +19 -0
- package/dist/ui/components/organisms/index.d.ts +7 -0
- package/dist/ui/components/organisms/index.js +7 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +4 -0
- package/dist/ui/layouts/router_layouts.d.ts +17 -0
- package/dist/ui/layouts/router_layouts.js +20 -0
- package/dist/ui/screens/database/database.context.d.ts +32 -0
- package/dist/ui/screens/database/database.context.js +158 -0
- package/dist/ui/screens/database/database.json_detail.d.ts +2 -0
- package/dist/ui/screens/database/database.json_detail.js +19 -0
- package/dist/ui/screens/database/database.list.d.ts +1 -0
- package/dist/ui/screens/database/database.list.js +41 -0
- package/dist/ui/screens/database/database.table_rows.d.ts +2 -0
- package/dist/ui/screens/database/database.table_rows.js +10 -0
- package/dist/ui/screens/dead_letter_queue/dead_letter_queue.context.d.ts +34 -0
- package/dist/ui/screens/dead_letter_queue/dead_letter_queue.context.js +166 -0
- package/dist/ui/screens/dead_letter_queue/dead_letter_queue.list.d.ts +2 -0
- package/dist/ui/screens/dead_letter_queue/dead_letter_queue.list.js +24 -0
- package/dist/ui/screens/events/events.context.d.ts +25 -0
- package/dist/ui/screens/events/events.context.js +113 -0
- package/dist/ui/screens/events/events.list.d.ts +1 -0
- package/dist/ui/screens/events/events.list.js +26 -0
- package/dist/ui/screens/events/index.d.ts +1 -0
- package/dist/ui/screens/events/index.js +1 -0
- package/dist/ui/screens/index.d.ts +3 -0
- package/dist/ui/screens/index.js +3 -0
- package/package.json +125 -0
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
import { generateUUID } from "../../infrastructure/database/utils";
|
|
2
|
+
import { loggerQueueProcessor } from "../../infrastructure/logger";
|
|
3
|
+
import { EventBus } from "../../infrastructure/workers";
|
|
4
|
+
import { convertToArray } from "../../tools/utils/collections";
|
|
5
|
+
import { safeJsonStringify } from "../../tools/utils/serializer";
|
|
6
|
+
import { ValueObject } from "./value_object";
|
|
7
|
+
/**
|
|
8
|
+
* Type-safe, immutable collection for Entity aggregates with event sourcing support.
|
|
9
|
+
* Implements IEventSourced for domain event publishing and provides functional operations.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const collection = new EntityCollection([priceList1, priceList2]);
|
|
13
|
+
*
|
|
14
|
+
* // Functional operations (immutable)
|
|
15
|
+
* const active = collection.filter(pl => pl.isActive).sortBy(pl => pl.name);
|
|
16
|
+
*
|
|
17
|
+
* // Aggregations
|
|
18
|
+
* const total = collection.sumBy(pl => pl.price);
|
|
19
|
+
* const cheapest = collection.minBy(pl => pl.price);
|
|
20
|
+
*
|
|
21
|
+
* // Set operations
|
|
22
|
+
* const union = collection1.union(collection2);
|
|
23
|
+
* const common = collection1.intersect(collection2);
|
|
24
|
+
*
|
|
25
|
+
* // Event sourcing
|
|
26
|
+
* await collection.publishEventWith(PRICE_LISTS_SYNCED, pl => ({ id: pl.remoteId, name: pl.name }));
|
|
27
|
+
*/
|
|
28
|
+
export class EntityCollection extends ValueObject {
|
|
29
|
+
uuid;
|
|
30
|
+
eventIds = [];
|
|
31
|
+
entities = [];
|
|
32
|
+
_domainEventsV2 = [];
|
|
33
|
+
_correlationIdV2 = null;
|
|
34
|
+
constructor(entities = []) {
|
|
35
|
+
super();
|
|
36
|
+
this.uuid = generateUUID();
|
|
37
|
+
this.entities = entities;
|
|
38
|
+
}
|
|
39
|
+
/** Factory method for creating instances - override in subclasses */
|
|
40
|
+
createInstance(entities) {
|
|
41
|
+
return new EntityCollection(entities);
|
|
42
|
+
}
|
|
43
|
+
/** Creates collection from array of entities - polymorphic, works with subclasses */
|
|
44
|
+
static from(entities) {
|
|
45
|
+
return new this(entities);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Converts entity, array, or collection to plain array.
|
|
49
|
+
* Useful for repository save methods that accept multiple input types.
|
|
50
|
+
* Supports: single entity, arrays, Set, EntityCollection
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* const entities = EntityCollection.toArrayFrom(entity);
|
|
54
|
+
* // Works with: single entity, array, Set, or EntityCollection
|
|
55
|
+
*/
|
|
56
|
+
static toArrayFrom(entity) {
|
|
57
|
+
if (entity instanceof EntityCollection) {
|
|
58
|
+
return entity.toArray();
|
|
59
|
+
}
|
|
60
|
+
return convertToArray(entity);
|
|
61
|
+
}
|
|
62
|
+
/** Returns total number of entities in collection */
|
|
63
|
+
get length() {
|
|
64
|
+
return this.entities.length;
|
|
65
|
+
}
|
|
66
|
+
/** Checks if collection has no entities */
|
|
67
|
+
get isEmpty() {
|
|
68
|
+
return this.entities.length === 0;
|
|
69
|
+
}
|
|
70
|
+
/** Checks if collection has at least one entity */
|
|
71
|
+
get isNotEmpty() {
|
|
72
|
+
return this.entities.length > 0;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Enables native JavaScript/TypeScript boolean evaluation.
|
|
76
|
+
* Collection behaves like primitives in conditional contexts.
|
|
77
|
+
*
|
|
78
|
+
* Compatible with language keywords:
|
|
79
|
+
* - if (collection) { } // true if has entities
|
|
80
|
+
* - !collection // true if empty
|
|
81
|
+
* - collection && doSomething() // executes if not empty
|
|
82
|
+
* - collection || defaultValue // uses collection if not empty
|
|
83
|
+
* - collection ? a : b // ternary operator
|
|
84
|
+
* - while (collection) { } // loops while not empty
|
|
85
|
+
* - Boolean(collection) // explicit boolean conversion
|
|
86
|
+
* - +collection // converts to number (length)
|
|
87
|
+
* - String(collection) // converts to "EntityCollection(n)"
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* if (priceLists) { console.log("Has items"); }
|
|
91
|
+
* const items = priceLists || new EntityCollection();
|
|
92
|
+
* !emptyCollection // true
|
|
93
|
+
*/
|
|
94
|
+
[Symbol.toPrimitive](hint) {
|
|
95
|
+
if (hint === "number") {
|
|
96
|
+
return this.length;
|
|
97
|
+
}
|
|
98
|
+
if (hint === "string") {
|
|
99
|
+
return `EntityCollection(${this.length})`;
|
|
100
|
+
}
|
|
101
|
+
return this.isNotEmpty;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Returns numeric value for collection (length).
|
|
105
|
+
* Enables: +collection, Number(collection)
|
|
106
|
+
*/
|
|
107
|
+
valueOf() {
|
|
108
|
+
return this.length;
|
|
109
|
+
}
|
|
110
|
+
/** Converts collection to plain array (creates new copy) */
|
|
111
|
+
toArray() {
|
|
112
|
+
return [...this.entities];
|
|
113
|
+
}
|
|
114
|
+
/** Gets entity at specific index (0-based) */
|
|
115
|
+
at(index) {
|
|
116
|
+
return this.entities[index];
|
|
117
|
+
}
|
|
118
|
+
/** Gets entity at specific index (0-based) */
|
|
119
|
+
get(index) {
|
|
120
|
+
return this.entities[index];
|
|
121
|
+
}
|
|
122
|
+
/** Checks if collection contains entity (uses Entity.equals) */
|
|
123
|
+
includes(entity) {
|
|
124
|
+
return this.entities.some((e) => e.equals(entity));
|
|
125
|
+
}
|
|
126
|
+
/** Finds index of entity in collection (uses Entity.equals) */
|
|
127
|
+
indexOf(entity) {
|
|
128
|
+
return this.entities.findIndex((e) => e.equals(entity));
|
|
129
|
+
}
|
|
130
|
+
/** Concatenates multiple collections into new collection */
|
|
131
|
+
concat(...collections) {
|
|
132
|
+
const allEntities = [this.entities];
|
|
133
|
+
for (const collection of collections) {
|
|
134
|
+
allEntities.push(collection.toArray());
|
|
135
|
+
}
|
|
136
|
+
return this.createInstance(allEntities.flat());
|
|
137
|
+
}
|
|
138
|
+
/** Returns subset of collection from start to end index */
|
|
139
|
+
slice(start, end) {
|
|
140
|
+
return this.createInstance(this.entities.slice(start, end));
|
|
141
|
+
}
|
|
142
|
+
/** Returns first entity in collection */
|
|
143
|
+
first() {
|
|
144
|
+
return this.entities[0];
|
|
145
|
+
}
|
|
146
|
+
/** Returns last entity in collection */
|
|
147
|
+
last() {
|
|
148
|
+
return this.entities[this.entities.length - 1];
|
|
149
|
+
}
|
|
150
|
+
/** Adds entity to collection (mutates) */
|
|
151
|
+
add(entity) {
|
|
152
|
+
this.entities.push(entity);
|
|
153
|
+
return this;
|
|
154
|
+
}
|
|
155
|
+
/** Adds multiple entities to collection (mutates) */
|
|
156
|
+
addAll(entities) {
|
|
157
|
+
this.entities.push(...entities);
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
/** Removes entity from collection (mutates, uses Entity.equals) */
|
|
161
|
+
remove(entity) {
|
|
162
|
+
const index = this.entities.findIndex((e) => e.equals(entity));
|
|
163
|
+
if (index !== -1) {
|
|
164
|
+
this.entities.splice(index, 1);
|
|
165
|
+
}
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
/** Removes all entities from collection (mutates) */
|
|
169
|
+
clear() {
|
|
170
|
+
this.entities = [];
|
|
171
|
+
return this;
|
|
172
|
+
}
|
|
173
|
+
/** Makes collection iterable (use with for...of loops) */
|
|
174
|
+
[Symbol.iterator]() {
|
|
175
|
+
return this.entities[Symbol.iterator]();
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Splits collection into chunks of specified size
|
|
179
|
+
* @example
|
|
180
|
+
* const pages = collection.chunk(10); // [[...10], [...10], ...]
|
|
181
|
+
* pages.forEach((page, i) => console.log(`Page ${i}: ${page.length} items`));
|
|
182
|
+
*/
|
|
183
|
+
chunk(size) {
|
|
184
|
+
const chunks = [];
|
|
185
|
+
for (let i = 0; i < this.entities.length; i += size) {
|
|
186
|
+
chunks.push(this.createInstance(this.entities.slice(i, i + size)));
|
|
187
|
+
}
|
|
188
|
+
return chunks;
|
|
189
|
+
}
|
|
190
|
+
/** Counts entities matching predicate (returns length if no predicate) */
|
|
191
|
+
count(predicate) {
|
|
192
|
+
if (!predicate)
|
|
193
|
+
return this.length;
|
|
194
|
+
return this.entities.filter(predicate).length;
|
|
195
|
+
}
|
|
196
|
+
/** Finds entity with minimum value from selector */
|
|
197
|
+
minBy(selector) {
|
|
198
|
+
if (this.isEmpty)
|
|
199
|
+
return undefined;
|
|
200
|
+
return this.entities.reduce((min, entity) => selector(entity) < selector(min) ? entity : min);
|
|
201
|
+
}
|
|
202
|
+
/** Finds entity with maximum value from selector */
|
|
203
|
+
maxBy(selector) {
|
|
204
|
+
if (this.isEmpty)
|
|
205
|
+
return undefined;
|
|
206
|
+
return this.entities.reduce((max, entity) => selector(entity) > selector(max) ? entity : max);
|
|
207
|
+
}
|
|
208
|
+
/** Sums numeric values from selector across all entities */
|
|
209
|
+
sumBy(selector) {
|
|
210
|
+
return this.entities.reduce((sum, entity) => sum + selector(entity), 0);
|
|
211
|
+
}
|
|
212
|
+
/** Calculates average of numeric values from selector */
|
|
213
|
+
averageBy(selector) {
|
|
214
|
+
if (this.isEmpty)
|
|
215
|
+
return 0;
|
|
216
|
+
return this.sumBy(selector) / this.length;
|
|
217
|
+
}
|
|
218
|
+
/** Finds last entity matching predicate (searches from end) */
|
|
219
|
+
findLast(predicate) {
|
|
220
|
+
for (let i = this.entities.length - 1; i >= 0; i--) {
|
|
221
|
+
if (predicate(this.entities[i])) {
|
|
222
|
+
return this.entities[i];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Converts collection to Map using key selector (for fast lookups)
|
|
229
|
+
* @example
|
|
230
|
+
* const priceListMap = collection.toMap(pl => pl.remoteId!);
|
|
231
|
+
* const priceList = priceListMap.get(123); // O(1) lookup
|
|
232
|
+
*/
|
|
233
|
+
toMap(keySelector) {
|
|
234
|
+
const map = new Map();
|
|
235
|
+
for (const entity of this.entities) {
|
|
236
|
+
map.set(keySelector(entity), entity);
|
|
237
|
+
}
|
|
238
|
+
return map;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Converts collection to plain object using key and value selectors
|
|
242
|
+
* @example
|
|
243
|
+
* const names = collection.toRecord(pl => pl.remoteId!, pl => pl.name);
|
|
244
|
+
* // { 1: "Premium", 2: "Basic" }
|
|
245
|
+
*/
|
|
246
|
+
toRecord(keySelector, valueSelector) {
|
|
247
|
+
const record = {};
|
|
248
|
+
for (const entity of this.entities) {
|
|
249
|
+
record[keySelector(entity)] = valueSelector(entity);
|
|
250
|
+
}
|
|
251
|
+
return record;
|
|
252
|
+
}
|
|
253
|
+
/** Checks if collection contains any of provided entities */
|
|
254
|
+
containsAny(entities) {
|
|
255
|
+
return entities.some((entity) => this.includes(entity));
|
|
256
|
+
}
|
|
257
|
+
/** Checks if collection contains all provided entities */
|
|
258
|
+
containsAll(entities) {
|
|
259
|
+
return entities.every((entity) => this.includes(entity));
|
|
260
|
+
}
|
|
261
|
+
/** Returns new collection with entities matching predicate */
|
|
262
|
+
filter(predicate) {
|
|
263
|
+
return this.createInstance(this.entities.filter(predicate));
|
|
264
|
+
}
|
|
265
|
+
/** Transforms entities to new type, returns new collection */
|
|
266
|
+
map(mapper) {
|
|
267
|
+
return new EntityCollection(this.entities.map(mapper));
|
|
268
|
+
}
|
|
269
|
+
/** Transforms entities to any type, returns plain array */
|
|
270
|
+
mapToArray(mapper) {
|
|
271
|
+
return this.entities.map(mapper);
|
|
272
|
+
}
|
|
273
|
+
/** Maps and flattens results into new collection */
|
|
274
|
+
flatMap(mapper) {
|
|
275
|
+
return new EntityCollection(this.entities.flatMap(mapper));
|
|
276
|
+
}
|
|
277
|
+
/** Maps and flattens results into plain array */
|
|
278
|
+
flatMapToArray(mapper) {
|
|
279
|
+
return this.entities.flatMap(mapper);
|
|
280
|
+
}
|
|
281
|
+
/** Finds first entity matching predicate */
|
|
282
|
+
find(predicate) {
|
|
283
|
+
return this.entities.find(predicate);
|
|
284
|
+
}
|
|
285
|
+
/** Finds index of first entity matching predicate */
|
|
286
|
+
findIndex(predicate) {
|
|
287
|
+
return this.entities.findIndex(predicate);
|
|
288
|
+
}
|
|
289
|
+
/** Checks if at least one entity matches predicate */
|
|
290
|
+
some(predicate) {
|
|
291
|
+
return this.entities.some(predicate);
|
|
292
|
+
}
|
|
293
|
+
/** Checks if all entities match predicate */
|
|
294
|
+
every(predicate) {
|
|
295
|
+
return this.entities.every(predicate);
|
|
296
|
+
}
|
|
297
|
+
/** Executes callback for each entity */
|
|
298
|
+
forEach(callback) {
|
|
299
|
+
this.entities.forEach(callback);
|
|
300
|
+
}
|
|
301
|
+
/** Reduces collection to single value using reducer function */
|
|
302
|
+
reduce(reducer, initialValue) {
|
|
303
|
+
return this.entities.reduce(reducer, initialValue);
|
|
304
|
+
}
|
|
305
|
+
/** Sorts collection using compare function, returns new collection */
|
|
306
|
+
sort(compareFn) {
|
|
307
|
+
return this.createInstance([...this.entities].sort(compareFn));
|
|
308
|
+
}
|
|
309
|
+
/** Sorts collection by key selector, returns new collection */
|
|
310
|
+
sortBy(keySelector) {
|
|
311
|
+
return this.sort((a, b) => {
|
|
312
|
+
const aVal = keySelector(a);
|
|
313
|
+
const bVal = keySelector(b);
|
|
314
|
+
if (aVal < bVal)
|
|
315
|
+
return -1;
|
|
316
|
+
if (aVal > bVal)
|
|
317
|
+
return 1;
|
|
318
|
+
return 0;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Groups entities by key into Map of collections
|
|
323
|
+
* @example
|
|
324
|
+
* const byTyp = collection.groupBy(pl => pl.currencyId);
|
|
325
|
+
* // Map<number, EntityCollection<PriceList>>
|
|
326
|
+
* byType.get(1)?.forEach(pl => console.log(pl.name));
|
|
327
|
+
*/
|
|
328
|
+
groupBy(keySelector) {
|
|
329
|
+
const groups = new Map();
|
|
330
|
+
for (const entity of this.entities) {
|
|
331
|
+
const key = keySelector(entity);
|
|
332
|
+
if (!groups.has(key)) {
|
|
333
|
+
groups.set(key, []);
|
|
334
|
+
}
|
|
335
|
+
groups.get(key).push(entity);
|
|
336
|
+
}
|
|
337
|
+
const result = new Map();
|
|
338
|
+
for (const [key, entities] of groups.entries()) {
|
|
339
|
+
result.set(key, this.createInstance(entities));
|
|
340
|
+
}
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Splits collection into [matching, non-matching] tuple
|
|
345
|
+
* @example
|
|
346
|
+
* const [active, inactive] = collection.partition(pl => pl.isActive);
|
|
347
|
+
*/
|
|
348
|
+
partition(predicate) {
|
|
349
|
+
const truthy = [];
|
|
350
|
+
const falsy = [];
|
|
351
|
+
for (const entity of this.entities) {
|
|
352
|
+
if (predicate(entity)) {
|
|
353
|
+
truthy.push(entity);
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
falsy.push(entity);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return [this.createInstance(truthy), this.createInstance(falsy)];
|
|
360
|
+
}
|
|
361
|
+
/** Returns collection with unique entities (uses UUID by default or custom key selector) */
|
|
362
|
+
distinct(keySelector) {
|
|
363
|
+
if (!keySelector) {
|
|
364
|
+
const seen = new Set();
|
|
365
|
+
const unique = [];
|
|
366
|
+
for (const entity of this.entities) {
|
|
367
|
+
if (!seen.has(entity.uuid)) {
|
|
368
|
+
seen.add(entity.uuid);
|
|
369
|
+
unique.push(entity);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return this.createInstance(unique);
|
|
373
|
+
}
|
|
374
|
+
const seen = new Set();
|
|
375
|
+
const unique = [];
|
|
376
|
+
for (const entity of this.entities) {
|
|
377
|
+
const key = keySelector(entity);
|
|
378
|
+
if (!seen.has(key)) {
|
|
379
|
+
seen.add(key);
|
|
380
|
+
unique.push(entity);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return this.createInstance(unique);
|
|
384
|
+
}
|
|
385
|
+
/** Returns first N entities from collection */
|
|
386
|
+
take(count) {
|
|
387
|
+
return this.createInstance(this.entities.slice(0, count));
|
|
388
|
+
}
|
|
389
|
+
/** Skips first N entities, returns rest */
|
|
390
|
+
skip(count) {
|
|
391
|
+
return this.createInstance(this.entities.slice(count));
|
|
392
|
+
}
|
|
393
|
+
/** Returns union of collections (no duplicates by uuid) */
|
|
394
|
+
union(other) {
|
|
395
|
+
const combined = [...this.entities, ...other.entities];
|
|
396
|
+
return this.createInstance(combined).distinct((e) => e.uuid);
|
|
397
|
+
}
|
|
398
|
+
/** Returns entities present in both collections */
|
|
399
|
+
intersect(other) {
|
|
400
|
+
const otherUuids = new Set(other.mapToArray((e) => e.uuid));
|
|
401
|
+
return this.filter((e) => otherUuids.has(e.uuid));
|
|
402
|
+
}
|
|
403
|
+
/** Returns entities in this collection but not in other */
|
|
404
|
+
except(other) {
|
|
405
|
+
const otherUuids = new Set(other.mapToArray((e) => e.uuid));
|
|
406
|
+
return this.filter((e) => !otherUuids.has(e.uuid));
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Returns entities in this collection but not in other using custom selector
|
|
410
|
+
* @example
|
|
411
|
+
* const different = collection1.exceptBy(collection2, e => e.name);
|
|
412
|
+
*/
|
|
413
|
+
exceptBy(other, keySelector) {
|
|
414
|
+
const otherKeys = new Set(other.mapToArray(keySelector));
|
|
415
|
+
return this.filter((e) => !otherKeys.has(keySelector(e)));
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Finds entity by UUID
|
|
419
|
+
* @example
|
|
420
|
+
* const entity = collection.findByUuid("abc-123-def");
|
|
421
|
+
*/
|
|
422
|
+
findByUuid(uuid) {
|
|
423
|
+
return this.find((e) => e.uuid === uuid);
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Finds entities by multiple UUIDs
|
|
427
|
+
* @example
|
|
428
|
+
* const entities = collection.findByUuids(["uuid1", "uuid2", "uuid3"]);
|
|
429
|
+
*/
|
|
430
|
+
findByUuids(uuids) {
|
|
431
|
+
const uuidSet = new Set(uuids);
|
|
432
|
+
return this.filter((e) => uuidSet.has(e.uuid));
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Checks if collection contains entity with given UUID
|
|
436
|
+
* @example
|
|
437
|
+
* if (collection.hasUuid("abc-123")) { ... }
|
|
438
|
+
*/
|
|
439
|
+
hasUuid(uuid) {
|
|
440
|
+
return this.some((e) => e.uuid === uuid);
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Extracts all UUIDs as plain array
|
|
444
|
+
* @example
|
|
445
|
+
* const uuids = collection.getUuids();
|
|
446
|
+
* await deleteByUuids(uuids);
|
|
447
|
+
*/
|
|
448
|
+
getUuids() {
|
|
449
|
+
return this.mapToArray((e) => e.uuid);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Creates a Map indexed by UUID for O(1) lookups
|
|
453
|
+
* @example
|
|
454
|
+
* const byUuid = collection.toMapByUuid();
|
|
455
|
+
* const entity = byUuid.get("abc-123"); // Fast lookup
|
|
456
|
+
*/
|
|
457
|
+
toMapByUuid() {
|
|
458
|
+
return this.toMap((e) => e.uuid);
|
|
459
|
+
}
|
|
460
|
+
// ============================================
|
|
461
|
+
// V2 METHODS - DomainEvent class-based system
|
|
462
|
+
// ============================================
|
|
463
|
+
addDomainEvent(EventClass, payload) {
|
|
464
|
+
const event = EventClass.create(payload);
|
|
465
|
+
const existingEvent = this._domainEventsV2.find((e) => e.hasSameBusinessPayload(event));
|
|
466
|
+
if (existingEvent) {
|
|
467
|
+
loggerQueueProcessor.warn(`EntityCollection - V2 Event: '${event.name}' with identical payload already exists. Skipping duplicate.`);
|
|
468
|
+
return existingEvent.uuid;
|
|
469
|
+
}
|
|
470
|
+
if (!this._correlationIdV2) {
|
|
471
|
+
this._correlationIdV2 = generateUUID();
|
|
472
|
+
}
|
|
473
|
+
event
|
|
474
|
+
.withAggregateId(this.uuid)
|
|
475
|
+
.withCorrelationId(this._correlationIdV2)
|
|
476
|
+
.withSequence(this._domainEventsV2.length + 1);
|
|
477
|
+
this._domainEventsV2.push(event);
|
|
478
|
+
this.eventIds.push(event.uuid);
|
|
479
|
+
for (const entity of this.entities) {
|
|
480
|
+
entity.eventIds.push(event.uuid);
|
|
481
|
+
}
|
|
482
|
+
return event.uuid;
|
|
483
|
+
}
|
|
484
|
+
addDomainEventWithEntities(EventClass) {
|
|
485
|
+
const event = EventClass.create({
|
|
486
|
+
records: this.entities.map((e) => e.asJSON()),
|
|
487
|
+
});
|
|
488
|
+
const existingEvent = this._domainEventsV2.find((e) => e.hasSameBusinessPayload(event));
|
|
489
|
+
if (existingEvent) {
|
|
490
|
+
loggerQueueProcessor.warn(`EntityCollection - V2 Event: '${event.name}' with identical payload already exists. Skipping duplicate.`);
|
|
491
|
+
return existingEvent.uuid;
|
|
492
|
+
}
|
|
493
|
+
if (!this._correlationIdV2) {
|
|
494
|
+
this._correlationIdV2 = generateUUID();
|
|
495
|
+
}
|
|
496
|
+
event
|
|
497
|
+
.withAggregateId(this.uuid)
|
|
498
|
+
.withCorrelationId(this._correlationIdV2)
|
|
499
|
+
.withSequence(this._domainEventsV2.length + 1);
|
|
500
|
+
this._domainEventsV2.push(event);
|
|
501
|
+
this.eventIds.push(event.uuid);
|
|
502
|
+
for (const entity of this.entities) {
|
|
503
|
+
entity.eventIds.push(event.uuid);
|
|
504
|
+
}
|
|
505
|
+
return event.uuid;
|
|
506
|
+
}
|
|
507
|
+
getDomainEvents() {
|
|
508
|
+
return this._domainEventsV2.map((e) => e.clone());
|
|
509
|
+
}
|
|
510
|
+
clearDomainEvents() {
|
|
511
|
+
this._domainEventsV2 = [];
|
|
512
|
+
this._correlationIdV2 = null;
|
|
513
|
+
}
|
|
514
|
+
async publishAllDomainEvents() {
|
|
515
|
+
const events = this._domainEventsV2;
|
|
516
|
+
for (const event of events) {
|
|
517
|
+
await EventBus.publish(event);
|
|
518
|
+
}
|
|
519
|
+
this.clearDomainEvents();
|
|
520
|
+
}
|
|
521
|
+
async publishAllDomainEventsSync() {
|
|
522
|
+
const events = this._domainEventsV2;
|
|
523
|
+
const errors = [];
|
|
524
|
+
for (const event of events) {
|
|
525
|
+
try {
|
|
526
|
+
await EventBus.publishSync(event);
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
loggerQueueProcessor.warn(`EntityCollection - Error publishing V2 domain event '${event.name}': ${error.message}`);
|
|
530
|
+
errors.push(error);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
this.clearDomainEvents();
|
|
534
|
+
if (errors.length > 0) {
|
|
535
|
+
const errorList = errors.map((e) => ` - ${e.message}`).join("\n");
|
|
536
|
+
throw new Error(`Errors occurred while publishing V2 domain events synchronously:\n${errorList}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
async publishDomainEvent(EventClass, payload) {
|
|
540
|
+
this.addDomainEvent(EventClass, payload);
|
|
541
|
+
await this.publishAllDomainEvents();
|
|
542
|
+
}
|
|
543
|
+
async publishDomainEventSync(EventClass, payload) {
|
|
544
|
+
this.addDomainEvent(EventClass, payload);
|
|
545
|
+
await this.publishAllDomainEventsSync();
|
|
546
|
+
}
|
|
547
|
+
async publishDomainEventWithEntities(EventClass) {
|
|
548
|
+
this.addDomainEventWithEntities(EventClass);
|
|
549
|
+
await this.publishAllDomainEvents();
|
|
550
|
+
}
|
|
551
|
+
async publishDomainEventWithEntitiesSync(EventClass) {
|
|
552
|
+
this.addDomainEventWithEntities(EventClass);
|
|
553
|
+
await this.publishAllDomainEventsSync();
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Serializes collection as array of entity JSONs.
|
|
557
|
+
* Collection is a manager, not an entity, so it serializes as pure entity list.
|
|
558
|
+
* @example
|
|
559
|
+
* const json = collection.asJSON();
|
|
560
|
+
* await saveToDatabase(json);
|
|
561
|
+
*/
|
|
562
|
+
asJSON(pretty = false) {
|
|
563
|
+
if (!pretty) {
|
|
564
|
+
return safeJsonStringify(this.entities.map((e) => e.asJSON(pretty)), pretty);
|
|
565
|
+
}
|
|
566
|
+
return this.entities.map((e) => e.asJSON(pretty));
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Filters entities using criteria array. All criteria must match (AND).
|
|
570
|
+
* @example
|
|
571
|
+
* const filtered = collection.findByCriteria([
|
|
572
|
+
* { field: 'customerId', operator: '=', value: 123 },
|
|
573
|
+
* { field: 'state', operator: 'in', value: ['draft', 'posted'] }
|
|
574
|
+
* ]);
|
|
575
|
+
*/
|
|
576
|
+
findByCriteria(criteria) {
|
|
577
|
+
const filtered = this.filter((entity) => {
|
|
578
|
+
return criteria.every((c) => {
|
|
579
|
+
const fieldValue = entity[c.field];
|
|
580
|
+
switch (c.operator) {
|
|
581
|
+
case "=":
|
|
582
|
+
return fieldValue === c.value;
|
|
583
|
+
case "!=":
|
|
584
|
+
return fieldValue !== c.value;
|
|
585
|
+
case "in":
|
|
586
|
+
return Array.isArray(c.value) && c.value.includes(fieldValue);
|
|
587
|
+
case "not_in":
|
|
588
|
+
return Array.isArray(c.value) && !c.value.includes(fieldValue);
|
|
589
|
+
case "like":
|
|
590
|
+
return (typeof fieldValue === "string" &&
|
|
591
|
+
fieldValue.toLowerCase().includes(String(c.value).toLowerCase()));
|
|
592
|
+
case ">":
|
|
593
|
+
return fieldValue > c.value;
|
|
594
|
+
case "<":
|
|
595
|
+
return fieldValue < c.value;
|
|
596
|
+
case ">=":
|
|
597
|
+
return fieldValue >= c.value;
|
|
598
|
+
case "<=":
|
|
599
|
+
return fieldValue <= c.value;
|
|
600
|
+
default:
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
return this.createInstance(filtered.toArray());
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Deserializes collection from array of entity JSONs.
|
|
609
|
+
* Collection is a manager, not an entity, so it deserializes from pure entity list.
|
|
610
|
+
* @example
|
|
611
|
+
* const collection = EntityCollection.fromJSON<PriceList>(json, PriceList);
|
|
612
|
+
*/
|
|
613
|
+
static fromJSON(json, EntityClass) {
|
|
614
|
+
if (!EntityClass) {
|
|
615
|
+
throw new Error("EntityClass is required for EntityCollection.fromJSON");
|
|
616
|
+
}
|
|
617
|
+
const data = typeof json === "string" ? JSON.parse(json) : json;
|
|
618
|
+
if (!Array.isArray(data)) {
|
|
619
|
+
throw new Error("EntityCollection.fromJSON expects array of entity JSONs");
|
|
620
|
+
}
|
|
621
|
+
const entities = data.map((entityData) => EntityClass.prototype.fromJSON.call(EntityClass, entityData));
|
|
622
|
+
return new EntityCollection(entities);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Collection for RemoteEntity aggregates with DTO mapping support.
|
|
627
|
+
* Extends EntityCollection with fromRemoteDTO for batch entity creation.
|
|
628
|
+
*
|
|
629
|
+
* @example
|
|
630
|
+
* const collection = RemoteEntityCollection.fromRemoteDTO(
|
|
631
|
+
* remoteDTOs,
|
|
632
|
+
* PriceList
|
|
633
|
+
* );
|
|
634
|
+
* await collection.publishIds(PRICE_LISTS_SYNCED);
|
|
635
|
+
*/
|
|
636
|
+
export class RemoteEntityCollection extends EntityCollection {
|
|
637
|
+
/**
|
|
638
|
+
* Creates collection from remote DTOs using entity's fromRemoteDTO method
|
|
639
|
+
* @example
|
|
640
|
+
* const priceLists = RemoteEntityCollection.fromRemoteDTO(
|
|
641
|
+
* apiResponse.records,
|
|
642
|
+
* PriceList
|
|
643
|
+
* );
|
|
644
|
+
*/
|
|
645
|
+
static fromRemoteDTO(dtos, EntityClass) {
|
|
646
|
+
const entities = dtos.map((dto) => EntityClass.fromRemoteDTO(dto));
|
|
647
|
+
return new RemoteEntityCollection(entities);
|
|
648
|
+
}
|
|
649
|
+
/** Factory method override to create RemoteEntityCollection instances */
|
|
650
|
+
createInstance(entities) {
|
|
651
|
+
return new RemoteEntityCollection(entities);
|
|
652
|
+
}
|
|
653
|
+
// ============================================================================
|
|
654
|
+
// INHERITED METHODS - All these return RemoteEntityCollection thanks to createInstance()
|
|
655
|
+
//
|
|
656
|
+
// The following methods are inherited from EntityCollection and automatically
|
|
657
|
+
// return RemoteEntityCollection<T> instead of EntityCollection<T>:
|
|
658
|
+
//
|
|
659
|
+
// - filter(predicate) → RemoteEntityCollection<T>
|
|
660
|
+
// - partition(predicate) → [RemoteEntityCollection<T>, RemoteEntityCollection<T>]
|
|
661
|
+
// - findByCriteria(criteria) → RemoteEntityCollection<T>
|
|
662
|
+
// - distinct(keySelector?) → RemoteEntityCollection<T>
|
|
663
|
+
// - slice(start?, end?) → RemoteEntityCollection<T>
|
|
664
|
+
// - take(count) → RemoteEntityCollection<T>
|
|
665
|
+
// - skip(count) → RemoteEntityCollection<T>
|
|
666
|
+
// - sort(compareFn) → RemoteEntityCollection<T>
|
|
667
|
+
// - sortBy(keySelector) → RemoteEntityCollection<T>
|
|
668
|
+
// - concat(...collections) → RemoteEntityCollection<T>
|
|
669
|
+
// - chunk(size) → RemoteEntityCollection<T>[]
|
|
670
|
+
// - groupBy(keySelector) → Map<K, RemoteEntityCollection<T>>
|
|
671
|
+
// - union(other) → RemoteEntityCollection<T>
|
|
672
|
+
// - intersect(other) → RemoteEntityCollection<T>
|
|
673
|
+
// - except(other) → RemoteEntityCollection<T>
|
|
674
|
+
// - exceptBy(other, key) → RemoteEntityCollection<T>
|
|
675
|
+
// ============================================================================
|
|
676
|
+
/**
|
|
677
|
+
* Finds entity by remoteId
|
|
678
|
+
* Returns undefined if entity doesn't have remoteId or not found
|
|
679
|
+
* @example
|
|
680
|
+
* const customer = customers.findByRemoteId(123);
|
|
681
|
+
*/
|
|
682
|
+
findByRemoteId(remoteId) {
|
|
683
|
+
return this.find((e) => e.remoteId === remoteId);
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Finds entities by multiple remoteIds
|
|
687
|
+
* @example
|
|
688
|
+
* const products = collection.findByRemoteIds([1, 2, 3, 4, 5]);
|
|
689
|
+
*/
|
|
690
|
+
findByRemoteIds(remoteIds) {
|
|
691
|
+
const remoteIdSet = new Set(remoteIds);
|
|
692
|
+
return new RemoteEntityCollection(this.filter((e) => e.remoteId != null && remoteIdSet.has(e.remoteId)).toArray());
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Checks if collection contains entity with given remoteId
|
|
696
|
+
* @example
|
|
697
|
+
* if (customers.hasRemoteId(123)) { ... }
|
|
698
|
+
*/
|
|
699
|
+
hasRemoteId(remoteId) {
|
|
700
|
+
return this.some((e) => e.remoteId === remoteId);
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Returns entities that have remoteId set (synced with backend)
|
|
704
|
+
* @example
|
|
705
|
+
* const syncedEntities = collection.withRemoteId();
|
|
706
|
+
*/
|
|
707
|
+
withRemoteId() {
|
|
708
|
+
return new RemoteEntityCollection(this.filter((e) => e.remoteId != null).toArray());
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Returns entities without remoteId (local-only, not synced)
|
|
712
|
+
* @example
|
|
713
|
+
* const localOnlyEntities = collection.withoutRemoteId();
|
|
714
|
+
*/
|
|
715
|
+
withoutRemoteId() {
|
|
716
|
+
return new RemoteEntityCollection(this.filter((e) => e.remoteId == null).toArray());
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Extracts all remoteIds as plain array (filters out null/undefined)
|
|
720
|
+
* @example
|
|
721
|
+
* const remoteIds = collection.getRemoteIds();
|
|
722
|
+
* await syncWithBackend(remoteIds);
|
|
723
|
+
*/
|
|
724
|
+
getRemoteIds() {
|
|
725
|
+
return this.mapToArray((e) => e.remoteId).filter((id) => id != null);
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Creates a Map indexed by remoteId for O(1) lookups
|
|
729
|
+
* Filters out entities without remoteId
|
|
730
|
+
* @example
|
|
731
|
+
* const byRemoteId = collection.toMapByRemoteId();
|
|
732
|
+
* const customer = byRemoteId.get(123); // Fast lookup
|
|
733
|
+
*/
|
|
734
|
+
toMapByRemoteId() {
|
|
735
|
+
const map = new Map();
|
|
736
|
+
for (const entity of this) {
|
|
737
|
+
if (entity.remoteId != null) {
|
|
738
|
+
map.set(entity.remoteId, entity);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return map;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Returns entities in this collection but not in other, comparing by remoteId
|
|
745
|
+
* Useful for finding entities that exist locally but not in backend response
|
|
746
|
+
* @example
|
|
747
|
+
* const localOnly = localCollection.exceptByRemoteId(backendCollection);
|
|
748
|
+
*/
|
|
749
|
+
exceptByRemoteId(other) {
|
|
750
|
+
const otherRemoteIds = new Set(other.getRemoteIds());
|
|
751
|
+
return new RemoteEntityCollection(this.filter((e) => e.remoteId == null || !otherRemoteIds.has(e.remoteId)).toArray());
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Returns entities present in both collections, comparing by remoteId
|
|
755
|
+
* Useful for finding entities that exist in both local and backend
|
|
756
|
+
* @example
|
|
757
|
+
* const inBoth = localCollection.intersectByRemoteId(backendCollection);
|
|
758
|
+
*/
|
|
759
|
+
intersectByRemoteId(other) {
|
|
760
|
+
const otherRemoteIds = new Set(other.getRemoteIds());
|
|
761
|
+
return new RemoteEntityCollection(this.filter((e) => e.remoteId != null && otherRemoteIds.has(e.remoteId)).toArray());
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Removes duplicates by remoteId (keeps first occurrence)
|
|
765
|
+
* Entities without remoteId are kept and deduplicated by uuid
|
|
766
|
+
* @example
|
|
767
|
+
* const unique = collection.distinctByRemoteId();
|
|
768
|
+
*/
|
|
769
|
+
distinctByRemoteId() {
|
|
770
|
+
const seen = new Set();
|
|
771
|
+
const unique = [];
|
|
772
|
+
for (const entity of this) {
|
|
773
|
+
const key = entity.remoteId ?? entity.uuid;
|
|
774
|
+
if (!seen.has(key)) {
|
|
775
|
+
seen.add(key);
|
|
776
|
+
unique.push(entity);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return new RemoteEntityCollection(unique);
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Filters entities pending synchronization with backend
|
|
783
|
+
* @example
|
|
784
|
+
* const pending = collection.findPendingSync();
|
|
785
|
+
* for (const entity of pending) {
|
|
786
|
+
* await pushToBackend(entity);
|
|
787
|
+
* }
|
|
788
|
+
*/
|
|
789
|
+
findPendingSync() {
|
|
790
|
+
return new RemoteEntityCollection(this.filter((e) => e.remoteState === "PENDING").toArray());
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Filters entities successfully synced with backend
|
|
794
|
+
* @example
|
|
795
|
+
* const synced = collection.findSynced();
|
|
796
|
+
* console.log(`${synced.length} entities synced`);
|
|
797
|
+
*/
|
|
798
|
+
findSynced() {
|
|
799
|
+
return new RemoteEntityCollection(this.filter((e) => e.remoteState === "SYNCED").toArray());
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Filters entities that failed synchronization
|
|
803
|
+
* @example
|
|
804
|
+
* const failed = collection.findFailed();
|
|
805
|
+
* failed.forEach(e => logger.error(`Failed to sync: ${e.uuid}`));
|
|
806
|
+
*/
|
|
807
|
+
findFailed() {
|
|
808
|
+
return new RemoteEntityCollection(this.filter((e) => e.remoteState === "FAILED").toArray());
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Groups entities by remote state
|
|
812
|
+
* @example
|
|
813
|
+
* const byState = collection.groupByRemoteState();
|
|
814
|
+
* console.log(`Pending: ${byState.get('PENDING')?.length}`);
|
|
815
|
+
*/
|
|
816
|
+
groupByRemoteState() {
|
|
817
|
+
const groups = this.groupBy((e) => e.remoteState);
|
|
818
|
+
const result = new Map();
|
|
819
|
+
for (const [state, entities] of groups.entries()) {
|
|
820
|
+
result.set(state, new RemoteEntityCollection(entities.toArray()));
|
|
821
|
+
}
|
|
822
|
+
return result;
|
|
823
|
+
}
|
|
824
|
+
}
|