querier-ts 2.5.4 → 2.7.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.7.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 new overload to `orderBy()` method, allowing to specify a function that returns the values to be sorted and the order to be applied.
16
+
17
+ ## v2.6.0
18
+
19
+ ### Summary
20
+
21
+ - [ ] Bug fixes
22
+ - [ ] Code refactoring
23
+ - [x] New features
24
+ - [ ] Build and packaging updates
25
+ - [ ] Breaking changes
26
+
27
+ ### New features
28
+
29
+ - Added `limitPerGroup()` and `top()` methods to `Query`.
30
+
3
31
  ## v2.5.4
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,82 @@ 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
+ > Passing a non-integer or a negative number to `limit` will throw an `InvalidArgumentError`.
231
+
232
+ ---
233
+
234
+ #### `top(limit, options?)`
235
+
236
+ Keeps the top N rows, optionally partitioned by a key or callback.
237
+
238
+ - `limit`: The maximum number of rows to keep.
239
+ - `options.partitionBy`: A property name or function to define groups.
240
+ - `options.orderBy`: A column or list of columns to define ordering.
241
+
242
+ If `partitionBy` is provided, the limit is applied per group.
243
+
244
+ The rows kept depend on the ordering. Use `orderBy` (either here or before)
245
+ to control which rows are selected.
246
+
247
+ Examples:
191
248
 
192
249
  ```ts
193
- const secondId = Query.from(users)
194
- .select('id')
195
- .skip(1)
196
- .scalar();
250
+ // top N globally
251
+ const ids = Query.from(addresses)
252
+ .orderBy('-createdAt')
253
+ .top(3)
254
+ .column('id'); // [6, 5, 4]
255
+
256
+ // top N per group (key)
257
+ const countries = Query.from(addresses)
258
+ .top(2, {
259
+ partitionBy: 'country',
260
+ orderBy: '-createdAt',
261
+ })
262
+ .column('country'); // ['Argentina', 'Argentina', 'Brazil', 'Brazil', 'Chile', 'Chile']
263
+
264
+ // top N per group (callback)
265
+ const countries = Query.from(addresses)
266
+ .top(1, {
267
+ partitionBy: (row) => row.country,
268
+ orderBy: '-createdAt',
269
+ })
270
+ .column('country'); // ['Argentina', 'Brazil', 'Chile']
271
+
272
+ // without orderBy (keeps original order)
273
+ const countries = Query.from(addresses)
274
+ .top(1, { partitionBy: 'country' })
275
+ .column('country'); // ['Brazil', 'Chile', 'Argentina']
197
276
  ```
198
277
 
199
- > Passing a non-integer or a negative number will throw an `InvalidArgumentError`.
278
+ > Passing a non-integer or a negative number to `limit` will throw an `InvalidArgumentError`.
200
279
 
201
280
  ---
202
281
 
@@ -185,6 +185,21 @@ function getObjectPropertyNames(obj) {
185
185
  );
186
186
  }
187
187
 
188
+ // src/utils/functions/sort/sort-values.ts
189
+ function sortValues(a, b, order) {
190
+ if (a == null && b == null) return 0;
191
+ if (a == null) return 1 * order;
192
+ if (b == null) return -1 * order;
193
+ if (a < b) return -1 * order;
194
+ if (a > b) return 1 * order;
195
+ return 0;
196
+ }
197
+
198
+ // src/utils/functions/sort/sort-by-callback.ts
199
+ function sortByCallback(callback, sortOrder) {
200
+ return (a, b) => sortValues(callback(a), callback(b), sortOrder);
201
+ }
202
+
188
203
  // src/utils/functions/sort/sort-by-property.ts
