querier-ts 2.5.3 → 2.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.6.0
4
+
5
+ ### Summary
6
+
7
+ - [ ] Bug fixes
8
+ - [ ] Code refactoring
9
+ - [x] New features
10
+ - [ ] Build and packaging updates
11
+ - [ ] Breaking changes
12
+
13
+ ### New features
14
+
15
+ - Added `limitPerGroup()` and `top()` methods to `Query`.
16
+
17
+ ## v2.5.4
18
+
19
+ ### Summary
20
+
21
+ - [ ] Bug fixes
22
+ - [x] Code refactoring
23
+ - [ ] New features
24
+ - [ ] Build and packaging updates
25
+ - [ ] Breaking changes
26
+
27
+ ### Build and packaging updates
28
+
29
+ - Replaced pre-allocated arrays by static arrays for improved performance.
30
+
3
31
  ## v2.5.3
4
32
 
5
33
  ### Summary
package/README.md CHANGED
@@ -165,7 +165,28 @@ const lastId = Query.from(users)
165
165
 
166
166
  ---
167
167
 
168
- ### Limiting results
168
+ ### Paginating results
169
+
170
+ #### `skip(numberOfRows)`
171
+
172
+ Skips the first results.
173
+
174
+ ```ts
175
+ .skip(5)
176
+ ```
177
+
178
+ Example:
179
+
180
+ ```ts
181
+ const secondId = Query.from(users)
182
+ .select('id')
183
+ .skip(1)
184
+ .scalar();
185
+ ```
186
+
187
+ > Passing a non-integer or a negative number will throw an `InvalidArgumentError`.
188
+
189
+ ---
169
190
 
170
191
  #### `limit(limit)`
171
192
 
@@ -179,24 +200,79 @@ Limits the number of results returned.
179
200
 
180
201
  ---
181
202
 
182
- #### `skip(numberOfRows)`
203
+ #### `limitPerGroup(key | fn, limit)`
183
204
 
184
- Skips the first results.
205
+ Limits the number of rows per group.
206
+
207
+ - `key`: The property name to group by.
208
+ - `fn`: A function that maps each row to a grouping value.
209
+ - `limit`: The maximum number of rows to keep per group.
210
+
211
+ > ⚠️ The rows kept depend on the current ordering of the query.
212
+ > Use `orderBy()` beforehand to control which rows are selected.
213
+
214
+ Examples:
185
215
 
186
216
  ```ts
187
- .skip(5)
217
+ // with key
218
+ const countries = Query.from(addresses)
219
+ .orderBy('-createdAt')
220
+ .limitPerGroup('country', 2)
221
+ .column('country'); // ['Argentina', 'Argentina', 'Brazil', 'Brazil', 'Chile', 'Chile']
222
+
223
+ // with callback
224
+ const countries = Query.from(addresses)
225
+ .orderBy('-createdAt')
226
+ .limitPerGroup((row) => row.country, 2)
227
+ .column('country');
188
228
  ```
189
229
 
190
- Example:
230
+ ---
231
+
232
+ #### `top(limit, options?)`
233
+
234
+ Keeps the top N rows, optionally partitioned by a key or callback.
235
+
236
+ - `limit`: The maximum number of rows to keep.
237
+ - `options.partitionBy`: A property name or function to define groups.
238
+ - `options.orderBy`: A column or list of columns to define ordering.
239
+
240
+ If `partitionBy` is provided, the limit is applied per group.
241
+
242
+ The rows kept depend on the ordering. Use `orderBy` (either here or before)
243
+ to control which rows are selected.
244
+
245
+ Examples:
191
246
 
192
247
  ```ts
193
- const secondId = Query.from(users)
194
- .select('id')
195
- .skip(1)
196
- .scalar();
248
+ // top N globally
249
+ const ids = Query.from(addresses)
250
+ .orderBy('-createdAt')
251
+ .top(3)
252
+ .column('id'); // [6, 5, 4]
253
+
254
+ // top N per group (key)
255
+ const countries = Query.from(addresses)
256
+ .top(2, {
257
+ partitionBy: 'country',
258
+ orderBy: '-createdAt',
259
+ })
260
+ .column('country'); // ['Argentina', 'Argentina', 'Brazil', 'Brazil', 'Chile', 'Chile']
261
+
262
+ // top N per group (callback)
263
+ const countries = Query.from(addresses)
264
+ .top(1, {
265
+ partitionBy: (row) => row.country,
266
+ orderBy: '-createdAt',
267
+ })
268
+ .column('country'); // ['Argentina', 'Brazil', 'Chile']
269
+
270
+ // without orderBy (keeps original order)
271
+ const countries = Query.from(addresses)
272
+ .top(1, { partitionBy: 'country' })
273
+ .column('country'); // ['Brazil', 'Chile', 'Argentina']
197
274
  ```
