@warlock.js/cascade 5.12.0 → 5.13.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 +31 -0
- package/cjs/index.cjs +685 -128
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/database-driver.contract.d.mts +44 -7
- package/esm/contracts/database-driver.contract.d.mts.map +1 -1
- package/esm/contracts/index.d.mts +2 -2
- package/esm/contracts/query-builder.contract.d.mts +61 -1
- package/esm/contracts/query-builder.contract.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.d.mts +8 -4
- package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.mjs +16 -6
- package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.d.mts +32 -7
- package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.mjs +56 -8
- package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-parser.d.mts +39 -12
- package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-parser.mjs +143 -55
- package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-update-translator.mjs +25 -0
- package/esm/drivers/mongodb/mongodb-update-translator.mjs.map +1 -0
- package/esm/drivers/mongodb/pipeline-stage-object.mjs +24 -0
- package/esm/drivers/mongodb/pipeline-stage-object.mjs.map +1 -0
- package/esm/drivers/mongodb/types.d.mts +7 -1
- package/esm/drivers/mongodb/types.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-driver.d.mts +52 -9
- package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-driver.mjs +170 -38
- package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-query-builder.d.mts +17 -1
- package/esm/drivers/postgres/postgres-query-builder.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-query-builder.mjs +31 -0
- package/esm/drivers/postgres/postgres-query-builder.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-update-validator.mjs +36 -0
- package/esm/drivers/postgres/postgres-update-validator.mjs.map +1 -0
- package/esm/errors/unsupported-lean-operation.error.d.mts +25 -0
- package/esm/errors/unsupported-lean-operation.error.d.mts.map +1 -0
- package/esm/errors/unsupported-lean-operation.error.mjs +31 -0
- package/esm/errors/unsupported-lean-operation.error.mjs.map +1 -0
- package/esm/errors/unsupported-query-operation.error.d.mts +30 -0
- package/esm/errors/unsupported-query-operation.error.d.mts.map +1 -0
- package/esm/errors/unsupported-query-operation.error.mjs +37 -0
- package/esm/errors/unsupported-query-operation.error.mjs.map +1 -0
- package/esm/errors/unsupported-update-operation.error.d.mts +30 -0
- package/esm/errors/unsupported-update-operation.error.d.mts.map +1 -0
- package/esm/errors/unsupported-update-operation.error.mjs +37 -0
- package/esm/errors/unsupported-update-operation.error.mjs.map +1 -0
- package/esm/index.d.mts +6 -3
- package/esm/index.mjs +4 -1
- package/esm/model/methods/query-methods.mjs +22 -6
- package/esm/model/methods/query-methods.mjs.map +1 -1
- package/esm/model/model.d.mts +28 -11
- package/esm/model/model.d.mts.map +1 -1
- package/esm/model/model.mjs +30 -13
- package/esm/model/model.mjs.map +1 -1
- package/esm/query-builder/lean-records.mjs +34 -0
- package/esm/query-builder/lean-records.mjs.map +1 -0
- package/esm/query-builder/query-builder.d.mts +13 -1
- package/esm/query-builder/query-builder.d.mts.map +1 -1
- package/esm/query-builder/query-builder.mjs +16 -0
- package/esm/query-builder/query-builder.mjs.map +1 -1
- package/llms-full.txt +72 -5
- package/llms.txt +3 -3
- package/package.json +4 -4
- package/skills/aggregate-data/SKILL.md +24 -1
- package/skills/perform-atomic-ops/SKILL.md +38 -3
- package/skills/query-data/SKILL.md +10 -1
package/package.json
CHANGED
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
"@mongez/events": "^2.2.7",
|
|
14
14
|
"@mongez/reinforcements": "^4.0.1",
|
|
15
15
|
"@mongez/supportive-is": "^2.1.4",
|
|
16
|
-
"@warlock.js/context": "5.
|
|
17
|
-
"@warlock.js/logger": "5.
|
|
18
|
-
"@warlock.js/seal": "5.
|
|
16
|
+
"@warlock.js/context": "5.13.0",
|
|
17
|
+
"@warlock.js/logger": "5.13.0",
|
|
18
|
+
"@warlock.js/seal": "5.13.0",
|
|
19
19
|
"citty": "^0.2.2",
|
|
20
20
|
"fast-glob": "^3.3.3"
|
|
21
21
|
},
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
],
|
|
52
52
|
"author": "hassanzohdy",
|
|
53
53
|
"license": "MIT",
|
|
54
|
-
"version": "5.
|
|
54
|
+
"version": "5.13.0",
|
|
55
55
|
"main": "./cjs/index.cjs",
|
|
56
56
|
"module": "./esm/index.mjs",
|
|
57
57
|
"types": "./esm/index.d.mts",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: aggregate-data
|
|
3
|
-
description: 'Compute aggregates over a query — scalar `.count()` / `.sum(field)` / `.avg` / `.min` / `.max`, plus grouped rollups via the two-arg `.groupBy(fields, { alias: $agg.* })`, portable date-bucketing via `.groupByDate(col, unit, aggregates?)`, the `$agg` helpers (including expression-aware `$agg.sum($expr.mul("price","quantity"))` / `$agg.sumRaw`), and `.having(alias, op, value)` on computed aggregates. Triggers: `.count`, `.sum`, `.avg`, `.min`, `.max`, `.groupBy`, `.groupByDate`, `.having`, `$agg`, `$agg.sum`, `$agg.sumRaw`, `$agg.count`, `$expr`, `$expr.mul`, `$expr.col`, `$expr.lit`; "monthly revenue report", "revenue per month", "X per category", "group by status", "sum price times quantity", "dashboard rollup"; typical import `import { Model, $agg, $expr } from "@warlock.js/cascade"`. Skip: row queries — `@warlock.js/cascade/query-data/SKILL.md`; cached aggregates — `@warlock.js/cache/use-cached-hof/SKILL.md`; competing tools raw SQL `GROUP BY`, `mongoose aggregate`, `prisma` `groupBy`.'
|
|
3
|
+
description: 'Compute aggregates over a query — scalar `.count()` / `.sum(field)` / `.avg` / `.min` / `.max`, plus grouped rollups via the two-arg `.groupBy(fields, { alias: $agg.* })`, portable date-bucketing via `.groupByDate(col, unit, aggregates?)`, the `$agg` helpers (including expression-aware `$agg.sum($expr.mul("price","quantity"))` / `$agg.sumRaw`), and `.having(alias, op, value)` on computed aggregates, plus MongoDB pipeline stages `.unwind(field, options?)` / `.addFields(fields)` / pipeline-form `join()` (`UnsupportedQueryOperationError` on Postgres). Triggers: `.unwind`, `.addFields`, `.joinRaw`, `.raw`, `$unwind`, `$lookup`, `.count`, `.sum`, `.avg`, `.min`, `.max`, `.groupBy`, `.groupByDate`, `.having`, `$agg`, `$agg.sum`, `$agg.sumRaw`, `$agg.count`, `$expr`, `$expr.mul`, `$expr.col`, `$expr.lit`; "monthly revenue report", "revenue per month", "X per category", "group by status", "sum price times quantity", "dashboard rollup"; typical import `import { Model, $agg, $expr } from "@warlock.js/cascade"`. Skip: row queries — `@warlock.js/cascade/query-data/SKILL.md`; cached aggregates — `@warlock.js/cache/use-cached-hof/SKILL.md`; competing tools raw SQL `GROUP BY`, `mongoose aggregate`, `prisma` `groupBy`.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Use aggregates and groupBy
|
|
@@ -130,6 +130,29 @@ await Order.query()
|
|
|
130
130
|
|
|
131
131
|
The `orderBy` reference matches the alias from the aggregates object.
|
|
132
132
|
|
|
133
|
+
## Pipeline stages — `.unwind()` / `.addFields()` / pipeline `join()` (MongoDB)
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// One row per tag, then filter the tags (stages run in call order)
|
|
137
|
+
const rows = await Post.query().unwind("tags").where("tags", "news").lean().get();
|
|
138
|
+
|
|
139
|
+
// Keep posts with no tags; record each tag's position
|
|
140
|
+
await Post.query().unwind("tags", { preserveNullAndEmptyArrays: true, includeArrayIndex: "position" }).get();
|
|
141
|
+
|
|
142
|
+
// Computed field, existing fields kept
|
|
143
|
+
await Post.query().addFields({ score: { $add: ["$likes", "$shares"] } }).orderBy("score", "desc").get();
|
|
144
|
+
|
|
145
|
+
// Pipeline $lookup
|
|
146
|
+
await Post.query().join({ table: "comments", localField: "id", foreignField: "postId",
|
|
147
|
+
alias: "approved", pipeline: [{ $match: { approved: true } }] }).lean().get();
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`unwind()` returns several rows for one stored document. Use `.lean()` when you only read them. `addFields` expressions are raw aggregation expressions: write them in code, never from request data. Computed columns in the output also work with `selectRaw({ alias: expr })` (`$project`). There is no `$facet` method yet.
|
|
151
|
+
|
|
152
|
+
Raw stages: `joinRaw({ $lookup: { from, let, pipeline, as } })` (or an array of stages) is emitted verbatim in call order. `raw((pipeline) => [...pipeline, { $sample: { size: 5 } }])` gets the pipeline built so far; return a new array or mutate it and return nothing. Anything else (a SQL string, a non-stage object) throws `UnsupportedQueryOperationError`. Build these in code, never from request data.
|
|
153
|
+
|
|
154
|
+
**Postgres:** `unwind()` and `addFields()` throw `UnsupportedQueryOperationError` (`operation`, `driver`) when you call them. They are never dropped without an error. Use `selectRaw` with `jsonb_array_elements()` / `unnest()`, or a related table, instead.
|
|
155
|
+
|
|
133
156
|
## Gotchas
|
|
134
157
|
|
|
135
158
|
- **`where` vs `having`.** Row filters go in `.where()` (before grouping, index-friendly); aggregate filters go in `.having()`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: perform-atomic-ops
|
|
3
|
-
description: 'Avoid races on concurrent writes — `Model.increase(filter, field, n)` / `Model.decrease` for atomic counters, `Model.atomic(filter, ops)` for arbitrary mutations (`$set` / `$inc` / `$push` / `$pull`), `Model.createMany` / `Model.findAndUpdate` / `Model.delete` for bulk. `atomic()` / `findAndUpdate()` / `findOneAndUpdate()` / `findAndReplace()` / `findOneAndDelete()` sanitize their `filter` argument — `$`-prefixed keys throw `UnsafeFilterError`, same check as `where()`. Triggers: `Model.increase`, `Model.decrease`, `Model.atomic`, `Model.createMany`, `createMany bulk`, `batchSize`, `Model.findAndUpdate`, `Model.findOneAndUpdate`, `Model.findAndReplace`, `Model.findOneAndDelete`, `Model.delete`, `$inc`, `$set`, `UnsafeFilterError`; "increment counter under concurrency", "bulk insert without N+1", "fast bulk insert", "insert thousands of rows", "atomic update without loading", "is atomic() safe with a request body filter"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: multi-row atomicity — `@warlock.js/cascade/manage-transactions/SKILL.md`; competing patterns `mongoose findOneAndUpdate`, `pg` `UPDATE ... SET x = x + 1`.'
|
|
3
|
+
description: 'Avoid races on concurrent writes — `Model.increase(filter, field, n)` / `Model.decrease` for atomic counters, `Model.atomic(filter, ops, options?)` for arbitrary mutations (`$set` / `$inc` / `$push` / `$pull` / `$addToSet` / `$setOnInsert`, pipeline updates, `upsert`, `returnDocument`, `arrayFilters`, `trustedFilter`), `Model.createMany` / `Model.findAndUpdate` / `Model.delete` for bulk. `atomic()` / `findAndUpdate()` / `findOneAndUpdate()` / `findAndReplace()` / `findOneAndDelete()` sanitize their `filter` argument — `$`-prefixed keys throw `UnsafeFilterError`, same check as `where()`. Triggers: `Model.increase`, `Model.decrease`, `Model.atomic`, `Model.createMany`, `createMany bulk`, `batchSize`, `Model.findAndUpdate`, `Model.findOneAndUpdate`, `Model.findAndReplace`, `Model.findOneAndDelete`, `Model.delete`, `$inc`, `$set`, `UnsafeFilterError`, `UnsupportedUpdateOperationError`, `upsert`, `returnDocument`, `$setOnInsert`; "upsert a counter", "reserve quota atomically", "increment counter under concurrency", "bulk insert without N+1", "fast bulk insert", "insert thousands of rows", "atomic update without loading", "is atomic() safe with a request body filter"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: multi-row atomicity — `@warlock.js/cascade/manage-transactions/SKILL.md`; competing patterns `mongoose findOneAndUpdate`, `pg` `UPDATE ... SET x = x + 1`.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Use atomic operations
|
|
@@ -25,9 +25,44 @@ await User.atomic({ id: userId }, {
|
|
|
25
25
|
});
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
`Model.atomic(filter, operations)` → `Promise<number>`. Driver-flavored atomic mutation — MongoDB has `$set` / `$inc` / `$push` / `$pull
|
|
28
|
+
`Model.atomic(filter, operations)` → `Promise<number>`. Driver-flavored atomic mutation — MongoDB has `$set` / `$inc` / `$push` / `$pull` natively; the Postgres driver supports `$set` / `$unset` / `$inc` / `$dec` (and `$setOnInsert` on upsert) and throws `UnsupportedUpdateOperationError` for the rest. Use when you need to combine multiple field changes atomically without loading the model first.
|
|
29
29
|
|
|
30
|
-
**`filter` is sanitized like `where()`.** `atomic()`, `findAndUpdate()`, `findOneAndUpdate()`, `findAndReplace()` and `findOneAndDelete()` all run their `filter` argument through the same `$`-prefixed-key check as `Model.where()` (see [`query-data`](@warlock.js/cascade/query-data/SKILL.md)) and throw `UnsafeFilterError` on a key like `$ne`. Before this, these five bypassed `where()` entirely, so a filter forwarded straight from a request body — `User.atomic(req.body.filter, { $set: { role: "admin" } })` — could still smuggle an operator like `{ role: { $ne: "admin" } }` through as a live query even though `where()` itself was already guarded. Only the FILTER is checked — the update-operator object (`$set`/`$inc`/`$unset`/…) is untouched, since that's meant to carry `$` keys. If you legitimately need operator conditions in the filter, express them through `Model.query().where(...)`
|
|
30
|
+
**`filter` is sanitized like `where()`.** `atomic()`, `findAndUpdate()`, `findOneAndUpdate()`, `findAndReplace()` and `findOneAndDelete()` all run their `filter` argument through the same `$`-prefixed-key check as `Model.where()` (see [`query-data`](@warlock.js/cascade/query-data/SKILL.md)) and throw `UnsafeFilterError` on a key like `$ne`. Before this, these five bypassed `where()` entirely, so a filter forwarded straight from a request body — `User.atomic(req.body.filter, { $set: { role: "admin" } })` — could still smuggle an operator like `{ role: { $ne: "admin" } }` through as a live query even though `where()` itself was already guarded. Only the FILTER is checked — the update-operator object (`$set`/`$inc`/`$unset`/…) is untouched, since that's meant to carry `$` keys. If you legitimately need operator conditions in the filter, express them through `Model.query().where(...)`, or pass `{ trustedFilter: true }` for a code-authored filter (see below).
|
|
31
|
+
|
|
32
|
+
## Upsert, returnDocument, pipelines — the options argument
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// Counter: filter + $inc + $setOnInsert + upsert → the new document, one call
|
|
36
|
+
const counter = await Counter.findOneAndUpdate(
|
|
37
|
+
{ key: "signups" },
|
|
38
|
+
{ $inc: { count: 1 }, $setOnInsert: { startedAt: new Date() } },
|
|
39
|
+
{ upsert: true }, // returnDocument defaults to "after"
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Quota reservation: the conditional filter needs trustedFilter (code-authored only!)
|
|
43
|
+
const granted = await Quota.atomic(
|
|
44
|
+
{ id: quotaId, used: { $lt: 10 } },
|
|
45
|
+
{ $inc: { used: 1 } },
|
|
46
|
+
{ trustedFilter: true },
|
|
47
|
+
); // 1 = reserved, 0 = quota full. 50 parallel calls → exactly 10 succeed
|
|
48
|
+
|
|
49
|
+
// MongoDB only: pipeline update, $addToSet, arrayFilters
|
|
50
|
+
await Post.findOneAndUpdate({ slug }, [{ $set: { score: { $add: ["$likes", "$shares"] } } }]);
|
|
51
|
+
await Post.atomic({ slug }, { $set: { "grades.$[low].score": 50 } }, { arrayFilters: [{ "low.score": { $lt: 50 } }] });
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- `atomic(filter, update, { upsert?, arrayFilters?, trustedFilter? })` → modified + upserted count.
|
|
55
|
+
- `findOneAndUpdate(filter, update, { ...same, returnDocument?: "before" | "after" })`.
|
|
56
|
+
- `findAndUpdate(filter, update, { upsert?, arrayFilters? })`. No `trustedFilter`, because it reads the rows back with `where(filter)`.
|
|
57
|
+
|
|
58
|
+
**Postgres.** An upsert runs as `INSERT … ON CONFLICT (target) DO UPDATE … RETURNING *`. The target is the primary key, or a unique index, whose columns are ALL equality keys of the filter. Other filter conditions become `DO UPDATE … WHERE`. If the row exists and fails them, nothing is written and `findOneAndUpdate` returns `null`. The driver throws `UnsupportedUpdateOperationError` (`.operation`, `.driver`) for:
|
|
59
|
+
- pipeline updates
|
|
60
|
+
- `arrayFilters`
|
|
61
|
+
- `$push`, `$pull`, `$addToSet` and unknown operators
|
|
62
|
+
- `returnDocument: "before"` together with `upsert`
|
|
63
|
+
- an upsert with no unique target it can find
|
|
64
|
+
|
|
65
|
+
It never ignores one of these without throwing.
|
|
31
66
|
|
|
32
67
|
## Bulk insert — `Model.createMany`
|
|
33
68
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: query-data
|
|
3
|
-
description: 'Query records via the model — `.where(field, value)` / `.where(field, op, value)`, `.find(id)` / `.first` / `.all`, `.orderBy`, `.count` / `.exists`, plus `.whereIn` / `.whereBetween` / `.whereLike` / `.pluck` / `.firstOrFail` / scopes via `addScope`. Covers filter safety: `where()` rejects `$`-prefixed keys (`UnsafeFilterError`), `whereRaw()` string form rejects `$where`/`$function`/`$accumulator` (`UnsafeRawExpressionError`), and `whereLike`/`whereStartsWith`/`whereEndsWith`/`whereSearch` match string arguments literally (pass a `RegExp` for pattern semantics). Triggers: `.where`, `.find`, `.first`, `.firstOrFail`, `.all`, `.get`, `.orderBy`, `.exists`, `.whereIn`, `.whereBetween`, `.whereLike`, `.whereRaw`, `addScope`, `escapeRegex`, `likePatternToRegexSource`, `UnsafeFilterError`, `UnsafeRawExpressionError`; "filter by status", "find by id", "fetch active users", "check existence", "search box query", "is where() safe from injection"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: pagination — `@warlock.js/cascade/paginate-results/SKILL.md`; aggregates — `@warlock.js/cascade/aggregate-data/SKILL.md`.'
|
|
3
|
+
description: 'Query records via the model — `.where(field, value)` / `.where(field, op, value)`, `.find(id)` / `.first` / `.all`, `.orderBy`, `.count` / `.exists`, `.lean()` plain-object reads, plus `.whereIn` / `.whereBetween` / `.whereLike` / `.pluck` / `.firstOrFail` / scopes via `addScope`. Covers filter safety: `where()` rejects `$`-prefixed keys (`UnsafeFilterError`), `whereRaw()` string form rejects `$where`/`$function`/`$accumulator` (`UnsafeRawExpressionError`), and `whereLike`/`whereStartsWith`/`whereEndsWith`/`whereSearch` match string arguments literally (pass a `RegExp` for pattern semantics). Triggers: `.where`, `.find`, `.first`, `.firstOrFail`, `.all`, `.get`, `.orderBy`, `.exists`, `.whereIn`, `.whereBetween`, `.whereLike`, `.whereRaw`, `.lean`, `UnsupportedLeanOperationError`, `addScope`, `escapeRegex`, `likePatternToRegexSource`, `UnsafeFilterError`, `UnsafeRawExpressionError`; "filter by status", "find by id", "fetch active users", "check existence", "search box query", "is where() safe from injection"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: pagination — `@warlock.js/cascade/paginate-results/SKILL.md`; aggregates — `@warlock.js/cascade/aggregate-data/SKILL.md`.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Query data
|
|
@@ -104,6 +104,15 @@ const newest = await User
|
|
|
104
104
|
|
|
105
105
|
For pagination see [`@warlock.js/cascade/paginate-results/SKILL.md`](@warlock.js/cascade/paginate-results/SKILL.md).
|
|
106
106
|
|
|
107
|
+
## Plain objects — `.lean()`
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
const rows = await User.query().where("status", "active").lean().orderBy("id").get();
|
|
111
|
+
rows[0].name; // typed as UserSchema, not a User instance
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`.lean()` (anywhere before the terminator) returns the rows as the driver sent them: no `User` instances, no driver casting (a date string stays a string, Mongo `_id` stays an `ObjectId`), no `onFetched` / model `fetched` event. Use it for read-only lists and exports. It still strips `static hidden` fields. `where` / `select` / `orderBy` / `limit` / `first` / `paginate` behave as usual. Adding `.with()` or `.joinWith()` throws `UnsupportedLeanOperationError`: load relations with a second lean query, or drop `.lean()`. About 1.9x faster than a hydrated read for 10k Mongo documents (one run on a busy machine).
|
|
115
|
+
|
|
107
116
|
## Count and existence
|
|
108
117
|
|
|
109
118
|
```ts
|