189
204
  function sortByProperty(property) {
190
205
  let sortOrder = 1;
@@ -197,12 +212,7 @@ function sortByProperty(property) {
197
212
  return (a, b) => {
198
213
  const valueA = a[key];
199
214
  const valueB = b[key];
200
- if (valueA == null && valueB == null) return 0;
201
- if (valueA == null) return 1 * sortOrder;
202
- if (valueB == null) return -1 * sortOrder;
203
- if (valueA < valueB) return -1 * sortOrder;
204
- if (valueA > valueB) return 1 * sortOrder;
205
- return 0;
215
+ return sortValues(valueA, valueB, sortOrder);
206
216
  };
207
217
  }
208
218
 
@@ -339,32 +349,19 @@ var _Query = class _Query {
339
349
  this.filterRows(condition, { ignoreNullValues: true });
340
350
  return this;
341
351
  }
342
- /**
343
- * Adds ordering to the results.
344
- *
345
- * This method should be called after `select()`; otherwise, the ordering will
346
- * be applied only to the selected columns.
347
- *
348
- * @example
349
- * ```ts
350
- * // ❌ "age" will not be ordered, because it is not part of the selected columns
351
- * const query = Query.from(users)
352
- * .orderBy('-age')
353
- * .select('name');
354
- *
355
- * // ✅ "age" will be ordered
356
- * const query = Query.from(users)
357
- * .select('name', 'age')
358
- * .orderBy('-age');
359
- * ```
360
- *
361
- * @param columns Ascending or descending columns. To mark a field as
362
- * descending, prefix it with `-`.
363
- *
364
- * @returns Current query.
365
- */
366
- orderBy(...columns) {
367
- this.#rows = this.#rows.sort(sortByProperties(...columns));
352
+ orderBy(...arg) {
353
+ if (arg.length === 0) {
354
+ return this;
355
+ }
356
+ if (isFunction(arg[0])) {
357
+ this.#rows = this.#rows.sort(
358
+ sortByCallback(arg[0], arg[1] === "desc" ? -1 : 1)
359
+ );
360
+ } else {
361
+ this.#rows = this.#rows.sort(
362
+ sortByProperties(...arg)
363
+ );
364
+ }
368
365
  return this;
369
366
  }