198
275
 
199
- > Passing a non-integer or a negative number will throw an `InvalidArgumentError`.
200
276
 
201
277
  ---
202
278
 
@@ -263,14 +263,13 @@ var _Query = class _Query {
263
263
  */
264
264
  select(...columns) {
265
265
  const source = this.#rows;
266
- const length = source.length;
267
- const rows = new Array(length);
268
- for (let i = 0; i < length; i++) {
266
+ const rows = [];
267
+ for (let i = 0; i < source.length; i++) {
269
268
  const result = {};
270
269
  for (const column of columns) {
271
270
  result[column] = source[i][column];
272
271
  }
273
- rows[i] = result;
272
+ rows.push(result);
274
273
  }
275
274
  const query = new _Query(rows);
276
275
  this.cloneStateInto(query);
@@ -284,10 +283,9 @@ var _Query = class _Query {
284
283
  */
285
284
  map(callback) {
286
285
  const source = this.#rows;
287
- const length = source.length;
288
- const rows = new Array(length);
289
- for (let i = 0; i < length; i++) {
290
- rows[i] = callback(source[i]);
286
+ const rows = [];
287
+ for (let i = 0; i < source.length; i++) {
288
+ rows.push(callback(source[i]));
291
289
  }
292
290
  const query = new _Query(rows);
293
291
  this.cloneStateInto(query);
@@ -377,6 +375,52 @@ var _Query = class _Query {
377
375
  this.#limit = limit;
378
376
  return this;
379
377
  }
378
+ limitPerGroup(arg, limit) {
379
+ const counts = /* @__PURE__ */ new Map();
380
+ const result = [];
381
+ const rows = this.#rows;
382
+ const isFn = isFunction(arg);
383
+ for (let i = 0; i < rows.length; i++) {
384
+ const row = rows[i];
385
+ const group = isFn ? arg(row) : row[arg];
386
+ const count = counts.get(group) ?? 0;
387
+ if (count < limit) {
388
+ result.push(row);
389
+ counts.set(group, count + 1);
390
+ }
391
+ }
392
+ this.#rows = result;
393
+ return this;
394
+ }
395
+ top(limit, options) {
396
+ const rows = this.getLimitedRows();
397
+ if (rows.length === 0) {
398
+ return this;
399
+ }
400
+ const { partitionBy, orderBy } = options ?? {};
401
+ if (orderBy) {
402
+ const columns = Array.isArray(orderBy) ? orderBy : [orderBy];
403
+ this.#rows = rows.sort(sortByProperties(...columns));
404
+ }
405
+ if (!partitionBy) {
406
+ this.#rows = rows.slice(0, limit);
407
+ return this;
408
+ }
409
+ const isFn = isFunction(partitionBy);
410
+ const counts = /* @__PURE__ */ new Map();
411
+ const result = [];
412
+ for (let i = 0; i < rows.length; i++) {
413
+ const row = rows[i];
414
+ const group = isFn ? partitionBy(row) : row[partitionBy];
415
+ const count = counts.get(group) ?? 0;
416
+ if (count < limit) {
417
+ result.push(row);
418
+ counts.set(group, count + 1);
419
+ }
420
+ }
421
+ this.#rows = result;
422
+ return this;
423
+ }
380
424
  /**
381
425
  * Returns all results.
382
426
  *
@@ -448,9 +492,9 @@ var _Query = class _Query {
448
492
  }
449
493
  column = firstColumn;
450
494
  }
451
- const values = new Array(length);
495
+ const values = [];
452
496
  for (let i = 0; i < length; i++) {
453
- values[i] = source[i][column];
497
+ values.push(source[i][column]);
454
498
  }
455
499
  return values;
456
500
  }
@@ -462,10 +506,9 @@ var _Query = class _Query {
462
506
  */
463
507
  values() {
464
508
  const source = this.getLimitedRows();
465
- const length = source.length;
466
- const rows = new Array(length);
467
- for (let i = 0; i < length; i++) {
468
- rows[i] = Object.values(source[i]);
509
+ const rows = [];
510
+ for (let i = 0; i < source.length; i++) {
511
+ rows.push(Object.values(source[i]));
469
512
  }
470
513
  return rows;
471
514
  }
@@ -491,14 +534,14 @@ var _Query = class _Query {
491
534
  if (length === 0) {
492
535
  return null;
493
536
  }
494
- const values = new Array(length);
537
+ const values = [];
495
538
  if (isFunction(arg)) {
496
539
  for (let i = 0; i < length; i++) {
497
- values[i] = arg(source[i]);
540
+ values.push(arg(source[i]));
498
541
  }
499
542
  } else {
500
543
  for (let i = 0; i < length; i++) {
501
- values[i] = source[i][arg];
544
+ values.push(source[i][arg]);
502
545
  }
503
546
  }
504
547
  return Math.min(...values);
@@ -509,14 +552,14 @@ var _Query = class _Query {
509
552
  if (length === 0) {
510
553
  return null;
511
554
  }
512
- const values = new Array(length);
555
+ const values = [];
513
556
  if (isFunction(arg)) {
514
557
  for (let i = 0; i < length; i++) {
515
- values[i] = arg(source[i]);
558
+ values.push(arg(source[i]));
516
559
  }
517
560
  } else {
518
561
  for (let i = 0; i < length; i++) {
519
- values[i] = source[i][arg];
562
+ values.push(source[i][arg]);
520
563
  }
521
564
  }
522
565
  return Math.max(...values);
@@ -527,14 +570,14 @@ var _Query = class _Query {
527
570
  if (length === 0) {
528
571
  return 0;
529
572
  }
530
- const values = new Array(length);
573
+ const values = [];
531
574
  if (isFunction(arg)) {
532
575
  for (let i = 0; i < length; i++) {
533
- values[i] = arg(source[i]);
576
+ values.push(arg(source[i]));
534
577
  }
535
578
  } else {
536
579
  for (let i = 0; i < length; i++) {
537
- values[i] = source[i][arg];
580
+ values.push(source[i][arg]);
538
581
  }
539
582
  }
540
583
  return values.reduce((total, value) => total + value, 0);
@@ -619,6 +662,16 @@ __decorateClass([
619
662
  __decorateParam(0, integer),
620
663
  __decorateParam(0, min(0))
621
664
  ], _Query.prototype, "limit");
665
+ __decorateClass([
666
+ validateNumbers,
667
+ __decorateParam(1, integer),
668
+ __decorateParam(1, min(0))
669
+ ], _Query.prototype, "limitPerGroup");
670
+ __decorateClass([
671
+ validateNumbers,
672
+ __decorateParam(0, integer),
673
+ __decorateParam(0, min(0))
674
+ ], _Query.prototype, "top");
622
675
  var Query = _Query;
623
676
 
624
677
  exports.Query = Query;
@@ -197,6 +197,56 @@ declare class Query<T extends object> {
197
197
  * @throws {InvalidArgumentError} If the given limit is less than 0.
198
198
  */
199
199
  limit(limit: number): this;
200
+ /**
201
+ * Limits the number of rows per group.
202
+ *
203
+ * @param key Key to group by.
204
+ * @param limit Maximum number of rows per group.
205
+ *
206
+ * @returns Current query.
207
+ */
208
+ limitPerGroup<K extends keyof T>(key: K, limit: number): this;
209
+ /**
210
+ * Limits the number of rows per group using a callback.
211
+ *
212
+ * @param fn Function to define the group key.
213
+ * @param limit Maximum number of rows per group.
214
+ *
215
+ * @returns Current query.
216
+ */
217
+ limitPerGroup<TValue>(fn: (row: T) => TValue, limit: number): this;
218
+ /**
219
+ * Keep the top N rows.
220
+ *
221
+ * @param limit Maximum number of rows per group.
222
+ *
223
+ * @returns Current query.
224
+ */
225
+ top(limit: number): this;
226
+ /**
227
+ * Keep the top N rows, optionally partitioned by a key or callback.
228
+ *
229
+ * @param limit Maximum number of rows per group (or globally if no partition is provided).
230
+ * @param options Options to define partitioning and ordering.
231
+ *
232
+ * @returns Current query.
233
+ */
234
+ top<K extends keyof T>(limit: number, options: {
235
+ partitionBy: K;
236
+ orderBy?: OrderingColumn<T> | OrderingColumn<T>[];
237
+ }): this;
238
+ /**
239
+ * Keep the top N rows, optionally partitioned by a key or callback.
240
+ *
241
+ * @param limit Maximum number of rows per group (or globally if no partition is provided).
242
+ * @param options Options to define partitioning and ordering.
243
+ *
244
+ * @returns Current query.
245
+ */
246
+ top<TValue>(limit: number, options: {
247
+ partitionBy: (row: T) => TValue;
248
+ orderBy?: OrderingColumn<T> | OrderingColumn<T>[];
249
+ }): this;
200
250
  /**
201
251
  * Returns all results.
202
252
  *
package/dist/esm/index.js CHANGED
@@ -261,14 +261,13 @@ var _Query = class _Query {
261
261
  */
262
262
  select(...columns) {
263
263
  const source = this.#rows;
264
- const length = source.length;
265
- const rows = new Array(length);
266
- for (let i = 0; i < length; i++) {
264
+ const rows = [];
265
+ for (let i = 0; i < source.length; i++) {
267
266
  const result = {};
268
267
  for (const column of columns) {
269
268
  result[column] = source[i][column];
270
269
  }
271
- rows[i] = result;
270
+ rows.push(result);
272
271
  }
273
272
  const query = new _Query(rows);
274
273
  this.cloneStateInto(query);
@@ -282,10 +281,9 @@ var _Query = class _Query {
282
281
  */
283
282
  map(callback) {
284
283
  const source = this.#rows;
285
- const length = source.length;
286
- const rows = new Array(length);
287
- for (let i = 0; i < length; i++) {
288
- rows[i] = callback(source[i]);
284
+ const rows = [];
285
+ for (let i = 0; i < source.length; i++) {
286
+ rows.push(callback(source[i]));
289
287
  }
290
288
  const query = new _Query(rows);
291
289
  this.cloneStateInto(query);
@@ -375,6 +373,52 @@ var _Query = class _Query {
375
373
  this.#limit = limit;
376
374
  return this;
377
375
  }
376
+ limitPerGroup(arg, limit) {
377
+ const counts = /* @__PURE__ */ new Map();
378
+ const result = [];
379
+ const rows = this.#rows;
380
+ const isFn = isFunction(arg);
381
+ for (let i = 0; i < rows.length; i++) {
382
+ const row = rows[i];
383
+ const group = isFn ? arg(row) : row[arg];
384
+ const count = counts.get(group) ?? 0;
385
+ if (count < limit) {
386
+ result.push(row);
387
+ counts.set(group, count + 1);
388
+ }
389
+ }
390
+ this.#rows = result;
391
+ return this;
392
+ }
393
+ top(limit, options) {
394
+ const rows = this.getLimitedRows();
395
+ if (rows.length === 0) {
396
+ return this;
397
+ }
398
+ const { partitionBy, orderBy } = options ?? {};
399
+ if (orderBy) {
400
+ const columns = Array.isArray(orderBy) ? orderBy : [orderBy];
401
+ this.#rows = rows.sort(sortByProperties(...columns));
402
+ }
403
+ if (!partitionBy) {
404
+ this.#rows = rows.slice(0, limit);
405
+ return this;
406
+ }
407
+ const isFn = isFunction(partitionBy);
408
+ const counts = /* @__PURE__ */ new Map();
409
+ const result = [];
410
+ for (let i = 0; i < rows.length; i++) {
411
+ const row = rows[i];
412
+ const group = isFn ? partitionBy(row) : row[partitionBy];
413
+ const count = counts.get(group) ?? 0;
414
+ if (count < limit) {
415
+ result.push(row);
416
+ counts.set(group, count + 1);
417
+ }
418
+ }
419
+ this.#rows = result;
420
+ return this;
421
+ }
378
422
  /**
379
423
  * Returns all results.
380
424
  *
@@ -446,9 +490,9 @@ var _Query = class _Query {
446
490
  }
447
491
  column = firstColumn;
448
492
  }
449
- const values = new Array(length);
493
+ const values = [];
450
494
  for (let i = 0; i < length; i++) {
451
- values[i] = source[i][column];
495
+ values.push(source[i][column]);
452
496
  }
453
497
  return values;
454
498
  }
@@ -460,10 +504,9 @@ var _Query = class _Query {
460
504
  */
461
505
  values() {
462
506
  const source = this.getLimitedRows();
463
- const length = source.length;
464
- const rows = new Array(length);
465
- for (let i = 0; i < length; i++) {
466
- rows[i] = Object.values(source[i]);
507
+ const rows = [];
508
+ for (let i = 0; i < source.length; i++) {
509
+ rows.push(Object.values(source[i]));
467
510
  }
468
511
  return rows;
469
512
  }
@@ -489,14 +532,14 @@ var _Query = class _Query {
489
532
  if (length === 0) {
490
533
  return null;
491
534
  }
492
- const values = new Array(length);
535
+ const values = [];
493
536
  if (isFunction(arg)) {
494
537
  for (let i = 0; i < length; i++) {
495
- values[i] = arg(source[i]);
538
+ values.push(arg(source[i]));
496
539
  }
497
540
  } else {
498
541
  for (let i = 0; i < length; i++) {
499
- values[i] = source[i][arg];
542
+ values.push(source[i][arg]);
500
543
  }
501
544
  }
502
545
  return Math.min(...values);
@@ -507,14 +550,14 @@ var _Query = class _Query {
507
550
  if (length === 0) {
508
551
  return null;
509
552
  }
510
- const values = new Array(length);
553
+ const values = [];
511
554
  if (isFunction(arg)) {
512
555
  for (let i = 0; i < length; i++) {
513
- values[i] = arg(source[i]);
556
+ values.push(arg(source[i]));
514
557
  }
515
558
  } else {
516
559
  for (let i = 0; i < length; i++) {
517
- values[i] = source[i][arg];
560
+ values.push(source[i][arg]);
518
561
  }
519
562
  }
520
563
  return Math.max(...values);
@@ -525,14 +568,14 @@ var _Query = class _Query {
525
568
  if (length === 0) {
526
569
  return 0;
527
570
  }
528
- const values = new Array(length);
571
+ const values = [];
529
572
  if (isFunction(arg)) {
530
573
  for (let i = 0; i < length; i++) {
531
- values[i] = arg(source[i]);
574
+ values.push(arg(source[i]));
532
575
  }
533
576
  } else {
534
577
  for (let i = 0; i < length; i++) {
535
- values[i] = source[i][arg];
578
+ values.push(source[i][arg]);
536
579
  }
537
580
  }
538
581
  return values.reduce((total, value) => total + value, 0);
@@ -617,6 +660,16 @@ __decorateClass([
617
660
  __decorateParam(0, integer),
618
661
  __decorateParam(0, min(0))
619
662
  ], _Query.prototype, "limit");
663
+ __decorateClass([
664
+ validateNumbers,
665
+ __decorateParam(1, integer),
666
+ __decorateParam(1, min(0))
667
+ ], _Query.prototype, "limitPerGroup");
668
+ __decorateClass([
669
+ validateNumbers,
670
+ __decorateParam(0, integer),
671
+ __decorateParam(0, min(0))
672
+ ], _Query.prototype, "top");
620
673
  var Query = _Query;
621
674
 
622
675
  export { Query };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "querier-ts",
3
3
  "type": "module",
4
- "version": "2.5.3",
4
+ "version": "2.6.0",
5
5
  "description": "A lightweight, type-safe in-memory query engine for JavaScript and TypeScript",
6
6
  "repository": {
7
7
  "type": "git",
@@ -60,7 +60,7 @@
60
60
  },
61
61
  "devDependencies": {
62
62
  "@eslint/js": "^10.0.1",
63
- "@vitest/coverage-v8": "^4.1.2",
63
+ "@vitest/coverage-v8": "^4.1.4",
64
64
  "eslint": "^10.2.0",
65
65
  "eslint-config-prettier": "^10.1.8",
66
66
  "eslint-plugin-prettier": "^5.5.5",
@@ -72,8 +72,8 @@
72
72
  "prettier-plugin-organize-imports": "^4.3.0",
73
73
  "tsup": "^8.5.1",
74
74
  "typescript": "~5.9.3",
75
- "typescript-eslint": "^8.58.0",
76
- "vitest": "^4.1.2"
75
+ "typescript-eslint": "^8.58.1",
76
+ "vitest": "^4.1.4"
77
77
  },
78
78
  "packageManager": "pnpm@10.33.0"
79
79
  }