@goatlab/fluent-loki 0.8.3 → 0.9.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.
@@ -17,11 +17,14 @@ class LokiConnector extends fluent_1.BaseConnector {
17
17
  this.outputSchema =
18
18
  outputSchema || inputSchema;
19
19
  this.entity = entity;
20
- const dbModels = [];
21
- for (const collection of dataSource.collections) {
22
- dbModels.push(collection.name);
20
+ // Use Set for O(1) lookup instead of array
21
+ const dbModels = new Set();
22
+ const collections = dataSource.collections;
23
+ const collectionsLength = collections.length;
24
+ for (let i = 0; i < collectionsLength; i++) {
25
+ dbModels.add(collections[i].name);
23
26
  }
24
- if (!dbModels.includes(entity.name)) {
27
+ if (!dbModels.has(entity.name)) {
25
28
  dataSource.addCollection(entity.name);
26
29
  }
27
30
  this.dataSource = dataSource;
@@ -54,21 +57,26 @@ class LokiConnector extends fluent_1.BaseConnector {
54
57
  }
55
58
  async insertMany(data) {
56
59
  const validatedData = this.inputSchema.array().parse(data);
57
- const insertedElements = [];
58
- for (const data of validatedData) {
59
- const now = new Date();
60
- insertedElements.push({
61
- ...data,
62
- id: js_utils_1.Ids.uuid(),
60
+ const dataLength = validatedData.length;
61
+ const insertedElements = new Array(dataLength);
62
+ const now = new Date();
63
+ const nowTime = now.getTime(); // Cache timestamp for reuse
64
+ for (let i = 0; i < dataLength; i++) {
65
+ const id = js_utils_1.Ids.uuid();
66
+ insertedElements[i] = {
67
+ ...validatedData[i],
68
+ id,
63
69
  created: now,
64
70
  createdAt: now,
65
71
  updatedAt: now
66
- });
72
+ };
67
73
  }
68
74
  await this.collection.insert(insertedElements);
69
- return this.outputSchema.array().parse(insertedElements.map(d => {
70
- return js_utils_1.Objects.clearEmpties(js_utils_1.Objects.deleteNulls(d));
71
- }));
75
+ const cleanedResults = new Array(dataLength);
76
+ for (let i = 0; i < dataLength; i++) {
77
+ cleanedResults[i] = js_utils_1.Objects.clearEmpties(js_utils_1.Objects.deleteNulls(insertedElements[i]));
78
+ }
79
+ return this.outputSchema.array().parse(cleanedResults);
72
80
  }
73
81
  /**
74
82
  *
@@ -83,40 +91,51 @@ class LokiConnector extends fluent_1.BaseConnector {
83
91
  }
84
92
  async findMany(query) {
85
93
  const where = this.getLokiWhere(query?.where);
86
- const sort = [];
87
94
  let baseQuery = this.collection
88
95
  .chain()
89
96
  .find(where);
90
- // Pagination
91
- if (query?.paginated) {
92
- baseQuery.limit(query.paginated.perPage);
93
- baseQuery.offset((query.paginated?.page - 1) * query.paginated.perPage);
94
- }
97
+ // Build sort array if needed - pre-calculate total size
95
98
  if (query?.orderBy) {
96
- for (const order of query?.orderBy) {
97
- const flattenObject = js_utils_1.Objects.flatten(order);
98
- for (const attribute of Object.keys(flattenObject)) {
99
- const isDecending = flattenObject[attribute] === 'desc';
100
- sort.push([attribute, isDecending]);
99
+ let totalSortFields = 0;
100
+ const orderLength = query.orderBy.length;
101
+ // First pass: count total fields
102
+ for (let i = 0; i < orderLength; i++) {
103
+ const flattenObject = js_utils_1.Objects.flatten(query.orderBy[i]);
104
+ totalSortFields += Object.keys(flattenObject).length;
105
+ }
106
+ // Pre-allocate sort array
107
+ const sort = new Array(totalSortFields);
108
+ let sortIndex = 0;
109
+ // Second pass: populate array
110
+ for (let i = 0; i < orderLength; i++) {
111
+ const flattenObject = js_utils_1.Objects.flatten(query.orderBy[i]);
112
+ const attributes = Object.keys(flattenObject);
113
+ const attrLength = attributes.length;
114
+ for (let j = 0; j < attrLength; j++) {
115
+ const attribute = attributes[j];
116
+ const isDescending = flattenObject[attribute] === 'desc';
117
+ sort[sortIndex++] = [attribute, isDescending];
101
118
  }
102
119
  }
103
120
  baseQuery = baseQuery.compoundsort(sort);
104
121
  }
105
- // Apply offset and limit after sorting
106
- if (query?.offset) {
107
- baseQuery = baseQuery.offset(query.offset);
108
- }
109
- if (query?.limit) {
110
- baseQuery = baseQuery.limit(query.limit);
122
+ // Apply pagination, offset and limit
123
+ if (query?.paginated) {
124
+ const offset = (query.paginated.page - 1) * query.paginated.perPage;
125
+ baseQuery = baseQuery.offset(offset).limit(query.paginated.perPage);
111
126
  }
112
- else if (!query?.paginated) {
113
- // Default limit if no pagination
114
- baseQuery = baseQuery.limit(10);
127
+ else {
128
+ if (query?.offset) {
129
+ baseQuery = baseQuery.offset(query.offset);
130
+ }
131
+ baseQuery = baseQuery.limit(query?.limit || 10);
115
132
  }
116
133
  let found = baseQuery.data();
117
- found.map(d => {
118
- js_utils_1.Objects.clearEmpties(js_utils_1.Objects.deleteNulls(d));
119
- });
134
+ // Clean data in-place
135
+ const foundLength = found.length;
136
+ for (let i = 0; i < foundLength; i++) {
137
+ js_utils_1.Objects.clearEmpties(js_utils_1.Objects.deleteNulls(found[i]));
138
+ }
120
139
  if (query?.paginated) {
121
140
  const paginationInfo = {
122
141
  total: 0,
@@ -139,15 +158,19 @@ class LokiConnector extends fluent_1.BaseConnector {
139
158
  }
140
159
  // Validate Output against schema
141
160
  // Use partial schema to handle objects that may have been replaced with partial data
142
- const validatedResults = found.map(item => {
161
+ // Optimize validation by caching partial schema
162
+ const partialSchema = this.outputSchema.partial();
163
+ const validatedResults = new Array(found.length);
164
+ for (let i = 0; i < found.length; i++) {
165
+ const item = found[i];
143
166
  try {
144
- return this.outputSchema.parse(item);
167
+ validatedResults[i] = this.outputSchema.parse(item);
145
168
  }
146
169
  catch (e) {
147
170
  // If full validation fails, try partial validation
148
- return this.outputSchema.partial().parse(item);
171
+ validatedResults[i] = partialSchema.parse(item);
149
172
  }
150
- });
173
+ }
151
174
  return validatedResults;
152
175
  }
153
176
  /**
@@ -193,11 +216,16 @@ class LokiConnector extends fluent_1.BaseConnector {
193
216
  * @param data
194
217
  */
195
218
  async replaceById(id, data) {
196
- let value = await this.collection.findOne({ id });
197
- const flatValue = js_utils_1.Objects.flatten(JSON.parse(JSON.stringify(value)));
198
- Object.keys(flatValue).forEach(key => {
199
- flatValue[key] = null;
200
- });
219
+ const value = await this.collection.findOne({ id });
220
+ // Avoid JSON parse/stringify overhead
221
+ const clonedValue = typeof structuredClone !== 'undefined' ? structuredClone(value) : JSON.parse(JSON.stringify(value));
222
+ const flatValue = js_utils_1.Objects.flatten(clonedValue);
223
+ const keys = Object.keys(flatValue);
224
+ const keysLength = keys.length;
225
+ const nullValue = null;
226
+ for (let i = 0; i < keysLength; i++) {
227
+ flatValue[keys[i]] = nullValue;
228
+ }
201
229
  const nullObject = js_utils_1.Objects.nest(flatValue);
202
230
  const newValue = { ...nullObject, ...data };
203
231
  delete newValue._id;
@@ -207,7 +235,7 @@ class LokiConnector extends fluent_1.BaseConnector {
207
235
  const dataToInsert = this.outputKeys.includes('updated')
208
236
  ? {
209
237
  ...data,
210
- ...{ updated: new Date() }
238
+ updated: new Date()
211
239
  }
212
240
  : data;
213
241
  // For replace operations, use partial validation since we're replacing with only provided fields
@@ -220,12 +248,16 @@ class LokiConnector extends fluent_1.BaseConnector {
220
248
  };
221
249
  // Remove all fields except LokiJS metadata and validated fields
222
250
  const lokiMetaFields = ['$loki', 'meta'];
223
- const allowedFields = [...lokiMetaFields, 'id', 'created', ...Object.keys(validatedData)];
224
- Object.keys(updatedValue).forEach(key => {
225
- if (!allowedFields.includes(key)) {
251
+ const validatedKeys = Object.keys(validatedData);
252
+ const allowedFields = new Set([...lokiMetaFields, 'id', 'created', ...validatedKeys]);
253
+ const updatedKeys = Object.keys(updatedValue);
254
+ const updatedKeysLength = updatedKeys.length;
255
+ for (let i = 0; i < updatedKeysLength; i++) {
256
+ const key = updatedKeys[i];
257
+ if (!allowedFields.has(key)) {
226
258
  delete updatedValue[key];
227
259
  }
228
- });
260
+ }
229
261
  await this.collection.update(updatedValue);
230
262
  const val = await this.collection.findOne({ id });
231
263
  // For replace operations, use partial output schema since we only have the replaced fields
@@ -233,61 +265,44 @@ class LokiConnector extends fluent_1.BaseConnector {
233
265
  return partialOutputSchema.parse(js_utils_1.Objects.clearEmpties(js_utils_1.Objects.deleteNulls(val)));
234
266
  }
235
267
  getLokiWhere(where) {
236
- /*
237
-
238
- if (this.relationQuery && this.relationQuery.data) {
239
- const ids = this.relationQuery.data.map(
240
- d => Ids.objectID(d.id) as unknown as ObjectID
241
- )
242
-
243
- andFilters.push([
244
- this.relationQuery.relation.inverseSidePropertyPath,
245
- 'in',
246
- ids
247
- ])
248
- }
249
-
250
- if (!andFilters || andFilters.length === 0) {
251
- return filters
252
- }
253
- */
254
268
  if (!where || Object.keys(where).length === 0) {
255
269
  return {};
256
270
  }
257
271
  const Filters = {
258
272
  where: { $or: [{ $and: [] }] }
259
273
  };
260
- const orConditions = (0, fluent_1.extractConditions)((where['OR'] || []));
261
- const andConditions = (0, fluent_1.extractConditions)((where['AND'] || []));
262
- const copy = js_utils_1.Objects.clone(where);
263
- if (!!copy['AND']) {
264
- delete copy['AND'];
265
- }
266
- if (!!copy['OR']) {
267
- delete copy['OR'];
268
- }
269
- const rootLevelConditions = (0, fluent_1.extractConditions)([copy]);
270
- // Process AND conditions
271
- for (const condition of andConditions) {
272
- let { element, operator, value } = condition;
273
- if (element === 'id') {
274
- // element = '_id'
275
- /*
276
- value = (Array.isArray(value)
277
- ? value.map(v => Ids.objectID(v) as unknown as ObjectID)
278
- : (Ids.objectID(value) as unknown as ObjectID) as unknown as PrimitivesArray | Primitives)
279
- */
280
- }
274
+ // Avoid cloning overhead - use destructuring
275
+ const { AND, OR, ...rootConditions } = where;
276
+ const orConditions = (0, fluent_1.extractConditions)((OR || []));
277
+ const andConditions = (0, fluent_1.extractConditions)((AND || []));
278
+ const rootLevelConditions = (0, fluent_1.extractConditions)([rootConditions]);
279
+ // Create operator map for O(1) lookup
280
+ const simpleOperatorMap = new Map([
281
+ [fluent_1.LogicOperator.equals, '$eq'],
282
+ [fluent_1.LogicOperator.isNot, '$neq'],
283
+ [fluent_1.LogicOperator.greaterThan, '$gt'],
284
+ [fluent_1.LogicOperator.greaterOrEqualThan, '$gte'],
285
+ [fluent_1.LogicOperator.lessThan, '$lt'],
286
+ [fluent_1.LogicOperator.lessOrEqualThan, '$lte'],
287
+ [fluent_1.LogicOperator.in, '$in'],
288
+ [fluent_1.LogicOperator.exists, '$exists'],
289
+ [fluent_1.LogicOperator.notExists, '$exists'],
290
+ [fluent_1.LogicOperator.regexp, '$regex']
291
+ ]);
292
+ // Helper function to process conditions
293
+ const processCondition = (condition, target) => {
294
+ const { element, operator, value } = condition;
281
295
  // Handle nested properties for LokiJS
282
296
  if (element.includes('.')) {
283
297
  const parts = element.split('.');
284
298
  const nestedFilter = {};
285
299
  let current = nestedFilter;
286
- for (let i = 0; i < parts.length - 1; i++) {
300
+ const partsLength = parts.length - 1;
301
+ for (let i = 0; i < partsLength; i++) {
287
302
  current[parts[i]] = {};
288
303
  current = current[parts[i]];
289
304
  }
290
- const lastPart = parts[parts.length - 1];
305
+ const lastPart = parts[partsLength];
291
306
  switch (operator) {
292
307
  case fluent_1.LogicOperator.equals:
293
308
  current[lastPart] = value;
@@ -323,184 +338,60 @@ class LokiConnector extends fluent_1.BaseConnector {
323
338
  current[lastPart] = { $regex: value };
324
339
  break;
325
340
  }
326
- Filters.where.$or[0].$and.push(nestedFilter);
341
+ target.push(nestedFilter);
327
342
  }
328
343
  else {
329
- switch (operator) {
330
- case fluent_1.LogicOperator.equals:
331
- Filters.where.$or[0].$and.push({ [element]: { $eq: value } });
332
- break;
333
- case fluent_1.LogicOperator.isNot:
334
- Filters.where.$or[0].$and.push({ [element]: { $neq: value } });
335
- break;
336
- case fluent_1.LogicOperator.greaterThan:
337
- Filters.where.$or[0].$and.push({ [element]: { $gt: value } });
338
- break;
339
- case fluent_1.LogicOperator.greaterOrEqualThan:
340
- Filters.where.$or[0].$and.push({ [element]: { $gte: value } });
341
- break;
342
- case fluent_1.LogicOperator.lessThan:
343
- Filters.where.$or[0].$and.push({ [element]: { $lt: value } });
344
- break;
345
- case fluent_1.LogicOperator.lessOrEqualThan:
346
- Filters.where.$or[0].$and.push({ [element]: { $lte: value } });
347
- break;
348
- case fluent_1.LogicOperator.in:
349
- Filters.where.$or[0].$and.push({ [element]: { $in: value } });
350
- break;
351
- case fluent_1.LogicOperator.notIn:
352
- Filters.where.$or[0].$and.push({
353
- [element]: { $not: { $in: value } }
354
- });
355
- break;
356
- case fluent_1.LogicOperator.exists:
357
- Filters.where.$or[0].$and.push({ [element]: { $exists: true } });
358
- break;
359
- case fluent_1.LogicOperator.notExists:
360
- Filters.where.$or[0].$and.push({ [element]: { $exists: false } });
361
- break;
362
- case fluent_1.LogicOperator.regexp:
363
- Filters.where.$or[0].$and.push({ [element]: { $regex: value } });
364
- break;
344
+ // Use map for O(1) operator lookup
345
+ const lokiOp = simpleOperatorMap.get(operator);
346
+ if (lokiOp) {
347
+ if (operator === fluent_1.LogicOperator.notExists) {
348
+ target.push({ [element]: { [lokiOp]: false } });
349
+ }
350
+ else if (operator === fluent_1.LogicOperator.exists) {
351
+ target.push({ [element]: { [lokiOp]: true } });
352
+ }
353
+ else {
354
+ target.push({ [element]: { [lokiOp]: value } });
355
+ }
356
+ }
357
+ else if (operator === fluent_1.LogicOperator.notIn) {
358
+ target.push({ [element]: { $not: { $in: value } } });
365
359
  }
366
360
  }
361
+ };
362
+ // Process AND conditions
363
+ const andLength = andConditions.length;
364
+ for (let i = 0; i < andLength; i++) {
365
+ processCondition(andConditions[i], Filters.where.$or[0].$and);
367
366
  }
368
- for (const condition of rootLevelConditions) {
369
- let { element, operator, value } = condition;
370
- if (element === 'id') {
371
- // element = '_id'
372
- /*
373
- value = (Array.isArray(value)
374
- ? value.map(v => Ids.objectID(v) as unknown as ObjectID)
375
- : (Ids.objectID(value) as unknown as ObjectID) as unknown as PrimitivesArray | Primitives)
376
- */
377
- }
378
- // Handle nested properties for LokiJS
379
- if (element.includes('.')) {
380
- const parts = element.split('.');
381
- const nestedFilter = {};
382
- let current = nestedFilter;
383
- for (let i = 0; i < parts.length - 1; i++) {
384
- current[parts[i]] = {};
385
- current = current[parts[i]];
367
+ // Process root level conditions
368
+ const rootLength = rootLevelConditions.length;
369
+ for (let i = 0; i < rootLength; i++) {
370
+ processCondition(rootLevelConditions[i], Filters.where.$or[0].$and);
371
+ }
372
+ // Process OR conditions
373
+ const orLength = orConditions.length;
374
+ for (let i = 0; i < orLength; i++) {
375
+ const condition = orConditions[i];
376
+ const { element, operator, value } = condition;
377
+ const orFilter = {};
378
+ // Reuse operator map for consistency
379
+ const lokiOp = simpleOperatorMap.get(operator);
380
+ if (lokiOp) {
381
+ if (operator === fluent_1.LogicOperator.notExists) {
382
+ orFilter[element] = { [lokiOp]: false };
386
383
  }
387
- const lastPart = parts[parts.length - 1];
388
- switch (operator) {
389
- case fluent_1.LogicOperator.equals:
390
- current[lastPart] = value;
391
- break;
392
- case fluent_1.LogicOperator.isNot:
393
- current[lastPart] = { $ne: value };
394
- break;
395
- case fluent_1.LogicOperator.greaterThan:
396
- current[lastPart] = { $gt: value };
397
- break;
398
- case fluent_1.LogicOperator.greaterOrEqualThan:
399
- current[lastPart] = { $gte: value };
400
- break;
401
- case fluent_1.LogicOperator.lessThan:
402
- current[lastPart] = { $lt: value };
403
- break;
404
- case fluent_1.LogicOperator.lessOrEqualThan:
405
- current[lastPart] = { $lte: value };
406
- break;
407
- case fluent_1.LogicOperator.in:
408
- current[lastPart] = { $in: value };
409
- break;
410
- case fluent_1.LogicOperator.notIn:
411
- current[lastPart] = { $nin: value };
412
- break;
413
- case fluent_1.LogicOperator.exists:
414
- current[lastPart] = { $exists: true };
415
- break;
416
- case fluent_1.LogicOperator.notExists:
417
- current[lastPart] = { $exists: false };
418
- break;
419
- case fluent_1.LogicOperator.regexp:
420
- current[lastPart] = { $regex: value };
421
- break;
384
+ else if (operator === fluent_1.LogicOperator.exists) {
385
+ orFilter[element] = { [lokiOp]: true };
422
386
  }
423
- Filters.where.$or[0].$and.push(nestedFilter);
424
- }
425
- else {
426
- switch (operator) {
427
- case fluent_1.LogicOperator.equals:
428
- Filters.where.$or[0].$and.push({ [element]: { $eq: value } });
429
- break;
430
- case fluent_1.LogicOperator.isNot:
431
- Filters.where.$or[0].$and.push({ [element]: { $neq: value } });
432
- break;
433
- case fluent_1.LogicOperator.greaterThan:
434
- Filters.where.$or[0].$and.push({ [element]: { $gt: value } });
435
- break;
436
- case fluent_1.LogicOperator.greaterOrEqualThan:
437
- Filters.where.$or[0].$and.push({ [element]: { $gte: value } });
438
- break;
439
- case fluent_1.LogicOperator.lessThan:
440
- Filters.where.$or[0].$and.push({ [element]: { $lt: value } });
441
- break;
442
- case fluent_1.LogicOperator.lessOrEqualThan:
443
- Filters.where.$or[0].$and.push({ [element]: { $lte: value } });
444
- break;
445
- case fluent_1.LogicOperator.in:
446
- Filters.where.$or[0].$and.push({ [element]: { $in: value } });
447
- break;
448
- case fluent_1.LogicOperator.notIn:
449
- Filters.where.$or[0].$and.push({
450
- [element]: { $not: { $in: value } }
451
- });
452
- break;
453
- case fluent_1.LogicOperator.exists:
454
- Filters.where.$or[0].$and.push({ [element]: { $exists: true } });
455
- break;
456
- case fluent_1.LogicOperator.notExists:
457
- Filters.where.$or[0].$and.push({ [element]: { $exists: false } });
458
- break;
459
- case fluent_1.LogicOperator.regexp:
460
- Filters.where.$or[0].$and.push({ [element]: { $regex: value } });
461
- break;
387
+ else {
388
+ orFilter[element] = { [lokiOp]: value };
462
389
  }
463
390
  }
464
- }
465
- for (const condition of orConditions) {
466
- let { element, operator, value } = condition;
467
- switch (operator) {
468
- case fluent_1.LogicOperator.equals:
469
- Filters.where.$or.push({ [element]: { $eq: value } });
470
- break;
471
- case fluent_1.LogicOperator.isNot:
472
- Filters.where.$or.push({ [element]: { $neq: value } });
473
- break;
474
- case fluent_1.LogicOperator.greaterThan:
475
- Filters.where.$or.push({ [element]: { $gt: value } });
476
- break;
477
- case fluent_1.LogicOperator.greaterOrEqualThan:
478
- Filters.where.$or.push({ [element]: { $gte: value } });
479
- break;
480
- case fluent_1.LogicOperator.lessThan:
481
- Filters.where.$or.push({ [element]: { $lt: value } });
482
- break;
483
- case fluent_1.LogicOperator.lessOrEqualThan:
484
- Filters.where.$or.push({ [element]: { $lte: value } });
485
- break;
486
- case fluent_1.LogicOperator.in:
487
- Filters.where.$or.push({ [element]: { $in: value } });
488
- break;
489
- case fluent_1.LogicOperator.notIn:
490
- Filters.where.$or.push({
491
- [element]: { $not: { $in: value } }
492
- });
493
- break;
494
- case fluent_1.LogicOperator.exists:
495
- Filters.where.$or.push({ [element]: { $exists: true } });
496
- break;
497
- case fluent_1.LogicOperator.notExists:
498
- Filters.where.$or.push({ [element]: { $exists: false } });
499
- break;
500
- case fluent_1.LogicOperator.regexp:
501
- Filters.where.$or.push({ [element]: { $regex: value } });
502
- break;
391
+ else if (operator === fluent_1.LogicOperator.notIn) {
392
+ orFilter[element] = { $not: { $in: value } };
503
393
  }
394
+ Filters.where.$or.push(orFilter);
504
395
  }
505
396
  // For simple queries without OR conditions, return a simpler format
506
397
  if (Filters.where.$or.length === 1 && Filters.where.$or[0].$and.length === 1) {
@@ -508,23 +399,45 @@ class LokiConnector extends fluent_1.BaseConnector {
508
399
  // For nested objects, LokiJS needs { "breed.family": "Angora" } format
509
400
  // Check if this is a nested object filter
510
401
  const keys = Object.keys(filter);
511
- if (keys.length === 1 && typeof filter[keys[0]] === 'object' && !filter[keys[0]].$eq && !filter[keys[0]].$ne && !filter[keys[0]].$gt && !filter[keys[0]].$gte && !filter[keys[0]].$lt && !filter[keys[0]].$lte && !filter[keys[0]].$in && !filter[keys[0]].$nin && !filter[keys[0]].$exists && !filter[keys[0]].$regex) {
512
- // This is a nested object filter like { breed: { family: "Angora" } }
513
- // Convert to dot notation for LokiJS
514
- const result = {};
515
- const flattenNestedObject = (obj, prefix = '') => {
516
- for (const key in obj) {
517
- const fullKey = prefix ? `${prefix}.${key}` : key;
518
- if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key]) && !obj[key].$eq && !obj[key].$ne && !obj[key].$gt && !obj[key].$gte && !obj[key].$lt && !obj[key].$lte && !obj[key].$in && !obj[key].$nin && !obj[key].$exists && !obj[key].$regex) {
519
- flattenNestedObject(obj[key], fullKey);
402
+ if (keys.length === 1) {
403
+ const firstKey = keys[0];
404
+ const firstValue = filter[firstKey];
405
+ if (typeof firstValue === 'object' &&
406
+ firstValue !== null &&
407
+ !Array.isArray(firstValue) &&
408
+ !firstValue.$eq && !firstValue.$ne &&
409
+ !firstValue.$gt && !firstValue.$gte &&
410
+ !firstValue.$lt && !firstValue.$lte &&
411
+ !firstValue.$in && !firstValue.$nin &&
412
+ !firstValue.$exists && !firstValue.$regex) {
413
+ // This is a nested object filter like { breed: { family: "Angora" } }
414
+ // Convert to dot notation for LokiJS
415
+ const result = {};
416
+ const flattenNestedObject = (obj, prefix = '') => {
417
+ const objKeys = Object.keys(obj);
418
+ const objKeysLength = objKeys.length;
419
+ for (let i = 0; i < objKeysLength; i++) {
420
+ const key = objKeys[i];
421
+ const fullKey = prefix ? `${prefix}.${key}` : key;
422
+ const value = obj[key];
423
+ if (typeof value === 'object' &&
424
+ value !== null &&
425
+ !Array.isArray(value) &&
426
+ !value.$eq && !value.$ne &&
427
+ !value.$gt && !value.$gte &&
428
+ !value.$lt && !value.$lte &&
429
+ !value.$in && !value.$nin &&
430
+ !value.$exists && !value.$regex) {
431
+ flattenNestedObject(value, fullKey);
432
+ }
433
+ else {
434
+ result[fullKey] = value;
435
+ }
520
436
  }
521
- else {
522
- result[fullKey] = obj[key];
523
- }
524
- }
525
- };
526
- flattenNestedObject(filter);
527
- return result;
437
+ };
438
+ flattenNestedObject(filter);
439
+ return result;
440
+ }
528
441
  }
529
442
  return filter;
530
443
  }
@@ -545,16 +458,24 @@ class LokiConnector extends fluent_1.BaseConnector {
545
458
  loadFirst(query) {
546
459
  // Create a clone of the original class
547
460
  // to avoid polluting attributes (relatedQuery)
548
- const detachedClass = Object.assign(Object.create(Object.getPrototypeOf(this)), this);
461
+ const detachedClass = this.clone();
549
462
  detachedClass.setRelatedQuery({
550
463
  entity: this.entity,
551
464
  repository: this,
552
- query
465
+ query: {
466
+ ...query,
467
+ limit: 1
468
+ }
553
469
  });
554
470
  return detachedClass;
555
471
  }
556
472
  clone() {
557
- return new this.constructor();
473
+ return new this.constructor({
474
+ entity: this.entity,
475
+ dataSource: this.dataSource,
476
+ inputSchema: this.inputSchema,
477
+ outputSchema: this.outputSchema
478
+ });
558
479
  }
559
480
  loadById(id) {
560
481
  // Create a new instance to avoid polluting the original one
@@ -582,34 +503,37 @@ class LokiConnector extends fluent_1.BaseConnector {
582
503
  // Handle both string and object path formats
583
504
  const pathKey = typeof path === 'string' ? path : Object.keys(js_utils_1.Objects.flatten(path))[0];
584
505
  const result = [];
585
- for (const item of data) {
586
- const extracted = js_utils_1.Objects.getFromPath(item, String(pathKey), undefined);
506
+ const dataArray = data;
507
+ const dataLength = dataArray.length;
508
+ for (let i = 0; i < dataLength; i++) {
509
+ const extracted = js_utils_1.Objects.getFromPath(dataArray[i], String(pathKey), undefined);
587
510
  if (typeof extracted.value !== 'undefined') {
588
511
  result.push(extracted.value);
589
512
  }
590
513
  }
591
514
  return result;
592
515
  }
516
+ // Static map for better performance
517
+ static lokiOperatorMap = new Map([
518
+ ['=', '$eq'],
519
+ ['<', '$lt'],
520
+ ['>', '$gt'],
521
+ ['<=', '$lte'],
522
+ ['>=', '$gte'],
523
+ ['<>', '$ne'],
524
+ ['!=', '$ne'],
525
+ ['in', '$in'],
526
+ ['nin', '$nin'],
527
+ ['like', '$aeq'],
528
+ ['regexp', '$regex'],
529
+ ['startsWith', '$regex|^{{$var}}'],
530
+ ['endsWith', '$regex|{{$var}}$'],
531
+ ['contains', '$regex|{{$var}}']
532
+ ]);
593
533
  getLokiOperator(operator) {
594
- const lokiOperators = {
595
- '=': '$eq',
596
- '<': '$lt',
597
- '>': '$gt',
598
- '<=': '$lte',
599
- '>=': '$gte',
600
- '<>': '$ne',
601
- '!=': '$ne',
602
- in: '$in',
603
- nin: '$nin',
604
- like: '$aeq',
605
- regexp: '$regex',
606
- startsWith: '$regex|^{{$var}}',
607
- endsWith: '$regex|{{$var}}$',
608
- contains: '$regex|{{$var}}'
609
- };
610
- const converted = js_utils_1.Objects.get(() => lokiOperators[operator], undefined);
534
+ const converted = LokiConnector.lokiOperatorMap.get(operator);
611
535
  if (!converted) {
612
- throw new Error(`The operator "${operator}" is not supported in Loki `);
536
+ throw new Error(`The operator "${operator}" is not supported in Loki`);
613
537
  }
614
538
  return converted;
615
539
  }