flongo 1.0.2 → 1.2.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.
@@ -0,0 +1,39 @@
1
+ import { BaseCacheStore, CacheStoreOptions } from './cacheStore';
2
+ export interface MemoryCacheOptions extends CacheStoreOptions {
3
+ checkInterval?: number;
4
+ maxMemoryMB?: number;
5
+ }
6
+ export declare class MemoryCache<T> extends BaseCacheStore<T> {
7
+ private cache;
8
+ private lruOrder;
9
+ private cleanupInterval?;
10
+ private readonly memoryOptions;
11
+ private readonly locks;
12
+ private capacityLock;
13
+ constructor(options?: MemoryCacheOptions);
14
+ get(key: string): Promise<T | undefined>;
15
+ set(key: string, value: T, ttlSeconds?: number): Promise<void>;
16
+ delete(key: string): Promise<boolean>;
17
+ clear(): Promise<void>;
18
+ has(key: string): Promise<boolean>;
19
+ size(): Promise<number>;
20
+ keys(): Promise<string[]>;
21
+ getMemoryUsage(): Promise<number>;
22
+ destroy(): void;
23
+ private isExpired;
24
+ private updateLRU;
25
+ private removeLRU;
26
+ private ensureCapacity;
27
+ private checkMemoryLimit;
28
+ private estimateSize;
29
+ private startCleanupInterval;
30
+ private cleanupExpired;
31
+ private acquireLock;
32
+ private createLockPromise;
33
+ private releaseLock;
34
+ private waitForLock;
35
+ private acquireCapacityLock;
36
+ private releaseCapacityLock;
37
+ private waitForCapacityLock;
38
+ }
39
+ //# sourceMappingURL=memoryCache.d.ts.map
@@ -0,0 +1,295 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MemoryCache = void 0;
4
+ const cacheStore_1 = require("./cacheStore");
5
+ class MemoryCache extends cacheStore_1.BaseCacheStore {
6
+ constructor(options = {}) {
7
+ super(options);
8
+ this.capacityLock = null;
9
+ this.cache = new Map();
10
+ this.lruOrder = [];
11
+ this.locks = new Map();
12
+ this.memoryOptions = {
13
+ ...this.options,
14
+ checkInterval: options.checkInterval ?? 60000,
15
+ maxMemoryMB: options.maxMemoryMB ?? 100
16
+ };
17
+ if (this.memoryOptions.checkInterval > 0) {
18
+ this.startCleanupInterval();
19
+ }
20
+ }
21
+ async get(key) {
22
+ await this.waitForLock(key);
23
+ const entry = this.cache.get(key);
24
+ if (!entry) {
25
+ this.incrementStat('misses');
26
+ return undefined;
27
+ }
28
+ if (this.isExpired(entry)) {
29
+ await this.delete(key);
30
+ this.incrementStat('misses');
31
+ return undefined;
32
+ }
33
+ this.updateLRU(key);
34
+ entry.lastAccessedAt = Date.now();
35
+ entry.accessCount++;
36
+ this.incrementStat('hits');
37
+ return entry.value;
38
+ }
39
+ async set(key, value, ttlSeconds) {
40
+ // Acquire capacity lock for eviction check
41
+ await this.waitForCapacityLock();
42
+ const capacityLock = this.acquireCapacityLock();
43
+ try {
44
+ await capacityLock;
45
+ // Check and ensure capacity with global lock
46
+ await this.checkMemoryLimit();
47
+ await this.ensureCapacity();
48
+ }
49
+ finally {
50
+ this.releaseCapacityLock();
51
+ }
52
+ // Now acquire key-specific lock for the actual set
53
+ const lock = this.acquireLock(key);
54
+ try {
55
+ await lock;
56
+ const ttl = ttlSeconds ?? this.options.defaultTTL;
57
+ const now = Date.now();
58
+ const entry = {
59
+ value,
60
+ ttl: ttl > 0 ? ttl * 1000 : undefined,
61
+ createdAt: now,
62
+ lastAccessedAt: now,
63
+ accessCount: 0
64
+ };
65
+ this.cache.set(key, entry);
66
+ this.updateLRU(key);
67
+ this.incrementStat('sets');
68
+ }
69
+ finally {
70
+ this.releaseLock(key);
71
+ }
72
+ }
73
+ async delete(key) {
74
+ const lock = this.acquireLock(key);
75
+ try {
76
+ await lock;
77
+ const existed = this.cache.has(key);
78
+ if (existed) {
79
+ const entry = this.cache.get(key);
80
+ this.cache.delete(key);
81
+ this.removeLRU(key);
82
+ this.incrementStat('deletes');
83
+ if (entry && this.options.onEviction) {
84
+ this.options.onEviction(key, entry.value);
85
+ }
86
+ }
87
+ return existed;
88
+ }
89
+ finally {
90
+ this.releaseLock(key);
91
+ }
92
+ }
93
+ async clear() {
94
+ const keys = Array.from(this.cache.keys());
95
+ for (const key of keys) {
96
+ await this.waitForLock(key);
97
+ }
98
+ this.cache.clear();
99
+ this.lruOrder = [];
100
+ this.incrementStat('clears');
101
+ }
102
+ async has(key) {
103
+ await this.waitForLock(key);
104
+ const entry = this.cache.get(key);
105
+ if (!entry) {
106
+ return false;
107
+ }
108
+ if (this.isExpired(entry)) {
109
+ await this.delete(key);
110
+ return false;
111
+ }
112
+ return true;
113
+ }
114
+ async size() {
115
+ return this.cache.size;
116
+ }
117
+ async keys() {
118
+ const validKeys = [];
119
+ for (const [key, entry] of this.cache.entries()) {
120
+ if (!this.isExpired(entry)) {
121
+ validKeys.push(key);
122
+ }
123
+ }
124
+ return validKeys;
125
+ }
126
+ async getMemoryUsage() {
127
+ let totalSize = 0;
128
+ for (const [key, entry] of this.cache.entries()) {
129
+ totalSize += this.estimateSize(key, entry);
130
+ }
131
+ return totalSize;
132
+ }
133
+ destroy() {
134
+ if (this.cleanupInterval) {
135
+ clearInterval(this.cleanupInterval);
136
+ this.cleanupInterval = undefined;
137
+ }
138
+ this.cache.clear();
139
+ this.lruOrder = [];
140
+ this.locks.clear();
141
+ }
142
+ isExpired(entry) {
143
+ if (!entry.ttl) {
144
+ return false;
145
+ }
146
+ return Date.now() - entry.createdAt > entry.ttl;
147
+ }
148
+ updateLRU(key) {
149
+ const index = this.lruOrder.indexOf(key);
150
+ if (index > -1) {
151
+ this.lruOrder.splice(index, 1);
152
+ }
153
+ this.lruOrder.push(key);
154
+ }
155
+ removeLRU(key) {
156
+ const index = this.lruOrder.indexOf(key);
157
+ if (index > -1) {
158
+ this.lruOrder.splice(index, 1);
159
+ }
160
+ }
161
+ async ensureCapacity() {
162
+ // Need to ensure we have room for one more entry
163
+ if (this.cache.size >= this.options.maxEntries && this.lruOrder.length > 0) {
164
+ // Batch evict to make room
165
+ const entriesToEvict = Math.max(1, this.cache.size - this.options.maxEntries + 1);
166
+ const keysToEvict = this.lruOrder.slice(0, Math.min(entriesToEvict, this.lruOrder.length));
167
+ // Batch delete without recursion
168
+ for (const key of keysToEvict) {
169
+ const entry = this.cache.get(key);
170
+ if (entry) {
171
+ this.cache.delete(key);
172
+ this.removeLRU(key);
173
+ this.incrementStat('evictions');
174
+ this.incrementStat('deletes');
175
+ if (this.options.onEviction) {
176
+ this.options.onEviction(key, entry.value);
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+ async checkMemoryLimit() {
183
+ const memoryUsageMB = (await this.getMemoryUsage()) / (1024 * 1024);
184
+ if (memoryUsageMB > this.memoryOptions.maxMemoryMB && this.lruOrder.length > 0) {
185
+ // Calculate how many entries to evict (at least 10% of cache or 1 entry)
186
+ const entriesToEvict = Math.max(1, Math.ceil(this.cache.size * 0.1));
187
+ const keysToEvict = this.lruOrder.slice(0, Math.min(entriesToEvict, this.lruOrder.length));
188
+ for (const key of keysToEvict) {
189
+ await this.delete(key);
190
+ this.incrementStat('evictions');
191
+ }
192
+ }
193
+ }
194
+ estimateSize(key, entry) {
195
+ let size = key.length * 2;
196
+ size += 8 * 4;
197
+ const value = entry.value;
198
+ if (value === null || value === undefined) {
199
+ return size;
200
+ }
201
+ if (typeof value === 'string') {
202
+ size += value.length * 2;
203
+ }
204
+ else if (typeof value === 'number') {
205
+ size += 8;
206
+ }
207
+ else if (typeof value === 'boolean') {
208
+ size += 4;
209
+ }
210
+ else if (typeof value === 'object') {
211
+ try {
212
+ size += JSON.stringify(value).length * 2;
213
+ }
214
+ catch (error) {
215
+ // Handle circular references or other JSON errors
216
+ // Estimate size based on object key count
217
+ if (error instanceof Error && error.message.includes('circular')) {
218
+ const keys = Object.keys(value).length;
219
+ size += keys * 50; // Rough estimate: 50 bytes per key
220
+ }
221
+ else {
222
+ size += 1024; // Default fallback
223
+ }
224
+ }
225
+ }
226
+ return size;
227
+ }
228
+ startCleanupInterval() {
229
+ this.cleanupInterval = setInterval(async () => {
230
+ await this.cleanupExpired();
231
+ }, this.memoryOptions.checkInterval);
232
+ if (this.cleanupInterval.unref) {
233
+ this.cleanupInterval.unref();
234
+ }
235
+ }
236
+ async cleanupExpired() {
237
+ const keysToDelete = [];
238
+ for (const [key, entry] of this.cache.entries()) {
239
+ if (this.isExpired(entry)) {
240
+ keysToDelete.push(key);
241
+ }
242
+ }
243
+ for (const key of keysToDelete) {
244
+ await this.delete(key);
245
+ }
246
+ }
247
+ acquireLock(key) {
248
+ const existingLock = this.locks.get(key);
249
+ if (existingLock) {
250
+ const newLock = existingLock.then(() => this.createLockPromise());
251
+ this.locks.set(key, newLock);
252
+ return newLock;
253
+ }
254
+ const lock = this.createLockPromise();
255
+ this.locks.set(key, lock);
256
+ return lock;
257
+ }
258
+ // Simple async lock for Node.js single-threaded environment
259
+ // Note: Node.js JavaScript execution is single-threaded
260
+ createLockPromise() {
261
+ return new Promise(resolve => {
262
+ // Use nextTick for immediate execution in next iteration
263
+ process.nextTick(resolve);
264
+ });
265
+ }
266
+ releaseLock(key) {
267
+ this.locks.delete(key);
268
+ }
269
+ async waitForLock(key) {
270
+ const lock = this.locks.get(key);
271
+ if (lock) {
272
+ await lock;
273
+ }
274
+ }
275
+ acquireCapacityLock() {
276
+ if (this.capacityLock) {
277
+ const newLock = this.capacityLock.then(() => this.createLockPromise());
278
+ this.capacityLock = newLock;
279
+ return newLock;
280
+ }
281
+ const lock = this.createLockPromise();
282
+ this.capacityLock = lock;
283
+ return lock;
284
+ }
285
+ releaseCapacityLock() {
286
+ this.capacityLock = null;
287
+ }
288
+ async waitForCapacityLock() {
289
+ if (this.capacityLock) {
290
+ await this.capacityLock;
291
+ }
292
+ }
293
+ }
294
+ exports.MemoryCache = MemoryCache;
295
+ //# sourceMappingURL=memoryCache.js.map
@@ -123,6 +123,34 @@ export declare class FlongoQuery implements ICollectionQuery {
123
123
  * @returns This query instance for chaining
124
124
  */
125
125
  arrContainsAny(val?: any[]): FlongoQuery;
126
+ /**
127
+ * Matches array elements that satisfy all specified conditions.
128
+ * Use this to query objects within arrays by their properties.
129
+ *
130
+ * @example
131
+ * // Find users with a contextRole where orgId is 'org1'
132
+ * query.where('contextRoles').elemMatch({ orgId: 'org1' })
133
+ *
134
+ * @example
135
+ * // Find users with a contextRole where orgId is 'org1' AND role is 'admin'
136
+ * query.where('contextRoles').elemMatch({ orgId: 'org1', role: 'admin' })
137
+ *
138
+ * @example
139
+ * // Using fluent API for conditions (supports all FlongoQuery operators)
140
+ * query.where('contextRoles').elemMatch(
141
+ * new FlongoQuery().where('orgId').eq('org1').and('role').eq('admin')
142
+ * )
143
+ *
144
+ * @example
145
+ * // Using comparison operators
146
+ * query.where('scores').elemMatch(
147
+ * new FlongoQuery().where('value').gtEq(80).and('subject').eq('math')
148
+ * )
149
+ *
150
+ * @param conditions - Object with field/value pairs or a FlongoQuery instance
151
+ * @returns This query instance for chaining
152
+ */
153
+ elemMatch(conditions?: Record<string, any> | FlongoQuery): FlongoQuery;
126
154
  /**
127
155
  * Adds case-insensitive string starts-with constraint
128
156
  * @param val - String prefix to match
@@ -175,6 +175,36 @@ class FlongoQuery {
175
175
  arrContainsAny(val) {
176
176
  return val?.length ? this.set("$in", val) : this.handleEmptyValue();
177
177
  }
178
+ /**
179
+ * Matches array elements that satisfy all specified conditions.
180
+ * Use this to query objects within arrays by their properties.
181
+ *
182
+ * @example
183
+ * // Find users with a contextRole where orgId is 'org1'
184
+ * query.where('contextRoles').elemMatch({ orgId: 'org1' })
185
+ *
186
+ * @example
187
+ * // Find users with a contextRole where orgId is 'org1' AND role is 'admin'
188
+ * query.where('contextRoles').elemMatch({ orgId: 'org1', role: 'admin' })
189
+ *
190
+ * @example
191
+ * // Using fluent API for conditions (supports all FlongoQuery operators)
192
+ * query.where('contextRoles').elemMatch(
193
+ * new FlongoQuery().where('orgId').eq('org1').and('role').eq('admin')
194
+ * )
195
+ *
196
+ * @example
197
+ * // Using comparison operators
198
+ * query.where('scores').elemMatch(
199
+ * new FlongoQuery().where('value').gtEq(80).and('subject').eq('math')
200
+ * )
201
+ *
202
+ * @param conditions - Object with field/value pairs or a FlongoQuery instance
203
+ * @returns This query instance for chaining
204
+ */
205
+ elemMatch(conditions) {
206
+ return this.set("$elemMatch", conditions);
207
+ }
178
208
  // ===========================================
179
209
  // STRING OPERATORS
180
210
  // ===========================================
@@ -366,11 +396,25 @@ class FlongoQuery {
366
396
  break; // _id queries are typically exclusive
367
397
  }
368
398
  // Build query object for non-_id fields
369
- const fieldValue = expression.op
370
- ? expression.op === "$regex"
371
- ? { [expression.op]: expression.val, ["$options"]: "i" } // Case-insensitive regex
372
- : { [expression.op]: expression.val }
373
- : expression.val; // Direct value assignment for simple equality
399
+ let fieldValue;
400
+ if (!expression.op) {
401
+ // Direct value assignment for simple equality (arrContains)
402
+ fieldValue = expression.val;
403
+ }
404
+ else if (expression.op === "$regex") {
405
+ // Case-insensitive regex
406
+ fieldValue = { [expression.op]: expression.val, ["$options"]: "i" };
407
+ }
408
+ else if (expression.op === "$elemMatch") {
409
+ // Handle FlongoQuery or raw object for $elemMatch
410
+ fieldValue = {
411
+ [expression.op]: expression.val instanceof FlongoQuery ? expression.val.build() : expression.val
412
+ };
413
+ }
414
+ else {
415
+ // Standard operator
416
+ fieldValue = { [expression.op]: expression.val };
417
+ }
374
418
  // Merge operators for the same field (e.g., range queries)
375
419
  if (mongodbQuery[expression.key] &&
376
420
  typeof mongodbQuery[expression.key] === "object" &&
package/dist/index.d.ts CHANGED
@@ -2,5 +2,6 @@ export { FlongoCollection, FlongoCollectionOptions } from "./flongoCollection";
2
2
  export { FlongoQuery, FlongoQueryBuilder } from "./flongoQuery";
3
3
  export { initializeFlongo, FlongoConfig, flongoClient, flongoDb } from "./flongo";
4
4
  export { Error404, Error400 } from "./errors";
5
- export { Entity, DbRecord, Pagination, Coordinates, Bounds, Event, EventName, EventRecord, Logic, SortDirection, ColRange, ColExpression, ICollectionQuery, ICollection, Repository } from "./types";
5
+ export { Entity, DbRecord, Pagination, Coordinates, Bounds, Event, EventName, EventRecord, Logic, SortDirection, ColRange, ColExpression, ICollectionQuery, ICollection, Repository, CacheOptions, CachedCollectionOptions } from "./types";
6
+ export { CacheStore, CacheEntry, CacheStats, CacheStoreOptions, BaseCacheStore, MemoryCache, MemoryCacheOptions, CacheKeyGenerator, CacheKeyOptions, InvalidationStrategy, InvalidationRule, InvalidationOptions, CacheInvalidator, TTLStrategy, LRUStrategy, CacheConfig, CacheProviderConfig, CacheConfiguration, createDefaultConfig, createProductionConfig, createDevelopmentConfig, DetailedCacheStats, CacheMetrics, CacheStatsCollector, CacheMonitor, getGlobalCacheMonitor, resetGlobalCacheMonitor } from "./cache";
6
7
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  // Main exports for flongo package
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.ColExpression = exports.ColRange = exports.SortDirection = exports.Logic = exports.EventName = exports.Error400 = exports.Error404 = exports.flongoDb = exports.flongoClient = exports.initializeFlongo = exports.FlongoQueryBuilder = exports.FlongoQuery = exports.FlongoCollection = void 0;
4
+ exports.resetGlobalCacheMonitor = exports.getGlobalCacheMonitor = exports.CacheMonitor = exports.CacheStatsCollector = exports.createDevelopmentConfig = exports.createProductionConfig = exports.createDefaultConfig = exports.CacheConfiguration = exports.LRUStrategy = exports.TTLStrategy = exports.CacheInvalidator = exports.InvalidationStrategy = exports.CacheKeyGenerator = exports.MemoryCache = exports.BaseCacheStore = exports.ColExpression = exports.ColRange = exports.SortDirection = exports.Logic = exports.EventName = exports.Error400 = exports.Error404 = exports.flongoDb = exports.flongoClient = exports.initializeFlongo = exports.FlongoQueryBuilder = exports.FlongoQuery = exports.FlongoCollection = void 0;
5
5
  var flongoCollection_1 = require("./flongoCollection");
6
6
  Object.defineProperty(exports, "FlongoCollection", { enumerable: true, get: function () { return flongoCollection_1.FlongoCollection; } });
7
7
  var flongoQuery_1 = require("./flongoQuery");
@@ -20,4 +20,21 @@ Object.defineProperty(exports, "Logic", { enumerable: true, get: function () { r
20
20
  Object.defineProperty(exports, "SortDirection", { enumerable: true, get: function () { return types_1.SortDirection; } });
21
21
  Object.defineProperty(exports, "ColRange", { enumerable: true, get: function () { return types_1.ColRange; } });
22
22
  Object.defineProperty(exports, "ColExpression", { enumerable: true, get: function () { return types_1.ColExpression; } });
23
+ // Cache exports
24
+ var cache_1 = require("./cache");
25
+ Object.defineProperty(exports, "BaseCacheStore", { enumerable: true, get: function () { return cache_1.BaseCacheStore; } });
26
+ Object.defineProperty(exports, "MemoryCache", { enumerable: true, get: function () { return cache_1.MemoryCache; } });
27
+ Object.defineProperty(exports, "CacheKeyGenerator", { enumerable: true, get: function () { return cache_1.CacheKeyGenerator; } });
28
+ Object.defineProperty(exports, "InvalidationStrategy", { enumerable: true, get: function () { return cache_1.InvalidationStrategy; } });
29
+ Object.defineProperty(exports, "CacheInvalidator", { enumerable: true, get: function () { return cache_1.CacheInvalidator; } });
30
+ Object.defineProperty(exports, "TTLStrategy", { enumerable: true, get: function () { return cache_1.TTLStrategy; } });
31
+ Object.defineProperty(exports, "LRUStrategy", { enumerable: true, get: function () { return cache_1.LRUStrategy; } });
32
+ Object.defineProperty(exports, "CacheConfiguration", { enumerable: true, get: function () { return cache_1.CacheConfiguration; } });
33
+ Object.defineProperty(exports, "createDefaultConfig", { enumerable: true, get: function () { return cache_1.createDefaultConfig; } });
34
+ Object.defineProperty(exports, "createProductionConfig", { enumerable: true, get: function () { return cache_1.createProductionConfig; } });
35
+ Object.defineProperty(exports, "createDevelopmentConfig", { enumerable: true, get: function () { return cache_1.createDevelopmentConfig; } });
36
+ Object.defineProperty(exports, "CacheStatsCollector", { enumerable: true, get: function () { return cache_1.CacheStatsCollector; } });
37
+ Object.defineProperty(exports, "CacheMonitor", { enumerable: true, get: function () { return cache_1.CacheMonitor; } });
38
+ Object.defineProperty(exports, "getGlobalCacheMonitor", { enumerable: true, get: function () { return cache_1.getGlobalCacheMonitor; } });
39
+ Object.defineProperty(exports, "resetGlobalCacheMonitor", { enumerable: true, get: function () { return cache_1.resetGlobalCacheMonitor; } });
23
40
  //# sourceMappingURL=index.js.map
package/dist/types.d.ts CHANGED
@@ -86,4 +86,21 @@ export type ICollection<T> = {
86
86
  updateFirst: (attributes: any, filters?: any, clientId?: string) => Promise<Entity & T>;
87
87
  };
88
88
  export type Repository = string;
89
+ export interface CacheOptions {
90
+ enabled?: boolean;
91
+ ttlSeconds?: number;
92
+ maxEntries?: number;
93
+ maxMemoryMB?: number;
94
+ invalidateOnWrite?: boolean;
95
+ customKey?: string;
96
+ }
97
+ export interface CachedCollectionOptions {
98
+ enableCaching?: boolean;
99
+ cacheConfig?: {
100
+ maxEntries?: number;
101
+ ttlSeconds?: number;
102
+ provider?: 'memory' | 'redis' | 'memcached' | 'custom';
103
+ enableStats?: boolean;
104
+ };
105
+ }
89
106
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flongo",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "Firestore-like fluent query API for MongoDB",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",