370
367
  skip(numberOfRows) {
@@ -375,6 +372,52 @@ var _Query = class _Query {
375
372
  this.#limit = limit;
376
373
  return this;
377
374
  }
375
+ limitPerGroup(arg, limit) {
376
+ const counts = /* @__PURE__ */ new Map();
377
+ const result = [];
378
+ const rows = this.#rows;
379
+ const isFn = isFunction(arg);
380
+ for (let i = 0; i < rows.length; i++) {
381
+ const row = rows[i];
382
+ const group = isFn ? arg(row) : row[arg];
383
+ const count = counts.get(group) ?? 0;
384
+ if (count < limit) {
385
+ result.push(row);
386
+ counts.set(group, count + 1);
387
+ }
388
+ }
389
+ this.#rows = result;
390
+ return this;
391
+ }
392
+ top(limit, options) {
393
+ const rows = this.getLimitedRows();
394
+ if (rows.length === 0) {
395
+ return this;
396
+ }
397
+ const { partitionBy, orderBy } = options ?? {};
398
+ if (orderBy) {
399
+ const columns = Array.isArray(orderBy) ? orderBy : [orderBy];
400
+ this.#rows = rows.sort(sortByProperties(...columns));
401
+ }
402
+ if (!partitionBy) {
403
+ this.#rows = rows.slice(0, limit);
404
+ return this;
405
+ }
406
+ const isFn = isFunction(partitionBy);
407
+ const counts = /* @__PURE__ */ new Map();
408
+ const result = [];
409
+ for (let i = 0; i < rows.length; i++) {
410
+ const row = rows[i];
411
+ const group = isFn ? partitionBy(row) : row[partitionBy];
412
+ const count = counts.get(group) ?? 0;
413
+ if (count < limit) {
414
+ result.push(row);
415
+ counts.set(group, count + 1);
416
+ }
417
+ }
418
+ this.#rows = result;
419
+ return this;
420
+ }
378
421
  /**
379
422
  * Returns all results.
380
423
  *
@@ -616,6 +659,16 @@ __decorateClass([
616
659
  __decorateParam(0, integer),
617
660
  __decorateParam(0, min(0))
618
661
  ], _Query.prototype, "limit");
662
+ __decorateClass([
663
+ validateNumbers,
664
+ __decorateParam(1, integer),
665
+ __decorateParam(1, min(0))
666
+ ], _Query.prototype, "limitPerGroup");
667
+ __decorateClass([
668
+ validateNumbers,
669
+ __decorateParam(0, integer),
670
+ __decorateParam(0, min(0))
671
+ ], _Query.prototype, "top");
619
672
  var Query = _Query;
620
673
 
621
674
  exports.Query = Query;
@@ -154,28 +154,21 @@ declare class Query<T extends object> {
154
154
  /**
155
155
  * Adds ordering to the results.
156
156
  *
157
- * This method should be called after `select()`; otherwise, the ordering will
158
- * be applied only to the selected columns.
159
- *
160
- * @example
161
- * ```ts
162
- * // ❌ "age" will not be ordered, because it is not part of the selected columns
163
- * const query = Query.from(users)
164
- * .orderBy('-age')
165
- * .select('name');
166
- *
167
- * // ✅ "age" will be ordered
168
- * const query = Query.from(users)
169
- * .select('name', 'age')
170
- * .orderBy('-age');
171
- * ```
172
- *
173
157
  * @param columns Ascending or descending columns. To mark a field as
174
158
  * descending, prefix it with `-`.
175
159
  *
176
160
  * @returns Current query.
177
161
  */
178
162
  orderBy(...columns: OrderingColumn<T>[]): this;
163
+ /**
164
+ * Adds ordering to the results.
165
+ *
166
+ * @param fn Function to map each row to a value.
167
+ * @param order Sort order. Defaults to `asc`.
168
+ *
169
+ * @returns Current query.
170
+ */
171
+ orderBy<TReturn>(fn: (row: T) => TReturn, order?: 'asc' | 'desc'): this;
179
172
  /**
180
173
  * Defines the number of rows to skip.
181
174
  *
@@ -197,6 +190,56 @@ declare class Query<T extends object> {
197
190
  * @throws {InvalidArgumentError} If the given limit is less than 0.
198
191
  */
199
192
  limit(limit: number): this;
193
+ /**
194
+ * Limits the number of rows per group.
195
+ *
196
+ * @param key Key to group by.
197
+ * @param limit Maximum number of rows per group.
198
+ *
199
+ * @returns Current query.
200
+ */
201
+ limitPerGroup<K extends keyof T>(key: K, limit: number): this;
202
+ /**
203
+ * Limits the number of rows per group using a callback.
204
+ *
205
+ * @param fn Function to define the group key.
206
+ * @param limit Maximum number of rows per group.
207
+ *
208
+ * @returns Current query.
209
+ */
210
+ limitPerGroup<TValue>(fn: (row: T) => TValue, limit: number): this;
211
+ /**
212
+ * Keep the top N rows.
213
+ *
214
+ * @param limit Maximum number of rows per group.
215
+ *
216
+ * @returns Current query.
217
+ */
218
+ top(limit: number): this;
219
+ /**
220
+ * Keep the top N rows, optionally partitioned by a key or callback.
221
+ *
222
+ * @param limit Maximum number of rows per group (or globally if no partition is provided).
223
+ * @param options Options to define partitioning and ordering.
224
+ *
225
+ * @returns Current query.
226
+ */
227
+ top<K extends keyof T>(limit: number, options: {
228
+ partitionBy: K;
229
+ orderBy?: OrderingColumn<T> | OrderingColumn<T>[];
230
+ }): this;
231
+ /**
232
+ * Keep the top N rows, optionally partitioned by a key or callback.
233
+ *
234
+ * @param limit Maximum number of rows per group (or globally if no partition is provided).
235
+ * @param options Options to define partitioning and ordering.
236
+ *
237
+ * @returns Current query.
238
+ */
239
+ top<TValue>(limit: number, options: {
240
+ partitionBy: (row: T) => TValue;
241
+ orderBy?: OrderingColumn<T> | OrderingColumn<T>[];
242
+ }): this;
200
243
  /**
201
244
  * Returns all results.
202
245
  *
package/dist/esm/index.js CHANGED
@@ -183,6 +183,21 @@ function getObjectPropertyNames(obj) {
183
183
  );
184
184
  }
185
185
 
186
+ // src/utils/functions/sort/sort-values.ts
187
+ function sortValues(a, b, order) {
188
+ if (a == null && b == null) return 0;
189
+ if (a == null) return 1 * order;
190
+ if (b == null) return -1 * order;
191
+ if (a < b) return -1 * order;
192
+ if (a > b) return 1 * order;
193
+ return 0;
194
+ }
195
+
196
+ // src/utils/functions/sort/sort-by-callback.ts
197
+ function sortByCallback(callback, sortOrder) {
198
+ return (a, b) => sortValues(callback(a), callback(b), sortOrder);
199
+ }
200
+
186
201
  // src/utils/functions/sort/sort-by-property.ts
187
202
  function sortByProperty(property) {
188
203
  let sortOrder = 1;
@@ -195,12 +210,7 @@ function sortByProperty(property) {
195
210
  return (a, b) => {
196
211
  const valueA = a[key];
197
212
  const valueB = b[key];
198
- if (valueA == null && valueB == null) return 0;
199
- if (valueA == null) return 1 * sortOrder;
200
- if (valueB == null) return -1 * sortOrder;
201
- if (valueA < valueB) return -1 * sortOrder;
202
- if (valueA > valueB) return 1 * sortOrder;
203
- return 0;
213
+ return sortValues(valueA, valueB, sortOrder);
204
214
  };
205
215
  }
206
216
 
@@ -337,32 +347,19 @@ var _Query = class _Query {
337
347
  this.filterRows(condition, { ignoreNullValues: true });
338
348
  return this;
339
349
  }
340
- /**
341
- * Adds ordering to the results.
342
- *
343
- * This method should be called after `select()`; otherwise, the ordering will
344
- * be applied only to the selected columns.
345
- *
346
- * @example
347
- * ```ts
348
- * // ❌ "age" will not be ordered, because it is not part of the selected columns
349
- * const query = Query.from(users)
350
- * .orderBy('-age')
351
- * .select('name');
352
- *
353
- * // ✅ "age" will be ordered
354
- * const query = Query.from(users)
355
- * .select('name', 'age')
356
- * .orderBy('-age');
357
- * ```
358
- *
359
- * @param columns Ascending or descending columns. To mark a field as
360
- * descending, prefix it with `-`.
361
- *
362
- * @returns Current query.
363
- */
364
- orderBy(...columns) {
365
- this.#rows = this.#rows.sort(sortByProperties(...columns));
350
+ orderBy(...arg) {
351
+ if (arg.length === 0) {
352
+ return this;
353
+ }
354
+ if (isFunction(arg[0])) {
355
+ this.#rows = this.#rows.sort(
356
+ sortByCallback(arg[0], arg[1] === "desc" ? -1 : 1)
357
+ );
358
+ } else {
359
+ this.#rows = this.#rows.sort(
360
+ sortByProperties(...arg)
361
+ );
362
+ }
366
363
  return this;
367
364
  }
368
365
  skip(numberOfRows) {
@@ -373,6 +370,52 @@ var _Query = class _Query {
373
370
  this.#limit = limit;
374
371
  return this;
375
372
  }
373
+ limitPerGroup(arg, limit) {
374
+ const counts = /* @__PURE__ */ new Map();
375
+ const result = [];
376
+ const rows = this.#rows;
377
+ const isFn = isFunction(arg);
378
+ for (let i = 0; i < rows.length; i++) {
379
+ const row = rows[i];
380
+ const group = isFn ? arg(row) : row[arg];
381
+ const count = counts.get(group) ?? 0;
382
+ if (count < limit) {
383
+ result.push(row);
384
+ counts.set(group, count + 1);
385
+ }
386
+ }
387
+ this.#rows = result;
388
+ return this;
389
+ }
390
+ top(limit, options) {
391
+ const rows = this.getLimitedRows();
392
+ if (rows.length === 0) {
393
+ return this;
394
+ }
395
+ const { partitionBy, orderBy } = options ?? {};
396
+ if (orderBy) {
397
+ const columns = Array.isArray(orderBy) ? orderBy : [orderBy];
398
+ this.#rows = rows.sort(sortByProperties(...columns));
399
+ }
400
+ if (!partitionBy) {
401
+ this.#rows = rows.slice(0, limit);
402
+ return this;
403
+ }
404
+ const isFn = isFunction(partitionBy);
405
+ const counts = /* @__PURE__ */ new Map();
406
+ const result = [];
407
+ for (let i = 0; i < rows.length; i++) {
408
+ const row = rows[i];
409
+ const group = isFn ? partitionBy(row) : row[partitionBy];
410
+ const count = counts.get(group) ?? 0;
411
+ if (count < limit) {
412
+ result.push(row);
413
+ counts.set(group, count + 1);
414
+ }
415
+ }
416
+ this.#rows = result;
417
+ return this;
418
+ }
376
419
  /**
377
420
  * Returns all results.
378
421
  *
@@ -614,6 +657,16 @@ __decorateClass([
614
657
  __decorateParam(0, integer),
615
658
  __decorateParam(0, min(0))
616
659
  ], _Query.prototype, "limit");
660
+ __decorateClass([
661
+ validateNumbers,
662
+ __decorateParam(1, integer),
663
+ __decorateParam(1, min(0))
664
+ ], _Query.prototype, "limitPerGroup");
665
+ __decorateClass([
666
+ validateNumbers,
667
+ __decorateParam(0, integer),
668
+ __decorateParam(0, min(0))
669
+ ], _Query.prototype, "top");
617
670
  var Query = _Query;
618
671
 
619
672
  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.4",
4
+ "version": "2.7.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",
@@ -68,12 +68,12 @@
68
68
  "husky": "^9.1.7",
69
69
  "jiti": "^2.6.1",
70
70
  "lint-staged": "^16.4.0",
71
- "prettier": "^3.8.1",
71
+ "prettier": "^3.8.2",
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
  }