Compare commits

..
Author SHA1 Message Date
Will Schurman c22ed50e80 v0.69.0 2026-09-29 08:34:30 -07:00
renovate[bot] 9dcbc3da23 chore(deps): update dependency ip-address to v10.5.1 [security] (#709)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [ip-address](https://redirect.github.com/beaugunderson/ip-address) | [`10.4.0` → `10.5.1`](https://renovatebot.com/diffs/npm/ip-address/10.4.0/10.5.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/ip-address/10.5.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/ip-address/10.4.0/10.5.1?slim=true) |

---

### ip-address: no classifier recognizes the NAT64 local-use range 64:ff9b:1::/48, allowing SSRF and trust-boundary bypass
[CVE-2026-101910](https://nvd.nist.gov/vuln/detail/CVE-2026-101910) / [GHSA-2vr4-cq9g-pvrc](https://redirect.github.com/advisories/GHSA-2vr4-cq9g-pvrc)

<details>
<summary>More information</summary>

#### Details
##### Summary

No classifier on `Address6` recognizes the NAT64 local-use range `64:ff9b:1::/48` (RFC 8215). `isPrivate()`, `isLoopback()`, `isLinkLocal()` and their siblings all return `false` for every address in it, so an internal IPv4 destination written through a local-use NAT64 prefix (`64:ff9b:1:7f00:0:100::` for `127.0.0.1`, `64:ff9b:1:a9fe:a9:fe00::` for `169.254.169.254`) reads as an ordinary global address. `getType()` names the range `'NAT64 (local-use)'`, so the library knows what the address is and classifies it as nothing.

An application that builds a network trust-boundary decision on these checks (for example a filter intended to block Server-Side Request Forgery, or SSRF) will classify an internal target as external and allow the request. SSRF is an attack in which a user-supplied address coaxes the server into making a request to an internal destination the user could not otherwise reach, such as a loopback service or a cloud metadata endpoint.

##### Details

The fix for GHSA-22jq-vg5j-6vgg (10.2.1) classifies IPv4-mapped (`::ffff:0:0/96`) and NAT64 well-known (`64:ff9b::/96`) addresses by their embedded IPv4 address: `embeddedIPv4()` in `src/ipv6.ts` decodes the trailing 32 bits and every special-use classifier delegates to the resulting `Address4`. Those are the only two ranges `embeddedIPv4()` handles.

The local-use range differs in kind from the well-known prefix, which is why extending the decoder does not fix it. RFC 8215 reserves `64:ff9b:1::/48` so an operator can carve their own NAT64 prefix out of it, of any length RFC 6052 allows (`/48`, `/56`, `/64`, or `/96`). Where the IPv4 address sits depends on that choice: `127.0.0.1` is `64:ff9b:1:7f00:0:100::` under a `/48` prefix and `64:ff9b:1::7f00:1` under a `/96` prefix, and each of those decodes to a different address under the other prefix length. There is no single embedded IPv4 address for the library to classify by.

What is well-defined is the range as a whole. The IANA IPv6 Special-Purpose Address Registry lists `64:ff9b:1::/48` as not globally reachable, and Python's `ipaddress` module reports `is_private` as `True` and `is_global` as `False` for every address in it. `isPrivate()` covered ULA (`fc00::/7`) plus the decoded mapped and well-known cases, and nothing in the local-use range.

##### Affected versions

`>= 10.2.0, <= 10.5.0`. The `is*` classification API was extended to `Address6` in 10.2.0; releases before it do not expose the method and are not affected through this vector.

##### Impact

Every address in `64:ff9b:1::/48` parses successfully, `isValid()` is `true`, and no classifier catches it. Which internal target is reached depends on the NAT64 prefix deployed on the server's network; the well-known prefix column is the patched control from GHSA-22jq-vg5j-6vgg.

| Internal target | Well-known form | Classified | Local-use form (`/48` prefix) | Classified |
|---|---|---|---|---|
| `127.0.0.1` | `64:ff9b::7f00:1` | loopback | `64:ff9b:1:7f00:0:100::` | nothing |
| `10.0.0.1` | `64:ff9b::a00:1` | private | `64:ff9b:1:a00:0:100::` | nothing |
| `169.254.169.254` | `64:ff9b::a9fe:a9fe` | link-local | `64:ff9b:1:a9fe:a9:fe00::` | nothing |
| `192.168.1.1` | `64:ff9b::c0a8:101` | private | `64:ff9b:1:c0a8:1:100::` | nothing |

##### Reachability

Reaching an internal host through one of these addresses requires that the server's network run a NAT64 translator on a prefix inside `64:ff9b:1::/48` and that the attacker guess or know the prefix length. That is the same precondition the well-known prefix carries (a translator on `64:ff9b::/96`), with the additional constraint that the prefix is operator-chosen rather than fixed. The severity reflects that precondition.

##### Proof of concept

`npm i [email protected]`, then:

```js
const { Address6 } = require('ip-address');

// A guard of the shape the library documents.
function isBlocked(host) {
  const a = new Address6(host);
  return a.isLoopback() || a.isLinkLocal() || a.isPrivate() || a.isMulticast() || a.isUnspecified();
}

for (const h of ['64:ff9b::7f00:1', '64:ff9b:1:7f00:0:100::', '64:ff9b:1::7f00:1']) {
  console.log(isBlocked(h) ? 'BLOCK' : 'ALLOW', h, '-> getType()', new Address6(h).getType());
}
```

On affected versions:

```
BLOCK 64:ff9b::7f00:1 -> getType() NAT64 (well-known)
ALLOW 64:ff9b:1:7f00:0:100:: -> getType() NAT64 (local-use)
ALLOW 64:ff9b:1::7f00:1 -> getType() NAT64 (local-use)
```

##### Remediation

Upgrade to the patched release. In the fix, `isPrivate()` returns `true` for every address in `64:ff9b:1::/48`, alongside ULA and the decoded mapped and well-known cases. The range is reported private as a whole rather than by a decoded IPv4 address, for the reason given above; `toAddress4Nat64(prefix)` remains the way to decode an address under a known deployment prefix. `isLoopback()` and `isLinkLocal()` are unchanged for this range, since without the prefix length the library cannot know which IPv4 address is embedded.

If you cannot upgrade immediately, test the range directly:

```js
const NAT64_LOCAL_USE = new Address6('64:ff9b:1::/48');
const localUse = new Address6(host).isHostInSubnet(NAT64_LOCAL_USE);
```

##### A note on SSRF defense

These methods are address classifiers, not a complete SSRF defense. Regardless of this fix, a robust SSRF guard must resolve the hostname and validate the *resolved* IP against the socket it connects to, and account for DNS rebinding and redirects. Treat these checks as one layer, not the only one.

#### Severity
- CVSS Score: 6.9 / 10 (Medium)
- Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:H/SI:N/SA:N`

#### References
- [https://github.com/beaugunderson/ip-address/security/advisories/GHSA-2vr4-cq9g-pvrc](https://redirect.github.com/beaugunderson/ip-address/security/advisories/GHSA-2vr4-cq9g-pvrc)
- [https://nvd.nist.gov/vuln/detail/CVE-2026-101910](https://nvd.nist.gov/vuln/detail/CVE-2026-101910)
- [https://github.com/beaugunderson/ip-address/commit/ab3dc88bcf5374344168a2ba075ca7ac4ff257f8](https://redirect.github.com/beaugunderson/ip-address/commit/ab3dc88bcf5374344168a2ba075ca7ac4ff257f8)
- [https://github.com/beaugunderson/ip-address/releases/tag/v10.5.1](https://redirect.github.com/beaugunderson/ip-address/releases/tag/v10.5.1)
- [https://github.com/advisories/GHSA-2vr4-cq9g-pvrc](https://redirect.github.com/advisories/GHSA-2vr4-cq9g-pvrc)

This data is provided by the [GitHub Advisory Database](https://redirect.github.com/advisories/GHSA-2vr4-cq9g-pvrc) ([CC-BY 4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)).
</details>

---

### Release Notes

<details>
<summary>beaugunderson/ip-address (ip-address)</summary>

### [`v10.5.1`](https://redirect.github.com/beaugunderson/ip-address/compare/v10.5.0...38b02d73cfc0724f9ad61c65b6328e116eac03ec)

[Compare Source](https://redirect.github.com/beaugunderson/ip-address/compare/v10.5.0...v10.5.1)

### [`v10.5.0`](https://redirect.github.com/beaugunderson/ip-address/releases/tag/v10.5.0)

[Compare Source](https://redirect.github.com/beaugunderson/ip-address/compare/v10.4.0...v10.5.0)

##### What's Changed

- Honor the fromURL graceful-failure contract for non-IPv6 hosts by [@&#8203;beaugunderson](https://redirect.github.com/beaugunderson) in [#&#8203;218](https://redirect.github.com/beaugunderson/ip-address/pull/218)
- Correct the documentation where it disagreed with the library by [@&#8203;beaugunderson](https://redirect.github.com/beaugunderson) in [#&#8203;219](https://redirect.github.com/beaugunderson/ip-address/pull/219)

**Full Changelog**: <https://github.com/beaugunderson/ip-address/compare/v10.4.0...v10.5.0>

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMTIuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjExMi4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-09-29 08:23:06 -07:00
Will Schurman 🦦 e87e2a3a28 feat: add ID-based and field-based knex database loaders for use with locking modifiers (#706) 2026-09-28 16:29:15 -07:00
renovate[bot] 68e222f512 chore(deps): update postgres:18 docker digest to 5a5a84b (#707)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [postgres](https://hub.docker.com/_/postgres) ([source](https://redirect.github.com/docker-library/postgres)) | digest | `86c951e` → `5a5a84b` |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMTIuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjExMi4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-09-28 08:37:30 -07:00
renovate[bot] e404297b30 chore(deps): update redis docker digest to 718f745 (#708)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| redis | digest | `8a1efc5` → `718f745` |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMTIuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjExMi4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-09-28 08:37:26 -07:00
Will Schurman cbf51c0488 v0.68.0 2026-09-25 08:45:56 -07:00
Will Schurman 🦦 243a0386fb feat: add forShare and skipLocked selection modifiers to knex loader (#705) 2026-09-24 15:53:40 -07:00
Will Schurman 🦦 727f16662d feat: add forUpdate to knex loader (#704) 2026-09-23 15:29:41 -07:00
renovate[bot] 0e1b9f9452 chore(deps): update dependency @types/node to v24.13.5 (#703)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@types/node](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) ([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)) | [`24.13.4` → `24.13.5`](https://renovatebot.com/diffs/npm/@types%2fnode/24.13.4/24.13.5) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fnode/24.13.5?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fnode/24.13.4/24.13.5?slim=true) |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMDMuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjEwMy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-09-21 09:19:23 -07:00
renovate[bot] 52327bd56c chore(deps): update dependency @tsconfig/bases to v1.0.28 (#702)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@tsconfig/bases](https://redirect.github.com/tsconfig/bases) ([source](https://redirect.github.com/tsconfig/bases/tree/HEAD/bases)) | [`1.0.27` → `1.0.28`](https://renovatebot.com/diffs/npm/@tsconfig%2fbases/1.0.27/1.0.28) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@tsconfig%2fbases/1.0.28?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tsconfig%2fbases/1.0.27/1.0.28?slim=true) |

---

### Release Notes

<details>
<summary>tsconfig/bases (@&#8203;tsconfig/bases)</summary>

### [`v1.0.28`](https://redirect.github.com/tsconfig/bases/compare/270039fbe015427858d03d84a8549d113aac0b76...270039fbe015427858d03d84a8549d113aac0b76)

[Compare Source](https://redirect.github.com/tsconfig/bases/compare/270039fbe015427858d03d84a8549d113aac0b76...270039fbe015427858d03d84a8549d113aac0b76)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMDMuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjEwMy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-09-21 09:17:59 -07:00
renovate[bot] af121ed765 chore(deps): update redis docker digest to 8a1efc5 (#701)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| redis | digest | `298e5b3` → `8a1efc5` |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMDMuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjEwMy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-09-21 09:17:53 -07:00
renovate[bot] ae9aab60bb chore(deps): update postgres:18 docker digest to 86c951e (#700)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [postgres](https://hub.docker.com/_/postgres) ([source](https://redirect.github.com/docker-library/postgres)) | digest | `4ef4dbc` → `86c951e` |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMDMuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjEwMy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-09-21 09:17:41 -07:00
Will Schurman 9629e36e0d v0.67.0 2026-09-18 12:23:16 -07:00
Will Schurman 🦦 4af5ede87d feat!: add user-facing denial reason to privacy policies (#699) 2026-09-18 10:30:50 -07:00
renovate[bot] fa5af4783f chore(deps): update node.js to v24.21.0 (#698)
> ℹ️ **Note**
> 
> This PR body was truncated due to platform limits.

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [node](https://nodejs.org) ([source](https://redirect.github.com/nodejs/node)) | tools | minor | `24.18.1` → `24.21.0` |

---

### Release Notes

<details>
<summary>nodejs/node (node)</summary>

### [`v24.21.0`](https://redirect.github.com/nodejs/node/releases/tag/v24.21.0): 2026-09-08, Version 24.21.0 'Krypton' (LTS), @&#8203;aduh95

[Compare Source](https://redirect.github.com/nodejs/node/compare/v24.20.0...v24.21.0)

##### Notable Changes

- \[[`71106e1f17`](https://redirect.github.com/nodejs/node/commit/71106e1f17)] - **crypto**: update root certificates to NSS 3.126 (Node.js GitHub Bot) [#&#8203;65495](https://redirect.github.com/nodejs/node/pull/65495)
- \[[`afca0a912d`](https://redirect.github.com/nodejs/node/commit/afca0a912d)] - **(SEMVER-MINOR)** **crypto**: support loading private keys through STORE loaders (Filip Skokan) [#&#8203;63949](https://redirect.github.com/nodejs/node/pull/63949)
- \[[`6274fccbd9`](https://redirect.github.com/nodejs/node/commit/6274fccbd9)] - **deps**: update OpenSSL to 3.5.8 (Node.js GitHub Bot) [#&#8203;65542](https://redirect.github.com/nodejs/node/pull/65542)
- \[[`53cba013c7`](https://redirect.github.com/nodejs/node/commit/53cba013c7)] - **deps**: update Undici to 7.29.1 (Node.js GitHub Bot) [#&#8203;65789](https://redirect.github.com/nodejs/node/pull/65789)
- \[[`0529772798`](https://redirect.github.com/nodejs/node/commit/0529772798)] - **(SEMVER-MINOR)** **lib,src**: improve histogram implementation (James M Snell) [#&#8203;65024](https://redirect.github.com/nodejs/node/pull/65024)
- \[[`41c7062b81`](https://redirect.github.com/nodejs/node/commit/41c7062b81)] - **(SEMVER-MINOR)** **net**: improve performance of `net.BlockList` (James M Snell) [#&#8203;64974](https://redirect.github.com/nodejs/node/pull/64974)
- \[[`5197b5a3c5`](https://redirect.github.com/nodejs/node/commit/5197b5a3c5)] - **(SEMVER-MINOR)** **perf\_hooks**: add statistical hypothesis testing to histogram (James M Snell) [#&#8203;65416](https://redirect.github.com/nodejs/node/pull/65416)
- \[[`35c635b032`](https://redirect.github.com/nodejs/node/commit/35c635b032)] - **(SEMVER-MINOR)** **util**: add non-throwing `MIMEType.parse` (James M Snell) [#&#8203;64965](https://redirect.github.com/nodejs/node/pull/64965)

##### Commits

- \[[`84d706cb9b`](https://redirect.github.com/nodejs/node/commit/84d706cb9b)] - **assert**: improve documentation wording (Kamal Rawal) [#&#8203;64953](https://redirect.github.com/nodejs/node/pull/64953)
- \[[`90d127db33`](https://redirect.github.com/nodejs/node/commit/90d127db33)] - **(SEMVER-MINOR)** **benchmark**: add --analyze mode to compare.js (James M Snell) [#&#8203;65416](https://redirect.github.com/nodejs/node/pull/65416)
- \[[`ad1d7884c3`](https://redirect.github.com/nodejs/node/commit/ad1d7884c3)] - **benchmark**: add test-only and mock timers cases (Luan Muniz) [#&#8203;64097](https://redirect.github.com/nodejs/node/pull/64097)
- \[[`1d8f045914`](https://redirect.github.com/nodejs/node/commit/1d8f045914)] - **benchmark**: apply `highWaterMark` in webstreams `pipe-to` (Matteo Collina) [#&#8203;65138](https://redirect.github.com/nodejs/node/pull/65138)
- \[[`ed7ba3c993`](https://redirect.github.com/nodejs/node/commit/ed7ba3c993)] - **benchmark**: complete the sqlite is-transaction fix (Edy Silva) [#&#8203;65218](https://redirect.github.com/nodejs/node/pull/65218)
- \[[`c8837e1aa3`](https://redirect.github.com/nodejs/node/commit/c8837e1aa3)] - **benchmark**: add test runner hooks and options (Luan Muniz) [#&#8203;63754](https://redirect.github.com/nodejs/node/pull/63754)
- \[[`2ba8661e23`](https://redirect.github.com/nodejs/node/commit/2ba8661e23)] - **buffer**: prevent string write offset overflow (Matteo Collina) [#&#8203;65043](https://redirect.github.com/nodejs/node/pull/65043)
- \[[`cc9ee6c2ae`](https://redirect.github.com/nodejs/node/commit/cc9ee6c2ae)] - **buffer**: treat detached ArrayBuffers as empty (Archkon) [#&#8203;64504](https://redirect.github.com/nodejs/node/pull/64504)
- \[[`5a5d73e4c5`](https://redirect.github.com/nodejs/node/commit/5a5d73e4c5)] - **build**: pass target architecture to small-icu genccode (ulofiai) [#&#8203;65095](https://redirect.github.com/nodejs/node/pull/65095)
- \[[`273e72d1a5`](https://redirect.github.com/nodejs/node/commit/273e72d1a5)] - **build**: deprecate always enabled `--enable-static` (Chengzhong Wu) [#&#8203;65103](https://redirect.github.com/nodejs/node/pull/65103)
- \[[`89a67246e3`](https://redirect.github.com/nodejs/node/commit/89a67246e3)] - **build**: check FIPS option value in node.gyp (Filip Skokan) [#&#8203;64982](https://redirect.github.com/nodejs/node/pull/64982)
- \[[`2d21f41cd5`](https://redirect.github.com/nodejs/node/commit/2d21f41cd5)] - **build**: handle malformed OpenSSL macros (Filip Skokan) [#&#8203;64982](https://redirect.github.com/nodejs/node/pull/64982)
- \[[`f62bc0f862`](https://redirect.github.com/nodejs/node/commit/f62bc0f862)] - **build,win**: add PGO workload scripts (Stefan Stojanovic) [#&#8203;63696](https://redirect.github.com/nodejs/node/pull/63696)
- \[[`33d0c7dc12`](https://redirect.github.com/nodejs/node/commit/33d0c7dc12)] - **child\_process**: keep SIGWINCH from killing on Win (Kirill Saied) [#&#8203;64510](https://redirect.github.com/nodejs/node/pull/64510)
- \[[`71106e1f17`](https://redirect.github.com/nodejs/node/commit/71106e1f17)] - **crypto**: update root certificates to NSS 3.126 (Node.js GitHub Bot) [#&#8203;65495](https://redirect.github.com/nodejs/node/pull/65495)
- \[[`419af8b86d`](https://redirect.github.com/nodejs/node/commit/419af8b86d)] - **crypto**: fix missing error checks on ASN1\_STRING\_to\_UTF8() (Nora Dossche) [#&#8203;65200](https://redirect.github.com/nodejs/node/pull/65200)
- \[[`8029383f3f`](https://redirect.github.com/nodejs/node/commit/8029383f3f)] - **crypto**: use available BoringSSL APIs (Filip Skokan) [#&#8203;65423](https://redirect.github.com/nodejs/node/pull/65423)
- \[[`a9bd780e19`](https://redirect.github.com/nodejs/node/commit/a9bd780e19)] - **crypto**: remove obsolete BoringSSL shims (Filip Skokan) [#&#8203;65423](https://redirect.github.com/nodejs/node/pull/65423)
- \[[`7defefad3f`](https://redirect.github.com/nodejs/node/commit/7defefad3f)] - **crypto**: read WebCrypto inputs through primordials (Filip Skokan) [#&#8203;65115](https://redirect.github.com/nodejs/node/pull/65115)
- \[[`6ed1e38627`](https://redirect.github.com/nodejs/node/commit/6ed1e38627)] - **crypto**: fix disabling FIPS mode (Filip Skokan) [#&#8203;64982](https://redirect.github.com/nodejs/node/pull/64982)
- \[[`afca0a912d`](https://redirect.github.com/nodejs/node/commit/afca0a912d)] - **(SEMVER-MINOR)** **crypto**: support loading private keys through STORE loaders (Filip Skokan) [#&#8203;63949](https://redirect.github.com/nodejs/node/pull/63949)
- \[[`9b9dd6e9cf`](https://redirect.github.com/nodejs/node/commit/9b9dd6e9cf)] - **debugger**: wait for target startup (Filip Skokan) [#&#8203;65194](https://redirect.github.com/nodejs/node/pull/65194)
- \[[`07faaeeffd`](https://redirect.github.com/nodejs/node/commit/07faaeeffd)] - **deps**: update corepack to 0.36.0 (Node.js GitHub Bot) [#&#8203;65653](https://redirect.github.com/nodejs/node/pull/65653)
- \[[`53cba013c7`](https://redirect.github.com/nodejs/node/commit/53cba013c7)] - **deps**: update undici to 7.29.1 (Node.js GitHub Bot) [#&#8203;65789](https://redirect.github.com/nodejs/node/pull/65789)
- \[[`0268ca547c`](https://redirect.github.com/nodejs/node/commit/0268ca547c)] - **deps**: update archs files for openssl-3.5.8 (Node.js GitHub Bot) [#&#8203;65542](https://redirect.github.com/nodejs/node/pull/65542)
- \[[`6274fccbd9`](https://redirect.github.com/nodejs/node/commit/6274fccbd9)] - **deps**: upgrade openssl sources to openssl-3.5.8 (Node.js GitHub Bot) [#&#8203;65542](https://redirect.github.com/nodejs/node/pull/65542)
- \[[`6bdcd121fa`](https://redirect.github.com/nodejs/node/commit/6bdcd121fa)] - **deps**: update zlib to 1.3.2.1-motley-8002e91 (Node.js GitHub Bot) [#&#8203;65316](https://redirect.github.com/nodejs/node/pull/65316)
- \[[`c2aa446b6d`](https://redirect.github.com/nodejs/node/commit/c2aa446b6d)] - **deps**: update simdjson to 4.6.7 (Node.js GitHub Bot) [#&#8203;65318](https://redirect.github.com/nodejs/node/pull/65318)
- \[[`3e58e48ea8`](https://redirect.github.com/nodejs/node/commit/3e58e48ea8)] - **deps**: update googletest to [`49495ea`](https://redirect.github.com/nodejs/node/commit/49495eacfdbda3f4b6ba219923fedbb2e3f99376) (Node.js GitHub Bot) [#&#8203;65317](https://redirect.github.com/nodejs/node/pull/65317)
- \[[`670b3665c0`](https://redirect.github.com/nodejs/node/commit/670b3665c0)] - **deps**: cherry-pick [libuv/libuv@`e640dc9`](https://redirect.github.com/libuv/libuv/commit/e640dc9) (ulofiai) [#&#8203;65118](https://redirect.github.com/nodejs/node/pull/65118)
- \[[`ca1c67b021`](https://redirect.github.com/nodejs/node/commit/ca1c67b021)] - **deps**: float ICU-23262 patch for icu78 (René) [#&#8203;64678](https://redirect.github.com/nodejs/node/pull/64678)
- \[[`4ad043b0aa`](https://redirect.github.com/nodejs/node/commit/4ad043b0aa)] - **deps**: enable AVX-512 OpenSSL asm with clang (Daniel Lemire) [#&#8203;65136](https://redirect.github.com/nodejs/node/pull/65136)
- \[[`96b4af109b`](https://redirect.github.com/nodejs/node/commit/96b4af109b)] - **deps**: update googletest to [`d89aac5`](https://redirect.github.com/nodejs/node/commit/d89aac5f0dd4021198d903d39de16f896726de21) (Node.js GitHub Bot) [#&#8203;65153](https://redirect.github.com/nodejs/node/pull/65153)
- \[[`774f663c56`](https://redirect.github.com/nodejs/node/commit/774f663c56)] - **dgram**: don't swallow bind errors when callback is provided (armanmikoyan) [#&#8203;62602](https://redirect.github.com/nodejs/node/pull/62602)
- \[[`94b118d62e`](https://redirect.github.com/nodejs/node/commit/94b118d62e)] - **diagnostics\_channel**: validate before channel activation (Trivikram Kamat) [#&#8203;65313](https://redirect.github.com/nodejs/node/pull/65313)
- \[[`09788665bd`](https://redirect.github.com/nodejs/node/commit/09788665bd)] - **dns**: validate address type in lookupService (Lazizbek Ergashev) [#&#8203;64878](https://redirect.github.com/nodejs/node/pull/64878)
- \[[`15f95fc0e2`](https://redirect.github.com/nodejs/node/commit/15f95fc0e2)] - **dns**: validate port range in `setServers()` (René) [#&#8203;65021](https://redirect.github.com/nodejs/node/pull/65021)
- \[[`37b9e9a154`](https://redirect.github.com/nodejs/node/commit/37b9e9a154)] - **dns**: fix crash on setServers with port 0 (Lazizbek Ergashev) [#&#8203;65009](https://redirect.github.com/nodejs/node/pull/65009)
- \[[`cd6205fa0d`](https://redirect.github.com/nodejs/node/commit/cd6205fa0d)] - **doc**: update AHAFS reference link (Taeuk Ha) [#&#8203;65481](https://redirect.github.com/nodejs/node/pull/65481)
- \[[`0e6f9ae42e`](https://redirect.github.com/nodejs/node/commit/0e6f9ae42e)] - **doc**: fix property names in os.networkInterfaces() example (Jihwan) [#&#8203;65469](https://redirect.github.com/nodejs/node/pull/65469)
- \[[`034a827b41`](https://redirect.github.com/nodejs/node/commit/034a827b41)] - **doc**: fix broken links in cli.md (Donghoon Kang) [#&#8203;65412](https://redirect.github.com/nodejs/node/pull/65412)
- \[[`eb364621d4`](https://redirect.github.com/nodejs/node/commit/eb364621d4)] - **doc**: remove outdated WASI version fallback (이혜미) [#&#8203;65303](https://redirect.github.com/nodejs/node/pull/65303)
- \[[`b13f425bf8`](https://redirect.github.com/nodejs/node/commit/b13f425bf8)] - **doc**: fix broken GYP link in n-api.md (Donghoon Kang) [#&#8203;65413](https://redirect.github.com/nodejs/node/pull/65413)
- \[[`45c4011067`](https://redirect.github.com/nodejs/node/commit/45c4011067)] - **doc**: document that an empty OPENSSL\_CONF skips config loading (Orgad Shaneh) [#&#8203;64949](https://redirect.github.com/nodejs/node/pull/64949)
- \[[`fde6776c5f`](https://redirect.github.com/nodejs/node/commit/fde6776c5f)] - **doc**: fix broken TLS security level example (soreavis) [#&#8203;65391](https://redirect.github.com/nodejs/node/pull/65391)
- \[[`290c1fec04`](https://redirect.github.com/nodejs/node/commit/290c1fec04)] - **doc**: clarify socket destroyed behavior (Dayun) [#&#8203;65395](https://redirect.github.com/nodejs/node/pull/65395)
- \[[`a7e8269947`](https://redirect.github.com/nodejs/node/commit/a7e8269947)] - **doc**: update outdated nodejs.org guide links (Donghoon Kang) [#&#8203;65394](https://redirect.github.com/nodejs/node/pull/65394)
- \[[`7809f11249`](https://redirect.github.com/nodejs/node/commit/7809f11249)] - **doc**: clarify that ipv4 mapped to ipv6 are classified as ipv6 (Vedant Kulkarni) [#&#8203;62117](https://redirect.github.com/nodejs/node/pull/62117)
- \[[`64cd3a6e95`](https://redirect.github.com/nodejs/node/commit/64cd3a6e95)] - **doc**: clarify how fs.Dirent file types are determined (soreavis) [#&#8203;64532](https://redirect.github.com/nodejs/node/pull/64532)
- \[[`9b92fdce14`](https://redirect.github.com/nodejs/node/commit/9b92fdce14)] - **doc**: update security release prepare command (Rafael Gonzaga) [#&#8203;64699](https://redirect.github.com/nodejs/node/pull/64699)
- \[[`6f9b9df3c1`](https://redirect.github.com/nodejs/node/commit/6f9b9df3c1)] - **doc**: clarify copyFile symlink behavior (T) [#&#8203;62941](https://redirect.github.com/nodejs/node/pull/62941)
- \[[`2480acb550`](https://redirect.github.com/nodejs/node/commit/2480acb550)] - **doc**: document setRawMode write access on Windows (Erik Demaine) [#&#8203;63856](https://redirect.github.com/nodejs/node/pull/63856)
- \[[`a1e9c3a5db`](https://redirect.github.com/nodejs/node/commit/a1e9c3a5db)] - **doc**: add missing return types in fs.md (Chaseton Collins) [#&#8203;65307](https://redirect.github.com/nodejs/node/pull/65307)
- \[[`4533572040`](https://redirect.github.com/nodejs/node/commit/4533572040)] - **doc**: add missing return types in buffer.md (Yuya Inoue) [#&#8203;65308](https://redirect.github.com/nodejs/node/pull/65308)
- \[[`39ecedbbd2`](https://redirect.github.com/nodejs/node/commit/39ecedbbd2)] - **doc**: fix lint clean command (greenhead) [#&#8203;65274](https://redirect.github.com/nodejs/node/pull/65274)
- \[[`0b1fb8fcd8`](https://redirect.github.com/nodejs/node/commit/0b1fb8fcd8)] - **doc**: fix typo in onboarding.md (서울민트초코) [#&#8203;65295](https://redirect.github.com/nodejs/node/pull/65295)
- \[[`3165b5d38a`](https://redirect.github.com/nodejs/node/commit/3165b5d38a)] - **doc**: add missing `added:` tags to `fs.lchmod` (Lazizbek Ergashev) [#&#8203;65283](https://redirect.github.com/nodejs/node/pull/65283)
- \[[`113b808e59`](https://redirect.github.com/nodejs/node/commit/113b808e59)] - **doc**: fix SQLite changeset constant descriptions (greenhead) [#&#8203;65265](https://redirect.github.com/nodejs/node/pull/65265)
- \[[`c3eb51d5a1`](https://redirect.github.com/nodejs/node/commit/c3eb51d5a1)] - **doc**: document open pull request limit (Matteo Collina) [#&#8203;65250](https://redirect.github.com/nodejs/node/pull/65250)
- \[[`d7accdcd52`](https://redirect.github.com/nodejs/node/commit/d7accdcd52)] - **doc**: document http2 header constants (Harjoth Khara) [#&#8203;64548](https://redirect.github.com/nodejs/node/pull/64548)
- \[[`f47111416f`](https://redirect.github.com/nodejs/node/commit/f47111416f)] - **doc**: create ai-guidelines and include to CONTRIBUTING (Rafael Gonzaga) [#&#8203;62105](https://redirect.github.com/nodejs/node/pull/62105)
- \[[`c3b120e737`](https://redirect.github.com/nodejs/node/commit/c3b120e737)] - **doc**: update synopsis (Augustin Mauroy) [#&#8203;65171](https://redirect.github.com/nodejs/node/pull/65171)
- \[[`39f4c831fd`](https://redirect.github.com/nodejs/node/commit/39f4c831fd)] - **doc**: fix broken internal links (greenhead) [#&#8203;64901](https://redirect.github.com/nodejs/node/pull/64901)
- \[[`be25cdd69e`](https://redirect.github.com/nodejs/node/commit/be25cdd69e)] - **doc**: report proper return type on urlPattern.test (Brian Muenzenmeyer) [#&#8203;64831](https://redirect.github.com/nodejs/node/pull/64831)
- \[[`1bf7737810`](https://redirect.github.com/nodejs/node/commit/1bf7737810)] - **doc**: fix permission documentation examples (greenhead) [#&#8203;64897](https://redirect.github.com/nodejs/node/pull/64897)
- \[[`b7932e68a1`](https://redirect.github.com/nodejs/node/commit/b7932e68a1)] - **doc**: document sqlite parameter binding (Guilherme Araújo) [#&#8203;65089](https://redirect.github.com/nodejs/node/pull/65089)
- \[[`5234a5169c`](https://redirect.github.com/nodejs/node/commit/5234a5169c)] - **doc**: finalize statements in sqlite examples (Guilherme Araújo) [#&#8203;65088](https://redirect.github.com/nodejs/node/pull/65088)
- \[[`39ea929da7`](https://redirect.github.com/nodejs/node/commit/39ea929da7)] - **doc**: document quic stopSending() and resetStream() (Issac) [#&#8203;64888](https://redirect.github.com/nodejs/node/pull/64888)
- \[[`2ba198db73`](https://redirect.github.com/nodejs/node/commit/2ba198db73)] - **doc**: clarify sqlite bare parameter default (Sumit Kumar Das) [#&#8203;62009](https://redirect.github.com/nodejs/node/pull/62009)
- \[[`314f9b200f`](https://redirect.github.com/nodejs/node/commit/314f9b200f)] - **doc**: remove usage of `util.inherits` (Augustin Mauroy) [#&#8203;60817](https://redirect.github.com/nodejs/node/pull/60817)
- \[[`d82a61662c`](https://redirect.github.com/nodejs/node/commit/d82a61662c)] - **doc**: fix grammar in worker\_threads.md (이혜미) [#&#8203;64913](https://redirect.github.com/nodejs/node/pull/64913)
- \[[`44c0c8ff5b`](https://redirect.github.com/nodejs/node/commit/44c0c8ff5b)] - **doc**: clarify OpenSSL FIPS configuration (Filip Skokan) [#&#8203;64982](https://redirect.github.com/nodejs/node/pull/64982)
- \[[`9b2ca70e0d`](https://redirect.github.com/nodejs/node/commit/9b2ca70e0d)] - **doc**: remove `--expose-gc` flag from CLI documentation (Dario Piotrowicz) [#&#8203;58909](https://redirect.github.com/nodejs/node/pull/58909)
- \[[`a0a12397b9`](https://redirect.github.com/nodejs/node/commit/a0a12397b9)] - **doc**: document ArrayBuffer support in pbkd2Sync (kyungrae2002) [#&#8203;64976](https://redirect.github.com/nodejs/node/pull/64976)
- \[[`b48699e077`](https://redirect.github.com/nodejs/node/commit/b48699e077)] - **doc**: correct default highWaterMark values (Yilong Li) [#&#8203;64617](https://redirect.github.com/nodejs/node/pull/64617)
- \[[`0312ee133c`](https://redirect.github.com/nodejs/node/commit/0312ee133c)] - **esm**: avoid super-linear data URL MIME regex (Sumit Kumar Das) [#&#8203;61951](https://redirect.github.com/nodejs/node/pull/61951)
- \[[`cd84d55c81`](https://redirect.github.com/nodejs/node/commit/cd84d55c81)] - **esm**: only register text format when enabled (Efe Karasakal) [#&#8203;64992](https://redirect.github.com/nodejs/node/pull/64992)
- \[[`c0a8ef611e`](https://redirect.github.com/nodejs/node/commit/c0a8ef611e)] - **esm**: fix wasm import name in error message (이혜미) [#&#8203;64950](https://redirect.github.com/nodejs/node/pull/64950)
- \[[`e6c34f90c2`](https://redirect.github.com/nodejs/node/commit/e6c34f90c2)] - **events**: inline iterationCondition hybrid dispatch closure (Szymon Łągiewka) [#&#8203;64473](https://redirect.github.com/nodejs/node/pull/64473)
- \[[`6ee4b40c91`](https://redirect.github.com/nodejs/node/commit/6ee4b40c91)] - **events**: inline createEvent hybrid dispatch closure (Szymon Łągiewka) [#&#8203;64473](https://redirect.github.com/nodejs/node/pull/64473)
- \[[`367549eed5`](https://redirect.github.com/nodejs/node/commit/367549eed5)] - **fs**: use sized reads for large files in readFileUtf8 (Shelley Vohr) [#&#8203;65328](https://redirect.github.com/nodejs/node/pull/65328)
- \[[`8a5b1ae4c2`](https://redirect.github.com/nodejs/node/commit/8a5b1ae4c2)] - **fs**: fix realpath of namespaced drive paths (Jason Zhang) [#&#8203;65378](https://redirect.github.com/nodejs/node/pull/65378)
- \[[`c9233b950d`](https://redirect.github.com/nodejs/node/commit/c9233b950d)] - **fs**: fix glob early return skipping sibling entries (Srinu desetti) [#&#8203;64895](https://redirect.github.com/nodejs/node/pull/64895)
- \[[`bc54dd8905`](https://redirect.github.com/nodejs/node/commit/bc54dd8905)] - **fs**: pass symlink type in cp when filter is provided (Jerry Zhao) [#&#8203;62654](https://redirect.github.com/nodejs/node/pull/62654)
- \[[`fcb4333aca`](https://redirect.github.com/nodejs/node/commit/fcb4333aca)] - **fs**: allocate FSReqPromise stat arrays lazily (Samuel Attard) [#&#8203;63886](https://redirect.github.com/nodejs/node/pull/63886)
- \[[`c35876154e`](https://redirect.github.com/nodejs/node/commit/c35876154e)] - **fs**: fix out-of-bounds write in mkdtemp for long prefixes (Hierax\_Umbra) [#&#8203;64770](https://redirect.github.com/nodejs/node/pull/64770)
- \[[`b269616936`](https://redirect.github.com/nodejs/node/commit/b269616936)] - **fs**: treat `std::errc::permission_denied` as `EPERM` error (Kirill Saied) [#&#8203;64698](https://redirect.github.com/nodejs/node/pull/64698)
- \[[`212fe77e76`](https://redirect.github.com/nodejs/node/commit/212fe77e76)] - **fs**: add windowsHandle option to file streams (Kirill Saied) [#&#8203;63851](https://redirect.github.com/nodejs/node/pull/63851)
- \[[`75df6cb435`](https://redirect.github.com/nodejs/node/commit/75df6cb435)] - **http**: improve performance with known-length calls to end() (Tim Perry) [#&#8203;65466](https://redirect.github.com/nodejs/node/pull/65466)
- \[[`48d9cd4a28`](https://redirect.github.com/nodejs/node/commit/48d9cd4a28)] - **http**: cache maxHeaderPairs per header section (GetThatCookie) [#&#8203;64988](https://redirect.github.com/nodejs/node/pull/64988)
- \[[`04785c8f43`](https://redirect.github.com/nodejs/node/commit/04785c8f43)] - **http**: fix keylog listener setup on existing agent sockets (Shani Singh) [#&#8203;65066](https://redirect.github.com/nodejs/node/pull/65066)
- \[[`4b90031534`](https://redirect.github.com/nodejs/node/commit/4b90031534)] - **http**: emit drain on socket takeover and avoid stale HWM reuse (Naman Trivedi) [#&#8203;64991](https://redirect.github.com/nodejs/node/pull/64991)
- \[[`83a27559cd`](https://redirect.github.com/nodejs/node/commit/83a27559cd)] - **http2**: adapt receive deferral for Node.js 24 (Matteo Collina) [#&#8203;65093](https://redirect.github.com/nodejs/node/pull/65093)
- \[[`f43bed0ecc`](https://redirect.github.com/nodejs/node/commit/f43bed0ecc)] - **http2**: avoid uaf while receiving and sending rst\_stream (esgor) [#&#8203;64166](https://redirect.github.com/nodejs/node/pull/64166)
- \[[`b42d664321`](https://redirect.github.com/nodejs/node/commit/b42d664321)] - **inspector**: avoid calling into JS from V8 interrupts (Joyee Cheung) [#&#8203;65028](https://redirect.github.com/nodejs/node/pull/65028)
- \[[`6bf852197d`](https://redirect.github.com/nodejs/node/commit/6bf852197d)] - **lib**: use bracket notation instead of startsWith/endsWith for single char (Taejin Kim) [#&#8203;61500](https://redirect.github.com/nodejs/node/pull/61500)
- \[[`151ca7e104`](https://redirect.github.com/nodejs/node/commit/151ca7e104)] - **lib**: harden webidl dictionary member reads (Filip Skokan) [#&#8203;65115](https://redirect.github.com/nodejs/node/pull/65115)
- \[[`7e8c2c9f44`](https://redirect.github.com/nodejs/node/commit/7e8c2c9f44)] - **lib**: use validateArray for array arguments (greenhead) [#&#8203;64959](https://redirect.github.com/nodejs/node/pull/64959)
- \[[`424fe2bc5a`](https://redirect.github.com/nodejs/node/commit/424fe2bc5a)] - **lib**: add and test \[EnforceRange] in webcrypto dictionaries (Filip Skokan) [#&#8203;65091](https://redirect.github.com/nodejs/node/pull/65091)
- \[[`0529772798`](https://redirect.github.com/nodejs/node/commit/0529772798)] - **(SEMVER-MINOR)** **lib,src**: improve histogram implementation (James M Snell) [#&#8203;65024](https://redirect.github.com/nodejs/node/pull/65024)
- \[[`186e1b76e8`](https://redirect.github.com/nodejs/node/commit/186e1b76e8)] - **meta**: move targos to emeritus (Michaël Zasso) [#&#8203;65393](https://redirect.github.com/nodejs/node/pull/65393)
- \[[`b41b07c72a`](https://redirect.github.com/nodejs/node/commit/b41b07c72a)] - **meta**: add unified http api initiative (James M Snell) [#&#8203;65139](https://redirect.github.com/nodejs/node/pull/65139)
- \[[`f85b6ecd67`](https://redirect.github.com/nodejs/node/commit/f85b6ecd67)] - **meta**: move one or more collaborators to emeritus (Node.js GitHub Bot) [#&#8203;65182](https://redirect.github.com/nodejs/node/pull/65182)
- \[[`8f3d01bdce`](https://redirect.github.com/nodejs/node/commit/8f3d01bdce)] - **meta**: add Aviv Keller to `.mailmap` (Aviv Keller) [#&#8203;65048](https://redirect.github.com/nodejs/node/pull/65048)
- \[[`cfad1d5b28`](https://redirect.github.com/nodejs/node/commit/cfad1d5b28)] - **meta**: update sccache to 0.17.0 (René) [#&#8203;64985](https://redirect.github.com/nodejs/node/pull/64985)
- \[[`349c53c441`](https://redirect.github.com/nodejs/node/commit/349c53c441)] - **module**: report unreadable package.json (Paul Bouchon) [#&#8203;65223](https://redirect.github.com/nodejs/node/pull/65223)
- \[[`bd21e6706e`](https://redirect.github.com/nodejs/node/commit/bd21e6706e)] - **module**: cache nearest parent package.json per directory (Shelley Vohr) [#&#8203;65326](https://redirect.github.com/nodejs/node/pull/65326)
- \[[`961bd04370`](https://redirect.github.com/nodejs/node/commit/961bd04370)] - **module**: fix --check on ambiguous ESM files (Paul Bouchon) [#&#8203;65203](https://redirect.github.com/nodejs/node/pull/65203)
- \[[`f45ef73420`](https://redirect.github.com/nodejs/node/commit/f45ef73420)] - **net**: handle undefined parent in \_unrefTimer and \_destroy (Shivay-98) [#&#8203;64644](https://redirect.github.com/nodejs/node/pull/64644)
- \[[`41c7062b81`](https://redirect.github.com/nodejs/node/commit/41c7062b81)] - **(SEMVER-MINOR)** **net**: improve performance of net.BlockList (James M Snell) [#&#8203;64974](https://redirect.github.com/nodejs/node/pull/64974)
- \[[`5197b5a3c5`](https://redirect.github.com/nodejs/node/commit/5197b5a3c5)] - **(SEMVER-MINOR)** **perf\_hooks**: add statistical hypothesis testing to histogram (James M Snell) [#&#8203;65416](https://redirect.github.com/nodejs/node/pull/65416)
- \[[`1722ddac28`](https://redirect.github.com/nodejs/node/commit/1722ddac28)] - **permission**: enforce addon permission in GetLinkedBinding (Rafael Gonzaga) [#&#8203;65432](https://redirect.github.com/nodejs/node/pull/65432)
- \[[`dff2b675db`](https://redirect.github.com/nodejs/node/commit/dff2b675db)] - **process**: validate resource stats array offsets (Archkon) [#&#8203;65098](https://redirect.github.com/nodejs/node/pull/65098)
- \[[`f277983e7b`](https://redirect.github.com/nodejs/node/commit/f277983e7b)] - **quic**: changes for nghttp3\_conn\_close\_stream2 (Marten Richter) [#&#8203;64574](https://redirect.github.com/nodejs/node/pull/64574)
- \[[`a4c770c78e`](https://redirect.github.com/nodejs/node/commit/a4c770c78e)] - **quic**: mark drain promise handled (James M Snell) [#&#8203;65319](https://redirect.github.com/nodejs/node/pull/65319)
- \[[`2460b171c5`](https://redirect.github.com/nodejs/node/commit/2460b171c5)] - **quic**: reset rejected HTTP/3 request streams with H3\_REQUEST\_REJECTED (trivenay) [#&#8203;65442](https://redirect.github.com/nodejs/node/pull/65442)
- \[[`18a7ccf302`](https://redirect.github.com/nodejs/node/commit/18a7ccf302)] - **quic**: write desired size needs update on maxstream (Marten Richter) [#&#8203;64768](https://redirect.github.com/nodejs/node/pull/64768)
- \[[`9017f4a780`](https://redirect.github.com/nodejs/node/commit/9017f4a780)] - **quic**: do not destroy incoming streams that have a consumer (trivenay) [#&#8203;65335](https://redirect.github.com/nodejs/node/pull/65335)
- \[[`ddc41c1ef4`](https://redirect.github.com/nodejs/node/commit/ddc41c1ef4)] - **quic**: fix wake up blob (Marten Richter) [#&#8203;64044](https://redirect.github.com/nodejs/node/pull/64044)
- \[[`88bee43d7c`](https://redirect.github.com/nodejs/node/commit/88bee43d7c)] - **quic**: convert incoming :status header to number (Hallison Pereira Melo) [#&#8203;63589](https://redirect.github.com/nodejs/node/pull/63589)
- \[[`cd776fe97c`](https://redirect.github.com/nodejs/node/commit/cd776fe97c)] - **quic**: fix infinite loop if STOP\_SENDING received on a buffering stream (Tim Perry) [#&#8203;64715](https://redirect.github.com/nodejs/node/pull/64715)
- \[[`4f6eda3c23`](https://redirect.github.com/nodejs/node/commit/4f6eda3c23)] - **repl**: keep entries added while history file is loading (Mhayk Whandson) [#&#8203;64513](https://redirect.github.com/nodejs/node/pull/64513)
- \[[`cd1e6ce29b`](https://redirect.github.com/nodejs/node/commit/cd1e6ce29b)] - **repl**: add benchmarks (Aviv Keller) [#&#8203;64590](https://redirect.github.com/nodejs/node/pull/64590)
- \[[`2ba740669d`](https://redirect.github.com/nodejs/node/commit/2ba740669d)] - **sea**: avoid dangling CLI option pointers (Archkon) [#&#8203;64755](https://redirect.github.com/nodejs/node/pull/64755)
- \[[`ec5e2d6856`](https://redirect.github.com/nodejs/node/commit/ec5e2d6856)] - **sea**: handle NUL bytes in asset keys (Archkon) [#&#8203;64773](https://redirect.github.com/nodejs/node/pull/64773)
- \[[`703b854293`](https://redirect.github.com/nodejs/node/commit/703b854293)] - **sea**: reject trailing content in config JSON (Archkon) [#&#8203;64774](https://redirect.github.com/nodejs/node/pull/64774)
- \[[`a61a5fdd1c`](https://redirect.github.com/nodejs/node/commit/a61a5fdd1c)] - **sqlite**: prevent reentrant session.close() (Trivikram Kamat) [#&#8203;65349](https://redirect.github.com/nodejs/node/pull/65349)
- \[[`6ae81be0a3`](https://redirect.github.com/nodejs/node/commit/6ae81be0a3)] - **sqlite**: reject statement-less SQL in prepare() (Trevor Burnham) [#&#8203;65157](https://redirect.github.com/nodejs/node/pull/65157)
- \[[`043dfe4996`](https://redirect.github.com/nodejs/node/commit/043dfe4996)] - **sqlite**: reject statement-less SQL in SQLTagStore (Trevor Burnham) [#&#8203;65157](https://redirect.github.com/nodejs/node/pull/65157)
- \[[`b8faee02e1`](https://redirect.github.com/nodejs/node/commit/b8faee02e1)] - **sqlite**: check null returns from sqlite value functions (Nora Dossche) [#&#8203;63288](https://redirect.github.com/nodejs/node/pull/63288)
- \[[`d6d2a71bee`](https://redirect.github.com/nodejs/node/commit/d6d2a71bee)] - **sqlite**: validate maxSize argument in createTagStore() (Anshika Jain) [#&#8203;63792](https://redirect.github.com/nodejs/node/pull/63792)
- \[[`61a046309f`](https://redirect.github.com/nodejs/node/commit/61a046309f)] - **sqlite**: reject non-positive backup rates (Trivikram Kamat) [#&#8203;64893](https://redirect.github.com/nodejs/node/pull/64893)
- \[[`514e3f30fb`](https://redirect.github.com/nodejs/node/commit/514e3f30fb)] - **sqlite**: clear SQLTagStore bindings (Matteo Collina) [#&#8203;65041](https://redirect.github.com/nodejs/node/pull/65041)
- \[[`c1542255b8`](https://redirect.github.com/nodejs/node/commit/c1542255b8)] - **sqlite**: bind Boolean (mike-git374) [#&#8203;62001](https://redirect.github.com/nodejs/node/pull/62001)
- \[[`cdb732beb5`](https://redirect.github.com/nodejs/node/commit/cdb732beb5)] - **sqlite**: fix undefined behaviour in `Session::Changeset()` (Nora Dossche) [#&#8203;63637](https://redirect.github.com/nodejs/node/pull/63637)
- \[[`fbe8861111`](https://redirect.github.com/nodejs/node/commit/fbe8861111)] - **sqlite**: bind ArrayBuffer (mike-git374) [#&#8203;62061](https://redirect.github.com/nodejs/node/pull/62061)
- \[[`e3c6bd6bc8`](https://redirect.github.com/nodejs/node/commit/e3c6bd6bc8)] - **src**: add missing vector include (Filip Skokan) [#&#8203;65622](https://redirect.github.com/nodejs/node/pull/65622)
- \[[`793cf69df8`](https://redirect.github.com/nodejs/node/commit/793cf69df8)] - **src**: fix heap value deduplication in embedder graph (Ilyas Shabi) [#&#8203;64801](https://redirect.github.com/nodejs/node/pull/64801)
- \[[`ea5935b04c`](https://redirect.github.com/nodejs/node/commit/ea5935b04c)] - **src**: fix out-of-bounds write when transcoding odd-length ucs2 (nashit hayat) [#&#8203;64512](https://redirect.github.com/nodejs/node/pull/64512)
- \[[`22e5023f4d`](https://redirect.github.com/nodejs/node/commit/22e5023f4d)] - **src**: escape Windows environment variables in task runner (Antoine du Hamel) [#&#8203;65217](https://redirect.github.com/nodejs/node/pull/65217)
- \[[`a0f3c62bd9`](https://redirect.github.com/nodejs/node/commit/a0f3c62bd9)] - **src**: simplify c++ diagnostics channel API (James M Snell) [#&#8203;65158](https://redirect.github.com/nodejs/node/pull/65158)
- \[[`8e831a3d2e`](https://redirect.github.com/nodejs/node/commit/8e831a3d2e)] - **src**: make minor cleanup to permission checks (James M Snell) [#&#8203;65158](https://redirect.github.com/nodejs/node/pull/65158)
- \[[`968b2ca3a3`](https://redirect.github.com/nodejs/node/commit/968b2ca3a3)] - **src**: use DictionaryTemplate for permission diag channel message (James M Snell) [#&#8203;65158](https://redirect.github.com/nodejs/node/pull/65158)
- \[[`18e16e7f8d`](https://redirect.github.com/nodejs/node/commit/18e16e7f8d)] - **src**: cache permission strings (James M Snell) [#&#8203;65158](https://redirect.github.com/nodejs/node/pull/65158)
- \[[`c8625a4b6f`](https://redirect.github.com/nodejs/node/commit/c8625a4b6f)] - **src**: add SetAbortHandler (Max H Fisher) [#&#8203;64684](https://redirect.github.com/nodejs/node/pull/64684)
- \[[`c990140d60`](https://redirect.github.com/nodejs/node/commit/c990140d60)] - **src**: match cmd.exe case-insensitively in task runner (Archkon) [#&#8203;64907](https://redirect.github.com/nodejs/node/pull/64907)
- \[[`d7463b9dbc`](https://redirect.github.com/nodejs/node/commit/d7463b9dbc)] - **src**: reuse cached env strings in remaining files (Seongeun Lee) [#&#8203;65039](https://redirect.github.com/nodejs/node/pull/65039)
- \[[`b055a43b93`](https://redirect.github.com/nodejs/node/commit/b055a43b93)] - **src**: expose Windows-only fs open flags (Kirill Saied) [#&#8203;64775](https://redirect.github.com/nodejs/node/pull/64775)
- \[[`7f21e37496`](https://redirect.github.com/nodejs/node/commit/7f21e37496)] - **src**: report why --enable-fips failed (Filip Skokan) [#&#8203;64979](https://redirect.github.com/nodejs/node/pull/64979)
- \[[`8c11beae27`](https://redirect.github.com/nodejs/node/commit/8c11beae27)] - **src**: update repeated use strings to env (James M Snell) [#&#8203;64760](https://redirect.github.com/nodejs/node/pull/64760)
- \[[`9e2c477e26`](https://redirect.github.com/nodejs/node/commit/9e2c477e26)] - **stream**: normalize fused stateless transform results (Trivikram Kamat) [#&#8203;65367](https://redirect.github.com/nodejs/node/pull/65367)
- \[[`107e96dd41`](https://redirect.github.com/nodejs/node/commit/107e96dd41)] - **stream**: encode whole chunks in TextEncoderStream (Matteo Collina) [#&#8203;65414](https://redirect.github.com/nodejs/node/pull/65414)
- \[[`164068279b`](https://redirect.github.com/nodejs/node/commit/164068279b)] - **stream**: prevent share from eagerly draining source (Trivikram Kamat) [#&#8203;65338](https://redirect.github.com/nodejs/node/pull/65338)
- \[[`93d822bdc1`](https://redirect.github.com/nodejs/node/commit/93d822bdc1)] - **stream**: drain pending writes before broadcast end (Trivikram Kamat) [#&#8203;65334](https://redirect.github.com/nodejs/node/pull/65334)
- \[[`6b8b9c362f`](https://redirect.github.com/nodejs/node/commit/6b8b9c362f)] - **stream**: reuse unexposed managed read buffers (GetThatCookie) [#&#8203;64990](https://redirect.github.com/nodejs/node/pull/64990)
- \[[`4ec4fab367`](https://redirect.github.com/nodejs/node/commit/4ec4fab367)] - **stream**: avoid duplicated endReadableNT scheduling (Matteo Collina) [#&#8203;65310](https://redirect.github.com/nodejs/node/pull/65310)
- \[[`86d1196ebf`](https://redirect.github.com/nodejs/node/commit/86d1196ebf)] - **stream**: decouple transform backpressure changes (Matteo Collina) [#&#8203;65143](https://redirect.github.com/nodejs/node/pull/65143)
- \[[`b8a7a75b19`](https://redirect.github.com/nodejs/node/commit/b8a7a75b19)] - **stream**: reject pull on signal abort during flush (Trivikram Kamat) [#&#8203;65346](https://redirect.github.com/nodejs/node/pull/65346)
- \[[`386ed6a06d`](https://redirect.github.com/nodejs/node/commit/386ed6a06d)] - **stream**: avoid leaking consumers on signal failure (Trivikram Kamat) [#&#8203;65299](https://redirect.github.com/nodejs/node/pull/65299)
- \[[`74d53bd73b`](https://redirect.github.com/nodejs/node/commit/74d53bd73b)] - **stream**: use validateObject for zlib/iter params (greenhead) [#&#8203;65015](https://redirect.github.com/nodejs/node/pull/65015)
- \[[`3a174ce16b`](https://redirect.github.com/nodejs/node/commit/3a174ce16b)] - **stream**: use validateNumber for BYOB reader options.min (greenhead) [#&#8203;65014](https://redirect.github.com/nodejs/node/pull/65014)
- \[[`859ea01cb2`](https://redirect.github.com/nodejs/node/commit/859ea01cb2)] - **stream**: consolidate non-op algorithm callbacks (Matteo Collina) [#&#8203;65138](https://redirect.github.com/nodejs/node/pull/65138)
- \[[`c577669825`](https://redirect.github.com/nodejs/node/commit/c577669825)] - **stream**: cut promise churn in webstreams hot paths (Matteo Collina) [#&#8203;65138](https://redirect.github.com/nodejs/node/pull/65138)
- \[[`d631e910db`](https://redirect.github.com/nodejs/node/commit/d631e910db)] - **stream**: preserve falsy cancellation reasons (Trivikram Kamat) [#&#8203;64705](https://redirect.github.com/nodejs/node/pull/64705)
- \[[`f9c21eabbd`](https://redirect.github.com/nodejs/node/commit/f9c21eabbd)] - **stream**: use validateBuffer for BYOB reader view (greenhead) [#&#8203;65046](https://redirect.github.com/nodejs/node/pull/65046)
- \[[`b89c8f5d1e`](https://redirect.github.com/nodejs/node/commit/b89c8f5d1e)] - **stream**: fix recursive WritableStream abort (Jeong SeokChan) [#&#8203;64825](https://redirect.github.com/nodejs/node/pull/64825)
- \[[`39e0457e86`](https://redirect.github.com/nodejs/node/commit/39e0457e86)] - **test**: fix link-local dgram scope assertion (Filip Skokan) [#&#8203;65629](https://redirect.github.com/nodejs/node/pull/65629)
- \[[`1433cf9d5b`](https://redirect.github.com/nodejs/node/commit/1433cf9d5b)] - **test**: account for varied OpenSSL CCM final behaviours (Filip Skokan) [#&#8203;65542](https://redirect.github.com/nodejs/node/pull/65542)
- \[[`7d135d24b7`](https://redirect.github.com/nodejs/node/commit/7d135d24b7)] - **test**: convert forEach to for of test-messageevent-brandcheck file (Nachiketa Pathak) [#&#8203;65279](https://redirect.github.com/nodejs/node/pull/65279)
- \[[`421ee11715`](https://redirect.github.com/nodejs/node/commit/421ee11715)] - **test**: use spawnSyncAndAssert in windowsHide test (Junsoo Ha) [#&#8203;65351](https://redirect.github.com/nodejs/node/pull/65351)
- \[[`929d5705bc`](https://redirect.github.com/nodejs/node/commit/929d5705bc)] - **test**: remove test-debugger-run-after-quit-restart as flaky on macOS (Yuya Inoue) [#&#8203;65424](https://redirect.github.com/nodejs/node/pull/65424)
- \[[`f38f154c14`](https://redirect.github.com/nodejs/node/commit/f38f154c14)] - **test**: simplify test-timers-interval-promisified.js (Donghoon Kang) [#&#8203;65322](https://redirect.github.com/nodejs/node/pull/65322)
- \[[`a98e27f2a9`](https://redirect.github.com/nodejs/node/commit/a98e27f2a9)] - **test**: add Headers coverage and benchmark (Yagiz Nizipli) [#&#8203;65365](https://redirect.github.com/nodejs/node/pull/65365)
- \[[`38fdbb62aa`](https://redirect.github.com/nodejs/node/commit/38fdbb62aa)] - **test**: deflake test-net-listen-ipv6only (sangwook) [#&#8203;64173](https://redirect.github.com/nodejs/node/pull/64173)
- \[[`06c7684b01`](https://redirect.github.com/nodejs/node/commit/06c7684b01)] - **test**: use common/child\_process spawnSync helpers (Junsoo Ha) [#&#8203;65377](https://redirect.github.com/nodejs/node/pull/65377)
- \[[`6438c70004`](https://redirect.github.com/nodejs/node/commit/6438c70004)] - **test**: fix Linux debug skip in SEA test guard (구현우) [#&#8203;63751](https://redirect.github.com/nodejs/node/pull/63751)
- \[[`fe0e4f1b65`](https://redirect.github.com/nodejs/node/commit/fe0e4f1b65)] - **test**: avoid timer race in event loop delay test (Trivikram Kamat) [#&#8203;64728](https://redirect.github.com/nodejs/node/pull/64728)
- \[[`6ff69baea8`](https://redirect.github.com/nodejs/node/commit/6ff69baea8)] - **test**: enforce exit code in `test-http-server-stale-close` (Antoine du Hamel) [#&#8203;65198](https://redirect.github.com/nodejs/node/pull/65198)
- \[[`ba87603016`](https://redirect.github.com/nodejs/node/commit/ba87603016)] - **test**: convert test-async-local-storage-bind to async loop (freida-code) [#&#8203;65270](https://redirect.github.com/nodejs/node/pull/65270)
- \[[`8d6b89f454`](https://redirect.github.com/nodejs/node/commit/8d6b89f454)] - **test**: replace `forEach()` with `for...of` in parallel tests (Phillip Markert) [#&#8203;65272](https://redirect.github.com/nodejs/node/pull/65272)
- \[[`a60572a7bb`](https://redirect.github.com/nodejs/node/commit/a60572a7bb)] - **test**: convert forEach to for in test-constant.js file (NIxxy25) [#&#8203;65271](https://redirect.github.com/nodejs/node/pull/65271)
- \[[`f82060c6ce`](https://redirect.github.com/nodejs/node/commit/f82060c6ce)] - **test**: use for-of instead of forEach (Felix P.) [#&#8203;65268](https://redirect.github.com/nodejs/node/pull/65268)
- \[[`5ded71f9cc`](https://redirect.github.com/nodejs/node/commit/5ded71f9cc)] - **test**: cover `realpathSync` resolving symlinks after a FIFO stat (Hendrik Liebau) [#&#8203;65113](https://redirect.github.com/nodejs/node/pull/65113)
- \[[`ac9835225e`](https://redirect.github.com/nodejs/node/commit/ac9835225e)] - **test**: account for \[EnforceRange] in test-webcrypto-prototype-pollution (Filip Skokan) [#&#8203;65173](https://redirect.github.com/nodejs/node/pull/65173)
- \[[`550d24277e`](https://redirect.github.com/nodejs/node/commit/550d24277e)] - **test**: update WPT for WebCryptoAPI to [`4c2fd05`](https://redirect.github.com/nodejs/node/commit/4c2fd05ed5) (Node.js GitHub Bot) [#&#8203;65150](https://redirect.github.com/nodejs/node/pull/65150)
- \[[`2aa26559f0`](https://redirect.github.com/nodejs/node/commit/2aa26559f0)] - **test**: update WPT for urlpattern to [`4832db4`](https://redirect.github.com/nodejs/node/commit/4832db4761) (Node.js GitHub Bot) [#&#8203;65151](https://redirect.github.com/nodejs/node/pull/65151)
- \[[`d45c010108`](https://redirect.github.com/nodejs/node/commit/d45c010108)] - **test**: fix hidden error in test-http-server-stale-close.js (Meghan Denny) [#&#8203;59357](https://redirect.github.com/nodejs/node/pull/59357)
- \[[`881f8d092d`](https://redirect.github.com/nodejs/node/commit/881f8d092d)] - **test**: avoid deadlock issue in pipeline http2 tests to fix flakiness (Tim Perry) [#&#8203;65079](https://redirect.github.com/nodejs/node/pull/65079)
- \[[`e4b1e3ee75`](https://redirect.github.com/nodejs/node/commit/e4b1e3ee75)] - **test**: allow half-open CONNECT tunnel sockets (Trivikram Kamat) [#&#8203;64973](https://redirect.github.com/nodejs/node/pull/64973)
- \[[`00f4240d48`](https://redirect.github.com/nodejs/node/commit/00f4240d48)] - **test**: update passphrases to comply with the next OpenSSL FIPS mode (Filip Skokan) [#&#8203;65077](https://redirect.github.com/nodejs/node/pull/65077)
- \[[`cf7680efb7`](https://redirect.github.com/nodejs/node/commit/cf7680efb7)] - **test**: use libuv clock for immediate queue test (Trivikram Kamat) [#&#8203;64889](https://redirect.github.com/nodejs/node/pull/64889)
- \[[`b0c12772ff`](https://redirect.github.com/nodejs/node/commit/b0c12772ff)] - **test**: increase timeout in probe-failure-hang-during-evaluate (Joyee Cheung) [#&#8203;64719](https://redirect.github.com/nodejs/node/pull/64719)
- \[[`7e279104b4`](https://redirect.github.com/nodejs/node/commit/7e279104b4)] - **test**: update WPT for WebCryptoAPI to [`82c3d90`](https://redirect.github.com/nodejs/node/commit/82c3d9069c) (Node.js GitHub Bot) [#&#8203;64977](https://redirect.github.com/nodejs/node/pull/64977)
- \[[`ac11f88d16`](https://redirect.github.com/nodejs/node/commit/ac11f88d16)] - **test,doc**: cover and document multi-byte offset/size in randomFill (kyungrae2002) [#&#8203;64834](https://redirect.github.com/nodejs/node/pull/64834)
- \[[`67da38cada`](https://redirect.github.com/nodejs/node/commit/67da38cada)] - **test\_runner**: match dotfiles in default coverage exclude (semimikoh) [#&#8203;63401](https://redirect.github.com/nodejs/node/pull/63401)
- \[[`b06c61a08f`](https://redirect.github.com/nodejs/node/commit/b06c61a08f)] - **test\_runner**: print coverage and diagnostic info with dot reporter (mag123c) [#&#8203;61423](https://redirect.github.com/nodejs/node/pull/61423)
- \[[`7cb9c9c126`](https://redirect.github.com/nodejs/node/commit/7cb9c9c126)] - **test\_runner**: use run options with isolation="none" (Sylvester Keil) [#&#8203;62269](https://redirect.github.com/nodejs/node/pull/62269)
- \[[`339acf4201`](https://redirect.github.com/nodejs/node/commit/339acf4201)] - **test\_runner**: mock dual-package with conditional exports (Maruthan G) [#&#8203;62943](https://redirect.github.com/nodejs/node/pull/62943)
- \[[`e7a68bca08`](https://redirect.github.com/nodejs/node/commit/e7a68bca08)] - **test\_runner**: add classname hierarchy for JUnit reporter (mag123c) [#&#8203;60220](https://redirect.github.com/nodejs/node/pull/60220)
- \[[`6a248acefe`](https://redirect.github.com/nodejs/node/commit/6a248acefe)] - **test\_runner**: fix junit report on empty diagnostic (Lazizbek Ergashev) [#&#8203;65357](https://redirect.github.com/nodejs/node/pull/65357)
- \[[`d01dda79b6`](https://redirect.github.com/nodejs/node/commit/d01dda79b6)] - **test\_runner**: do not tag-filter test file wrappers (Chemi Atlow) [#&#8203;65170](https://redirect.github.com/nodejs/node/pull/65170)
- \[[`32ead10ddd`](https://redirect.github.com/nodejs/node/commit/32ead10ddd)] - **test\_runner**: fix env option validation (Jihwan) [#&#8203;64865](https://redirect.github.com/nodejs/node/pull/64865)
- \[[`b0ef155440`](https://redirect.github.com/nodejs/node/commit/b0ef155440)] - **tls**: throw on invalid ALPNProtocols instead of aborting (Sankalp Thakur) [#&#8203;65076](https://redirect.github.com/nodejs/node/pull/65076)
- \[[`dcf65c50ce`](https://redirect.github.com/nodejs/node/commit/dcf65c50ce)] - **tls**: fix authorized state on no-cert TLS1.3 client cert resumption (Tim Perry) [#&#8203;64677](https://redirect.github.com/nodejs/node/pull/64677)
- \[[`b0f54bda78`](https://redirect.github.com/nodejs/node/commit/b0f54bda78)] - **tools**: improve commit queue failure comment (Filip Skokan) [#&#8203;65433](https://redirect.github.com/nodejs/node/pull/65433)
- \[[`bc1f2718d5`](https://redirect.github.com/nodejs/node/commit/bc1f2718d5)] - **tools**: fix max body length handler in `create-release-proposal.sh` (Antoine du Hamel) [#&#8203;65455](https://redirect.github.com/nodejs/node/pull/65455)
- \[[`bbe76516a9`](https://redirect.github.com/nodejs/node/commit/bbe76516a9)] - **tools**: bump brace-expansion in `/tools/clang-format` (dependabot\[bot]) [#&#8203;64984](https://redirect.github.com/nodejs/node/pull/64984)
- \[[`67697debdf`](https://redirect.github.com/nodejs/node/commit/67697debdf)] - **tools**: make env variables consistent in cron jobs (Antoine du Hamel) [#&#8203;65168](https://redirect.github.com/nodejs/node/pull/65168)
- \[[`ac8a68ec92`](https://redirect.github.com/nodejs/node/commit/ac8a68ec92)] - **tools**: only include fast-tracked and old enough PRs in CQ (Antoine du Hamel) [#&#8203;65197](https://redirect.github.com/nodejs/node/pull/65197)
- \[[`6d9999fa2e`](https://redirect.github.com/nodejs/node/commit/6d9999fa2e)] - **tools**: remove skip logic in `commit-queue.sh` (Antoine du Hamel) [#&#8203;65162](https://redirect.github.com/nodejs/node/pull/65162)
- \[[`60bfa1694e`](https://redirect.github.com/nodejs/node/commit/60bfa1694e)] - **tools**: bump js-yaml from 4.2.0 to 4.3.1 in /tools/lint-md (dependabot\[bot]) [#&#8203;65129](https://redirect.github.com/nodejs/node/pull/65129)
- \[[`782748527e`](https://redirect.github.com/nodejs/node/commit/782748527e)] - **tools**: bump js-yaml from 4.2.0 to 4.3.1 in /tools/eslint (dependabot\[bot]) [#&#8203;65130](https://redirect.github.com/nodejs/node/pull/65130)
- \[[`dac257340f`](https://redirect.github.com/nodejs/node/commit/dac257340f)] - **tools**: fix GITHUB\_TOKEN permissions for CQ workflow (Antoine du Hamel) [#&#8203;65192](https://redirect.github.com/nodejs/node/pull/65192)
- \[[`e624785846`](https://redirect.github.com/nodejs/node/commit/e624785846)] - **tools**: use the read-only token when filtering PRs in CQ (Antoine du Hamel) [#&#8203;65169](https://redirect.github.com/nodejs/node/pull/65169)
- \[[`7d9dcfaaa7`](https://redirect.github.com/nodejs/node/commit/7d9dcfaaa7)] - **tools**: delay removal of `commit-queue` label (Antoine du Hamel) [#&#8203;65101](https://redirect.github.com/nodejs/node/pull/65101)
- \[[`0d7c7936e7`](https://redirect.github.com/nodejs/node/commit/0d7c7936e7)] - **tools**: move ncu config to global for commit queue (Filip Skokan) [#&#8203;65132](https://redirect.github.com/nodejs/node/pull/65132)
- \[[`5e1db5a38a`](https://redirect.github.com/nodejs/node/commit/5e1db5a38a)] - **tools**: prefilter commit queue metadata (Filip Skokan) [#&#8203;64343](https://redirect.github.com/nodejs/node/pull/64343)
- \[[`f41509b91d`](https://redirect.github.com/nodejs/node/commit/f41509b91d)] - **tools**: lazy-abort failed PR merges in CQ (Antoine du Hamel) [#&#8203;65004](https://redirect.github.com/nodejs/node/pull/65004)
- \[[`ceb0e99acd`](https://redirect.github.com/nodejs/node/commit/ceb0e99acd)] - **tools**: sync mk-ca-bundle.pl with curl (Archkon) [#&#8203;64753](https://redirect.github.com/nodejs/node/pull/64753)
- \[[`600663b23f`](https://redirect.github.com/nodejs/node/commit/600663b23f)] - **tty**: add raw-vt and io raw modes (Samuel Williams) [#&#8203;64140](https://redirect.github.com/nodejs/node/pull/64140)
- \[[`a40bfc742e`](https://redirect.github.com/nodejs/node/commit/a40bfc742e)] - **typings**: add signal\_wrap internal binding types (Seongeun Lee) [#&#8203;65229](https://redirect.github.com/nodejs/node/pull/65229)
- \[[`b103a4a3b9`](https://redirect.github.com/nodejs/node/commit/b103a4a3b9)] - **typings**: add diagnostics\_channel typings (Seongeun Lee) [#&#8203;65227](https://redirect.github.com/nodejs/node/pull/65227)
- \[[`e64de34b89`](https://redirect.github.com/nodejs/node/commit/e64de34b89)] - **typings**: add watchdog internal binding types (Seongeun Lee) [#&#8203;65228](https://redirect.github.com/nodejs/node/pull/65228)
- \[[`c57c83b4d1`](https://redirect.github.com/nodejs/node/commit/c57c83b4d1)] - **typings**: add internal\_only\_v8 binding typeis (Donghoon Kang) [#&#8203;65071](https://redirect.github.com/nodejs/node/pull/65071)
- \[[`f15e8c9dcd`](https://redirect.github.com/nodejs/node/commit/f15e8c9dcd)] - **typings**: add credentials internal binding types (Donghoon Kang) [#&#8203;65036](https://redirect.github.com/nodejs/node/pull/65036)
- \[[`a500256b0b`](https://redirect.github.com/nodejs/node/commit/a500256b0b)] - **url**: skip unused href reuse comparison (Yagiz Nizipli) [#&#8203;65361](https://redirect.github.com/nodejs/node/pull/65361)
- \[[`58390f8bec`](https://redirect.github.com/nodejs/node/commit/58390f8bec)] - **url**: speed up WHATWG URL parsing (Yagiz Nizipli) [#&#8203;65361](https://redirect.github.com/nodejs/node/pull/65361)
- \[[`7d5428c812`](https://redirect.github.com/nodejs/node/commit/7d5428c812)] - **url**: speed up URLSearchParams (Yagiz Nizipli) [#&#8203;65363](https://redirect.github.com/nodejs/node/pull/65363)
- \[[`8e2461d819`](https://redirect.github.com/nodejs/node/commit/8e2461d819)] - **url**: bounds-check short Windows file URL paths (Archkon) [#&#8203;64788](https://redirect.github.com/nodejs/node/pull/64788)
- \[[`9ffc0da90c`](https://redirect.github.com/nodejs/node/commit/9ffc0da90c)] - **url**: handle unparsable serialized URLs in setters (Matteo Collina) [#&#8203;64651](https://redirect.github.com/nodejs/node/pull/64651)
- \[[`fa9d4e075d`](https://redirect.github.com/nodejs/node/commit/fa9d4e075d)] - **util**: allow single-line format when break length is infinite (Hamid Reza Ghavami) [#&#8203;64238](https://redirect.github.com/nodejs/node/pull/64238)
- \[[`b68a7c3862`](https://redirect.github.com/nodejs/node/commit/b68a7c3862)] - **util**: fix OSC 8 hyperlink stripping in stripVTControlCharacters (Dushyant Singh Hada) [#&#8203;64319](https://redirect.github.com/nodejs/node/pull/64319)
- \[[`10cbb6dc00`](https://redirect.github.com/nodejs/node/commit/10cbb6dc00)] - **util**: fix formatting of functions returned from getters (Richard Gibson) [#&#8203;64839](https://redirect.github.com/nodejs/node/pull/64839)
- \[[`64c20b449e`](https://redirect.github.com/nodejs/node/commit/64c20b449e)] - **util**: use more primordials in `comparisons.js` (Ayoub Mabrouk) [#&#8203;61198](https://redirect.github.com/nodejs/node/pull/61198)
- \[[`cea9786de8`](https://redirect.github.com/nodejs/node/commit/cea9786de8)] - **util**: preserve function names without source map names (Hiroki Osame) [#&#8203;65108](https://redirect.github.com/nodejs/node/pull/65108)
- \[[`35c635b032`](https://redirect.github.com/nodejs/node/commit/35c635b032)] - **(SEMVER-MINOR)** **util**: add non-throwing MIMEType.parse (James M Snell) [#&#8203;64965](https://redirect.github.com/nodejs/node/pull/64965)
- \[[`5858c2ba9a`](https://redirect.github.com/nodejs/node/commit/5858c2ba9a)] - **zlib**: validate pledgedSrcSize for sync zstd (Archkon) [#&#8203;64601](https://redirect.github.com/nodejs/node/pull/64601)

### [`v24.20.0`](https://redirect.github.com/nodejs/node/releases/tag/v24.20.0): 2026-08-26, Version 24.20.0 'Krypton' (LTS), @&#8203;aduh95

[Compare Source](https://redirect.github.com/nodejs/node/compare/v24.19.0...v24.20.0)

##### Notable Changes

- \[[`b12bcc9ae1`](https://redirect.github.com/nodejs/node/commit/b12bcc9ae1)] - **(SEMVER-MINOR)** **async\_hooks**: add `using` scopes to `AsyncLocalStorage` (Stephen Belanger) [#&#8203;61674](https://redirect.github.com/nodejs/node/pull/61674)
- \[[`e2eb88b36b`](https://redirect.github.com/nodejs/node/commit/e2eb88b36b)] - **(SEMVER-MINOR)** **buffer**: add `end` parameter (Robert Nagy) [#&#8203;62390](https://redirect.github.com/nodejs/node/pull/62390)
- \[[`1fefdda18e`](https://redirect.github.com/nodejs/node/commit/1fefdda18e)] - **crypto**: update root certificates to NSS 3.125 (Node.js GitHub Bot) [#&#8203;64746](https://redirect.github.com/nodejs/node/pull/64746)
- \[[`4a158cf1ab`](https://redirect.github.com/nodejs/node/commit/4a158cf1ab)] - **doc**: add MikeMcC399 as collaborator (Mike McCready) [#&#8203;64656](https://redirect.github.com/nodejs/node/pull/64656)
- \[[`d4cafce076`](https://redirect.github.com/nodejs/node/commit/d4cafce076)] - **(SEMVER-MINOR)** **lib,permission**: add `permission.drop` (Rafael Gonzaga) [#&#8203;62672](https://redirect.github.com/nodejs/node/pull/62672)
- \[[`b3cfb55267`](https://redirect.github.com/nodejs/node/commit/b3cfb55267)] - **(SEMVER-MINOR)** **loader**: implement package maps (Maël Nison) [#&#8203;62239](https://redirect.github.com/nodejs/node/pull/62239)
- \[[`cd1eb3e60b`](https://redirect.github.com/nodejs/node/commit/cd1eb3e60b)] - **(SEMVER-MINOR)** **src,permission**: add `--permission-audit` (RafaelGSS) [#&#8203;61869](https://redirect.github.com/nodejs/node/pull/61869)
- \[[`28dc85d8d2`](https://redirect.github.com/nodejs/node/commit/28dc85d8d2)] - **(SEMVER-MINOR)** **stream**: add `node:stream/iter` implementation (James M Snell) [#&#8203;62066](https://redirect.github.com/nodejs/node/pull/62066)
- \[[`d31c168740`](https://redirect.github.com/nodejs/node/commit/d31c168740)] - **(SEMVER-MINOR)** **test\_runner**: add `context.log()` and test:log event (Moshe Atlow) [#&#8203;64389](https://redirect.github.com/nodejs/node/pull/64389)
- \[[`add1edbc42`](https://redirect.github.com/nodejs/node/commit/add1edbc42)] - **(SEMVER-MINOR)** **test\_runner**: report `entryFile` in `TestStream` events (Moshe Atlow) [#&#8203;64309](https://redirect.github.com/nodejs/node/pull/64309)
- \[[`d937c8c6cd`](https://redirect.github.com/nodejs/node/commit/d937c8c6cd)] - **(SEMVER-MINOR)** **wasm**: enable JSPI (Guy Bedford) [#&#8203;59941](https://redirect.github.com/nodejs/node/pull/59941)

##### Commits

- \[[`1822c0f335`](https://redirect.github.com/nodejs/node/commit/1822c0f335)] - **assert,util**: fix TypeError on Maps with null keys (Paul Bouchon) [#&#8203;64441](https://redirect.github.com/nodejs/node/pull/64441)
- \[[`b12bcc9ae1`](https://redirect.github.com/nodejs/node/commit/b12bcc9ae1)] - **(SEMVER-MINOR)** **async\_hooks**: add us

> ❗ **Important**
> 
> ✂ PR body was truncated to here.


</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC43OS4xIiwidXBkYXRlZEluVmVyIjoiNDQuNzkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2026-09-14 08:03:32 -07:00
renovate[bot] 953b6a2d32 chore(deps): update lint (#697)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [oxfmt](https://oxc.rs/docs/guide/usage/formatter) ([source](https://redirect.github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt)) | [`0.66.0` → `0.67.0`](https://renovatebot.com/diffs/npm/oxfmt/0.66.0/0.67.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/oxfmt/0.67.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/oxfmt/0.66.0/0.67.0?slim=true) |
| [oxlint](https://oxc.rs/docs/guide/usage/linter) ([source](https://redirect.github.com/oxc-project/oxc/tree/HEAD/npm/oxlint)) | [`1.81.0` → `1.82.0`](https://renovatebot.com/diffs/npm/oxlint/1.81.0/1.82.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/oxlint/1.82.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/oxlint/1.81.0/1.82.0?slim=true) |

---

### Release Notes

<details>
<summary>oxc-project/oxc (oxfmt)</summary>

### [`v0.67.0`](https://redirect.github.com/oxc-project/oxc/blob/HEAD/npm/oxfmt/CHANGELOG.md#0670---2026-09-07)

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxfmt_v0.66.0...oxfmt_v0.67.0)

##### 🛡️ Security

- [`9a612d5`](https://redirect.github.com/oxc-project/oxc/commit/9a612d5) deps: Update npm packages ([#&#8203;26178](https://redirect.github.com/oxc-project/oxc/issues/26178)) (renovate\[bot])

</details>

<details>
<summary>oxc-project/oxc (oxlint)</summary>

### [`v1.82.0`](https://redirect.github.com/oxc-project/oxc/blob/HEAD/npm/oxlint/CHANGELOG.md#1820---2026-09-07)

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxlint_v1.81.0...oxlint_v1.82.0)

##### 🚀 Features

- [`6a0e19c`](https://redirect.github.com/oxc-project/oxc/commit/6a0e19c) linter/eslint/no-unmodified-loop-condition: Support `checkConditionalExpressions` option ([#&#8203;26249](https://redirect.github.com/oxc-project/oxc/issues/26249)) (camc314)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC43OS4xIiwidXBkYXRlZEluVmVyIjoiNDQuNzkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2026-09-14 08:03:13 -07:00
renovate[bot] 07b8abbb73 chore(deps): update dependency jdx/mise to v2026.9.7 (#696)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [jdx/mise](https://redirect.github.com/jdx/mise) | patch | `v2026.9.2` → `v2026.9.7` |

---

### Release Notes

<details>
<summary>jdx/mise (jdx/mise)</summary>

### [`v2026.9.7`](https://redirect.github.com/jdx/mise/releases/tag/v2026.9.7): : Lockfile revision 2 with npm and Python dependency graphs, dotfile conflict inspection

[Compare Source](https://redirect.github.com/jdx/mise/compare/v2026.9.6...v2026.9.7)

This release introduces `mise.lock` revision 2, which records complete transitive dependency graphs for npm tools (via embedded aube) and Python tools (via uv) in native sidecar files, and adds `mise bootstrap dotfiles conflicts` for inspecting dotfile sync conflicts before resolving them. It also lets dotfile templates consume bootstrap secrets, stops `minimum_release_age` from rejecting versions already committed to a lockfile, and closes a security gap in `history.describe_command`.

##### Added

- **lock:** Lockfile revision 2 records the full dependency graph of npm tools installed by embedded aube and of `pypi:` tools installed by uv, then replays it with a strict frozen install so two projects on the same top-level version can still receive their own reviewed transitive graph. Graphs live in native sidecar files (`uv.lock` / `aube-lock.yaml` plus a manifest) under `.mise/locks/<backend-tool>/<version>/`, referenced from `mise.lock` by relative path and SHA-256 digest, so the lockfile itself stays small. Commit the sidecar directory with `mise.lock`. New lockfiles use revision 2; existing revision 0 and 1 files keep their format until you run `mise lock --upgrade`. `mise lock --bump <tool>` refreshes a tool's transitive graph even when its top-level version is unchanged, ordinary `mise install` validates and accepts hand-edited sidecars, and `mise install --locked` rejects digest mismatches until you run `mise lock`. Python graph locking requires uv 0.12.10 or newer and published wheels for the target platform; Git sources, standalone pipx installs, and free-form `uvx_args`/`pipx_args` stay version-only. ([#&#8203;13131](https://redirect.github.com/jdx/mise/pull/13131), [#&#8203;13146](https://redirect.github.com/jdx/mise/pull/13146) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```sh
  mise lock --upgrade        # move an existing lockfile to revision 2 and resolve graphs
  mise install --locked      # replay the recorded graphs
  mise lock --bump pypi:black  # refresh Black's dependencies without changing its version
  ```
- **pypi:** `pypi:` is now the preferred name for the Python CLI backend; `pipx:` remains fully supported as an alias with no warnings, and settings accept both `pypi.*` and `pipx.*` names. The two spellings are distinct tool identities (`pypi-black` vs `pipx-black` install directories and lock entries), so switching spelling creates a new installation. ([#&#8203;13146](https://redirect.github.com/jdx/mise/pull/13146) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** `mise bootstrap dotfiles conflicts [PATH...]` shows a read-only comparison of the saved local and fetched remote versions of a conflicted dotfile so you can decide between `--take-remote` and `--keep-local` with full context. The default output is a unified diff including file-mode changes; `--difftool` opens the configured Git `diff.tool` (falling back to `merge.tool`) and `--tool <name>` picks one explicitly. Encrypted contents are decrypted only into private temporary files, and inspection never modifies either side or marks the conflict resolved. Bootstrap secrets are also now resolved from the same composed config maps as dotfile discovery, so root-scoped dotfile templates can use secrets declared by their bootstrap root. ([#&#8203;13144](https://redirect.github.com/jdx/mise/pull/13144) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```sh
  mise bootstrap dotfiles conflicts ~/.config/mise/config.toml
  mise bootstrap dotfiles conflicts --difftool ~/.config/mise/config.toml
  ```
- **dotfiles:** Dotfile templates (`mode = "template"`) can reference `[bootstrap.secrets]` values with `{{ secret(name="...") }}`, matching managed bootstrap file templates. Dotfiles commands that render templates (`add`, `apply`, `diff`, `edit`, `status`, `unapply`) accept `--prompt-secrets`; without an available value, rendering fails closed. A full `mise bootstrap` run preflights dotfile templates before making changes, `mise bootstrap status` reports secrets used only by dotfiles, and textual diffs redact resolved secret values. ([#&#8203;13140](https://redirect.github.com/jdx/mise/pull/13140) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [bootstrap.secrets]
  api_token = "EXAMPLE_API_TOKEN"

  [dotfiles."~/.config/example/credentials"]
  source = "dotfiles/credentials.tmpl"
  mode = "template"
  ```

##### Fixed

- **lock:** Installing from a committed `mise.lock` no longer fails when the locked release is younger than `minimum_release_age`. The cutoff still applies when resolving unlocked fuzzy requests and when generating or bumping a lockfile, and `npm:`/`pypi:` still forward it to unpinned transitive dependencies, but a reviewed lock entry now reproduces immediately in CI instead of waiting for the release to cool. ([#&#8203;13128](https://redirect.github.com/jdx/mise/pull/13128) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **config:** A `.python-version` (or other idiomatic version file) containing `system` selects the system interpreter without printing the mise-specific `@system` deprecation warning, matching the existing `.tool-versions` exception. Explicit `python@system` requests from mise configuration or command arguments still warn. ([#&#8203;13132](https://redirect.github.com/jdx/mise/pull/13132) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **npm:** Embedded aube is updated to 2.2.16, fixing the Bun checksum install regression and ensuring local npm tarballs keep their manifest package name. ([#&#8203;13145](https://redirect.github.com/jdx/mise/pull/13145) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **registry:** The `mc` shorthand uses `aqua:minio/mc` again now that the upstream Aqua registry entry is restored, with `asdf:mise-plugins/mise-mc` kept as the fallback. ([#&#8203;13124](https://redirect.github.com/jdx/mise/pull/13124) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Security

- **history:** `history.describe_command` is now global-only. Previously an implicitly trusted project could set it and have a later dotfiles history checkpoint execute the project-controlled command with unencrypted tracked-file diffs. The setting is honored only from system/global configuration or `MISE_HISTORY_DESCRIBE_COMMAND`; project values are ignored with a warning. ([#&#8203;13134](https://redirect.github.com/jdx/mise/pull/13134) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **oci:** `mise oci build` now renders dotfile templates with a restricted engine: `secret()` is rejected and the `env` context, `get_env()`, `exec()`, and `read_file()` are unavailable, so ambient credentials cannot be baked into a publishable image layer. ([#&#8203;13140](https://redirect.github.com/jdx/mise/pull/13140) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Breaking Changes

- **Lockfile revision 2 is not readable by older mise versions.** Newly created lockfiles use revision 2, and existing files switch only when you run `mise lock --upgrade`. Upgrade collaborators and CI to this release before committing a revision 2 `mise.lock`, and commit the `.mise/locks/` (or `.config/mise/locks/`) sidecar directory alongside it. Revision 2 `--locked` installs fail if a recorded graph is missing or its digest does not match. If you gitignore `mise.local.lock`, also ignore its matching sidecar subdirectory (for example `.mise/locks/mise.local/`).
- **`history.describe_command` in project configuration is ignored.** Move it to `~/.config/mise/config.toml` or set `MISE_HISTORY_DESCRIBE_COMMAND`.
- **`mise oci build` dotfile templates** can no longer call `secret()`, `get_env()`, `exec()`, or `read_file()` or read the `env` context.

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.9.6...v2026.9.7>

##### 💚 Sponsor mise

mise is built and maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer at [**entire.io**](https://entire.io/), the title sponsor of his open source work.

If mise saves you or your team time, please consider becoming an [individual or company sponsor](https://jdx.dev/sponsors.html). Your support funds ongoing development and helps keep mise fast, free, and independent.

### [`v2026.9.6`](https://redirect.github.com/jdx/mise/releases/tag/v2026.9.6): : Project daemons, mise doctor project, and vfox backend tool discovery

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.9.5...v2026.9.6)

This release adds experimental project daemons backed by pitchfork, a `mise doctor project` command for project-declared diagnostic checks, and tool discovery from vfox backend plugins in `mise search`. It also changes the HTTP backend's default install layout so uninstall and prune reclaim disk space, speeds up warm `lockfile_mode = "generate"` installs and repeated OCI builds, and fixes a batch of nushell, monorepo, lockfile, brew, and Windows bootstrap issues.

#### Highlights

- **Services next to tools:** `[daemons]` declares background processes and PostgreSQL/Redis presets in `mise.toml`, managed through `mise daemons` and optionally started when you enter the project. `[doctor.checks.<name>]` lets projects declare their own environment probes for `mise doctor project`.
- **Discovery and output:** `mise search`, shell completion, and interactive `mise use` now include tools published by installed vfox backend plugins, and `settings.truncate` / `--no-truncate` disable terminal-width truncation (automatically when a coding agent is detected).
- **Storage and speed:** HTTP tools now extract into their own install directory by default (opt back into deduplication with `shared_extraction = true`), warm generate-mode installs skip needless lockfile rewrites, and OCI builds share a local tool-layer cache.

#### Added

- **daemons:** New experimental `[daemons]` section and `mise daemons` command family (`start`, `stop`, `restart`, `ls`, `status`, `logs`, `tui`) manage project background processes with pitchfork. PostgreSQL and Redis presets install the database as a tool (participating in lockfiles), supply connection environment variables and readiness checks, and keep project data across stop/start. Daemons with `auto = ["start", "stop"]` start when entering the project from an activated Bash, Zsh, or Fish shell and are released when the last shell session leaves. Requires `experimental = true` and pitchfork 2.25.0 or later; database presets are Unix-only and PostgreSQL uses loopback trust authentication intended for local development. ([#&#8203;13085](https://redirect.github.com/jdx/mise/pull/13085) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [settings]
  experimental = true

  [daemons]
  postgres = "18"
  redis = "8"

  [daemons.web]
  run = "npm run dev"
  port = 3000
  auto = ["start", "stop"]
  ```
- **doctor:** `mise doctor project` runs checks declared in `[doctor.checks.<name>]` with the project's environment and installed tools, reporting PASS/FAIL/error/skipped per check in text or `--json`. Checks support `description`, `hint`, `timeout` (default `10s`), `dir`, `shell`, and `os` selectors, run concurrently under the `jobs` limit, and exit nonzero when any check fails. Ordinary `mise doctor` does not run them, and hints are never executed. A follow-up aligned `dir` resolution with task conventions (config root for project configs including `~/mise.toml`, `~/` expansion), fixed head-of-line blocking when one probe hangs, and kept `nohup mise doctor project` alive on SIGHUP. ([#&#8203;13062](https://redirect.github.com/jdx/mise/pull/13062), [#&#8203;13089](https://redirect.github.com/jdx/mise/pull/13089) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [doctor.checks.openssl]
  description = "OpenSSL development files are discoverable"
  run = "pkg-config --exists openssl"
  hint = "Run `mise bootstrap packages apply` to install the declared build dependencies."
  timeout = "5s"
  os = ["linux", "macos"]
  ```
- **vfox:** Tools provided by installed vfox backend plugins now appear in `mise search`, shell completion, and interactive `mise use`, namespaced as `<plugin>:<tool>`. Plugins can implement `BackendListTools` for a finite catalog and/or `BackendSearchTools` for query-driven discovery in large ecosystems; a prefixed query like `npm:eslint` is routed only to that plugin. Results are cached, slow plugins fall back to stale cache, and existing plugins need no changes. `mise registry` remains registry-only. ([#&#8203;13111](https://redirect.github.com/jdx/mise/pull/13111) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **cli:** New `settings.truncate` (and `MISE_TRUNCATE`, default `true`) controls terminal-width shortening of table cells and task metadata. `mise ls`, `mise config ls`, and `mise bootstrap dotfiles status` gain `--truncate` / `--no-truncate`, and output is kept complete automatically when a known coding agent is detected. ([#&#8203;13112](https://redirect.github.com/jdx/mise/pull/13112) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```sh
  mise bootstrap dotfiles status --no-truncate
  ```
- **bootstrap:** `[bootstrap.macos.dock]` gains `apps`, an ordered list of pinned application paths. Status compares identity and order (ignoring Dock-added metadata), apply adds, removes, and reorders application tiles while preserving other tiles and `persistent-others`, and an empty list removes all application tiles. Paths must be absolute or home-relative `.app` bundles. ([#&#8203;13075](https://redirect.github.com/jdx/mise/pull/13075) by [@&#8203;azohra](https://redirect.github.com/azohra))

  ```toml
  [bootstrap.macos.dock]
  apps = [
    "/System/Applications/Utilities/Terminal.app",
    "/Applications/Firefox.app",
  ]
  ```
- **bootstrap:** `mise bootstrap packages where brew:<formula>` prints an installed formula's stable `opt` root (for example `/opt/homebrew/opt/unzip`), so scripts can put keg-only executables on PATH without hardcoding the Homebrew prefix or Cellar version. Missing installs exit nonzero with empty stdout. ([#&#8203;13083](https://redirect.github.com/jdx/mise/pull/13083) by [@&#8203;himkt](https://redirect.github.com/himkt))
- **dotfiles:** Destination `variants` can omit `source` when every variant sets a `target`; the entry key is then resolved as a relative path under `settings.dotfiles.root` instead of next to `mise.toml`. Parent traversal is rejected. ([#&#8203;13087](https://redirect.github.com/jdx/mise/pull/13087) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [dotfiles."vscode/settings.json"]
  mode = "copy"
  variants = [
    { os = "macos", target = "~/Library/Application Support/Code/User/settings.json" },
    { os = "linux", target = "~/.config/Code/User/settings.json" },
  ]
  ```
- **fmt:** `mise fmt` now sorts lists whose order has no meaning: `redactions` lexically, and task `sources`/`outputs`, `task_templates` sources/outputs, `task_config.global_inputs`, and `input_groups` by reach (`@group:` references, then globs, then literal paths). Lists containing `!` exclusions, entries starting with template syntax, or comments are left untouched, and precedence-sensitive lists such as `env_file`, `tools.*`, `includes`, and `depends` are never sorted. ([#&#8203;13058](https://redirect.github.com/jdx/mise/pull/13058) by [@&#8203;jrandolf](https://redirect.github.com/jrandolf))
- **oci:** `mise oci build` gains `--no-cache` to bypass the new local tool-layer cache; entries live under each tool's cache directory and are removed by `mise cache clear TOOL`. ([#&#8203;13056](https://redirect.github.com/jdx/mise/pull/13056) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Changed

- **http:** New `http:` installations extract directly into their own install directory, so `mise uninstall` and `mise prune` now remove their files instead of leaving payloads in `$MISE_DATA_DIR/http-tarballs/`. Set `shared_extraction = true` on a tool to keep the previous deduplicated symlink layout. Existing symlinked installs keep working; `mise install --force <tool>` migrates one to independent files without disturbing other installs that share the content. Legacy `http-tarballs` entries are not reclaimed automatically. Shared raw and compressed binary caches now also include the executable filename in their key, so differently named tools no longer reuse the wrong filename. ([#&#8203;13059](https://redirect.github.com/jdx/mise/pull/13059) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **oci:** `mise oci push --no-cache` now bypasses both the remote registry cache and the local tool-layer cache. ([#&#8203;13056](https://redirect.github.com/jdx/mise/pull/13056) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **registry:** `postgres`, `redis`, and `mongodb` now prefer `conda:` backends, installing prebuilt conda-forge binaries in seconds instead of compiling through vfox; vfox and asdf remain as fallbacks. `conda:redis-server` covers Linux and macOS only. ([#&#8203;13061](https://redirect.github.com/jdx/mise/pull/13061) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Performance

- **lockfile:** Warm `mise install` runs in `lockfile_mode = "generate"` skip scheduling work for tools whose artifact metadata is already reusable, and skip rebuilding, serializing, and staging the lockfile entirely when nothing was installed and the on-disk lock already matches (preserving comments in the file). Explicit `mise lock`, forced provenance verification, upgrades, and new platforms still regenerate. ([#&#8203;13101](https://redirect.github.com/jdx/mise/pull/13101), [#&#8203;13103](https://redirect.github.com/jdx/mise/pull/13103) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **oci:** `mise oci push --from BASE` no longer downloads base layers when the base and target live in the same repository, and `oci build`, `oci run`, and `oci push` share a local cache of packaged tool layers keyed on file contents, so repeated builds with overlapping tools skip tar and gzip work. ([#&#8203;13055](https://redirect.github.com/jdx/mise/pull/13055), [#&#8203;13056](https://redirect.github.com/jdx/mise/pull/13056) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Fixed

- **nushell:** `mise activate nu` no longer throws `env_variable_not_found` on every prompt or `cd` when a variable to hide is absent from the current scope; `hide-env` is now wrapped in `try`, matching the no-op behavior of other shells. ([#&#8203;13071](https://redirect.github.com/jdx/mise/pull/13071) by [@&#8203;i-api](https://redirect.github.com/i-api))
- **task:** Task-level tools are now auto-installed for tasks referenced from `run` entries, including names rendered at runtime, right before they execute; `--skip-tools` is honored and install failures are reported as task failures without blocking siblings. ([#&#8203;13086](https://redirect.github.com/jdx/mise/pull/13086) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **config:** `[monorepo]` settings are now merged across same-directory config layers (base plus `mise.<env>.toml` overlays): omitted fields are inherited, an overlay's `config_roots` replaces the base list, and `monorepo_root = false` in an overlay disables the root and its descendant trust. ([#&#8203;13084](https://redirect.github.com/jdx/mise/pull/13084) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **lockfile:** Runtime tool requests such as `mise which hk --tool hk@latest` now use the lockfile belonging to the config that effectively defines the tool, instead of merging project and global pins and reporting a false "multiple resolutions" ambiguity or selecting an overridden pin. `mise which --tool` warns when a lower-precedence config has a matching pin the effective config lacks. ([#&#8203;13042](https://redirect.github.com/jdx/mise/pull/13042) by [@&#8203;nettlesh](https://redirect.github.com/nettlesh))
- **lockfile:** Complete lockfile generation for Packslip tools skips platforms with no published artifact while still writing supported targets, and a verified Packslip signer may now replace legacy `github-attestations` metadata on upgrade instead of being rejected as a provenance downgrade. ([#&#8203;13102](https://redirect.github.com/jdx/mise/pull/13102), [#&#8203;13105](https://redirect.github.com/jdx/mise/pull/13105) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **upgrade:** `mise upgrade` now detects updates between letter-suffixed versions such as tmux `3.7b` to `3.7c`; `sub-N` aliases keep resolving numeric components as before. ([#&#8203;13119](https://redirect.github.com/jdx/mise/pull/13119) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **brew:** Formulae that are keg-only solely because macOS ships them (such as `brew:zip` and `brew:unzip`) are now linked into `<prefix>/bin` on Linux, matching Homebrew. Kegs installed by earlier mise versions stay unlinked until the next `mise bootstrap packages upgrade` or a reinstall. ([#&#8203;13108](https://redirect.github.com/jdx/mise/pull/13108) by [@&#8203;lil-lon](https://redirect.github.com/lil-lon))
- **brew-cask:** Tap casks with `preflight_steps` or `postflight_steps` no longer fail during metadata extraction; declarative `run` steps are captured as structured steps and executed by mise, with support for `must_succeed = false`. ([#&#8203;13060](https://redirect.github.com/jdx/mise/pull/13060) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** `mise bootstrap remote` on Windows now finds `ssh.exe` and `tar.exe` on PATH instead of failing with `required command 'ssh' not found`. ([#&#8203;13117](https://redirect.github.com/jdx/mise/pull/13117) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **bootstrap:** macOS defaults status explains type mismatches, showing for example `2 (real; expected integer)` instead of two identical-looking values marked `differs`. ([#&#8203;13096](https://redirect.github.com/jdx/mise/pull/13096) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **dotfiles:** `mise bootstrap dotfiles track` honors the global `yes` setting for confirmations, and warns when tracking a symlink whose resolved source is not itself tracked, suggesting the command to enroll it. ([#&#8203;13072](https://redirect.github.com/jdx/mise/pull/13072) by [@&#8203;nettlesh](https://redirect.github.com/nettlesh), [#&#8203;13095](https://redirect.github.com/jdx/mise/pull/13095) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **schema:** The JSON schema now models Git directory `manifest` dotfile entries, restricting explicit modes to `copy` or `symlink-each` and rejecting combinations with inline content or file-edit fields. ([#&#8203;12741](https://redirect.github.com/jdx/mise/pull/12741) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **asdf:** `mise asdf install` and `mise asdf reshim` no longer re-enter the full CLI dispatch, avoiding stack overflows on small-stack Linux environments; `asdf install` now follows the same implicit config trust as `mise install`. Bash completions are regenerated for the updated usage-rs word-break handling. ([#&#8203;13114](https://redirect.github.com/jdx/mise/pull/13114) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Registry

- Added `mpv` (`conda:mpv`, Linux and macOS) ([#&#8203;13049](https://redirect.github.com/jdx/mise/pull/13049) by [@&#8203;i-api](https://redirect.github.com/i-api)), `agent-browser` (`aqua:vercel-labs/agent-browser`) ([#&#8203;13088](https://redirect.github.com/jdx/mise/pull/13088) by [@&#8203;3w36zj6](https://redirect.github.com/3w36zj6)), and `himalaya` (`github:pimalaya/himalaya`) ([#&#8203;13091](https://redirect.github.com/jdx/mise/pull/13091) by [@&#8203;i-api](https://redirect.github.com/i-api)).
- `editorconfig-checker` installs again after 4.0.1 renamed its assets and executable; the shorthand now uses the GitHub backend. ([#&#8203;13098](https://redirect.github.com/jdx/mise/pull/13098) by [@&#8203;jdx](https://redirect.github.com/jdx))
- `mc` installs from MinIO's GitHub releases instead of the retired Aqua download URL that returned HTTP 410. ([#&#8203;13113](https://redirect.github.com/jdx/mise/pull/13113) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Documentation

- The docs landing page gains interactive diagrams for project environment switching, bootstrap machine resources, tool configuration precedence, artifact cache execution, and tracked dotfile synchronization. ([#&#8203;13065](https://redirect.github.com/jdx/mise/pull/13065), [#&#8203;13066](https://redirect.github.com/jdx/mise/pull/13066), [#&#8203;13067](https://redirect.github.com/jdx/mise/pull/13067), [#&#8203;13068](https://redirect.github.com/jdx/mise/pull/13068), [#&#8203;13069](https://redirect.github.com/jdx/mise/pull/13069) by [@&#8203;jdx](https://redirect.github.com/jdx))
- The task-running guide is reorganized around finding and running tasks, passing arguments, then controlling execution. ([#&#8203;13077](https://redirect.github.com/jdx/mise/pull/13077) by [@&#8203;azohra](https://redirect.github.com/azohra))

#### New Contributors

- [@&#8203;lil-lon](https://redirect.github.com/lil-lon) made their first contribution in [#&#8203;13108](https://redirect.github.com/jdx/mise/pull/13108)

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.9.5...v2026.9.6>

#### 💚 Sponsor mise

mise is built and maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer at [**entire.io**](https://entire.io/), the title sponsor of his open source work.

If mise saves you or your team time, please consider becoming an [individual or company sponsor](https://jdx.dev/sponsors.html). Your support funds ongoing development and helps keep mise fast, free, and independent.

### [`v2026.9.5`](https://redirect.github.com/jdx/mise/releases/tag/v2026.9.5): : macOS defaults, per-platform dotfiles, and complete lockfiles

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.9.4...vfox-v2026.9.5)

This release deepens macOS bootstrap support with current-host and nested defaults, lets dotfiles and tasks adapt to the platform and to parsed arguments, and adds an opt-in trial of complete lockfile generation. It also carries a batch of install progress, self-update, brew-cask, and sandbox fixes.

#### Added

- **bootstrap:** New `[[bootstrap.macos.defaults_entries]]` blocks let you set macOS preferences explicitly with `domain`, `key`, `value`, and an optional `host` (`any` by default, or `current`), covering preferences normally written via `defaults -currentHost` while keeping the existing `[bootstrap.macos.defaults]` shorthand. Entries also accept an optional `path` to patch a nested dictionary value without replacing its siblings, preserving property-list types and creating missing parents. ([#&#8203;12983](https://redirect.github.com/jdx/mise/pull/12983) by [@&#8203;azohra](https://redirect.github.com/azohra), [#&#8203;12984](https://redirect.github.com/jdx/mise/pull/12984) by [@&#8203;azohra](https://redirect.github.com/azohra))

  ```toml
  [[bootstrap.macos.defaults_entries]]
  domain = "com.apple.dock"
  key = "autohide"
  value = true
  host = "current"
  ```
- **bootstrap:** More friendly macOS preferences: Finder folder sorting and default cloud save location, Dock autohide delay and timing (integers or floats), and keyboard automatic capitalization and spelling correction, all using snake\_case names consistent with the existing sections. ([#&#8203;13032](https://redirect.github.com/jdx/mise/pull/13032) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** `[bootstrap.files]` and `[bootstrap.directories]` entries gain `phase = "pre-packages"` so repository definitions, apt sources, and signing keys can be applied before package installation instead of only afterward. Existing declarations default to `"post-packages"`. ([#&#8203;13052](https://redirect.github.com/jdx/mise/pull/13052) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [bootstrap.files."/etc/apt/sources.list.d/vendor.sources"]
  source = "./files/vendor.sources"
  phase = "pre-packages"
  ```
- **bootstrap:** Ordinary `[bootstrap.files]` templates can now reference resolved `[vars]` values, alongside the existing `config_root`, `target`, and `secret()` helpers. ([#&#8203;13033](https://redirect.github.com/jdx/mise/pull/13033) by [@&#8203;nettlesh](https://redirect.github.com/nettlesh))
- **dotfiles:** A single dotfiles source can deploy to different destinations per operating system, architecture, or mise profile using `variants` with an optional `target`. This works for `copy`, `symlink`, `symlink-each`, and `template` modes. ([#&#8203;13050](https://redirect.github.com/jdx/mise/pull/13050) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [dotfiles.settings]
  source = "dotfiles/vscode/settings.json"
  mode = "copy"
  variants = [
    { os = "macos", target = "~/Library/Application Support/Code/User/settings.json" },
    { os = "linux", target = "~/.config/Code/User/settings.json" },
  ]
  ```
- **task:** Task `sources` and `outputs` can now use `{{usage.*}}` templates, resolved per invocation from parsed arguments and flags before freshness and artifact-cache checks run, so different argument values track freshness independently. ([#&#8203;13051](https://redirect.github.com/jdx/mise/pull/13051) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **brew-cask:** Casks with structured `set_permissions` preflight/postflight steps now install correctly (for example `brew-cask:blender`), running an unprivileged `chmod` over resolved staged or appdir paths instead of failing with an unsupported step-type error. ([#&#8203;13043](https://redirect.github.com/jdx/mise/pull/13043) by [@&#8203;azohra](https://redirect.github.com/azohra))
- **lock:** Opt-in trial of complete lockfile generation via `lockfile_mode = "generate"` (or `MISE_LOCKFILE_MODE=generate`). The default remains incremental `merge`. Generate mode rebuilds lockfiles from current requests while treating the previous file as an immutable baseline, reusing unchanged artifacts and publishing through staged atomic writes so failures or concurrent edits do not clobber a good lockfile. This mode records only cryptographically verified provenance per target platform; `provenance_verified` is no longer treated as a trust signal. ([#&#8203;13031](https://redirect.github.com/jdx/mise/pull/13031) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **self-update:** New `disable_update_warning` setting (`MISE_DISABLE_UPDATE_WARNING`) suppresses "newer mise available" notices in `mise version`, `mise --version`, and `mise doctor`. Explicit self-update and automatic updates are unaffected. ([#&#8203;13028](https://redirect.github.com/jdx/mise/pull/13028) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Fixed

- **install:** Interactive installs no longer leave a permanent line for every resolved, skipped, or already-installed tool; live progress shows what is happening while the final summary lists what changed (for example `installed 1 tool in 1.1s: [email protected]`). `mise upgrade` no longer duplicates its old to new version list. ([#&#8203;13030](https://redirect.github.com/jdx/mise/pull/13030) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **install:** Long non-TTY installs no longer flood CI logs with a snapshot every three seconds. The heartbeat now scales to roughly 10% of elapsed time, clamped between 3 seconds and 1 minute. ([#&#8203;13036](https://redirect.github.com/jdx/mise/pull/13036) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **self-update:** On 32-bit ARM, `mise self-update` now selects the correct `linux-armv7` archive instead of requesting a missing `linux-arm` one and falling back to an ARM64 binary that failed signature verification. A missing archive now fails asset selection rather than picking the wrong architecture. ([#&#8203;13023](https://redirect.github.com/jdx/mise/pull/13023) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **self-update:** npm installs now ship the instructions file that redirects update guidance to the package manager, so they no longer advertise `mise self-update`. ([#&#8203;13028](https://redirect.github.com/jdx/mise/pull/13028) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **brew-cask:** `mise bootstrap packages upgrade` no longer replaces the bundle of a running self-updating app (for example Chrome), which could strand helper processes and blank out tabs. Such apps are skipped while running and left to update themselves. ([#&#8203;13041](https://redirect.github.com/jdx/mise/pull/13041) by [@&#8203;azohra](https://redirect.github.com/azohra))
- **brew-cask:** The cask metadata fetch error no longer includes stale advice about installing with `brew` and a broken documentation anchor; it now gives a short, accurate message. ([#&#8203;12800](https://redirect.github.com/jdx/mise/pull/12800) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))
- **bootstrap:** `mise bootstrap dotfiles origin set <url>` now uses the repository's own default branch when `--branch` is omitted, so repositories on `master` connect correctly instead of publishing a second root branch. A missing requested branch is now reported clearly rather than mistaken for an empty repository. ([#&#8203;13037](https://redirect.github.com/jdx/mise/pull/13037) by [@&#8203;Dhaulagiri](https://redirect.github.com/Dhaulagiri))
- **dotfiles:** History watcher locks now live alongside the history store in the state directory instead of being hashed into the cache directory, so a launchd watcher and an interactive shell using different cache directories coordinate correctly and `dotfiles status` no longer misreports `declared-not-running`. Existing watchers must be stopped and restarted with the updated binary. ([#&#8203;13038](https://redirect.github.com/jdx/mise/pull/13038) by [@&#8203;ascarter](https://redirect.github.com/ascarter))
- **sandbox:** The macOS Seatbelt profile now allows `file-read-metadata` on the ancestors of readable paths, fixing `Operation not permitted` failures when a portable Ruby resolves its own executable during third-party tap evaluation. Symlinked data directories and allow-listed paths are also handled. ([#&#8203;13039](https://redirect.github.com/jdx/mise/pull/13039) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))

#### Registry

- Added `clipboard` ([github:Slackadays/Clipboard](https://redirect.github.com/Slackadays/Clipboard)) by [@&#8203;i-api](https://redirect.github.com/i-api) in [#&#8203;13044](https://redirect.github.com/jdx/mise/pull/13044)

#### New Contributors

- [@&#8203;Dhaulagiri](https://redirect.github.com/Dhaulagiri) made their first contribution in [#&#8203;13037](https://redirect.github.com/jdx/mise/pull/13037)

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.9.4...v2026.9.5>

#### 💚 Sponsor mise

mise is built and maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer at [**entire.io**](https://entire.io/), the title sponsor of his open source work.

If mise saves you or your team time, please consider becoming an [individual or company sponsor](https://jdx.dev/sponsors.html). Your support funds ongoing development and helps keep mise fast, free, and independent.

### [`v2026.9.4`](https://redirect.github.com/jdx/mise/releases/tag/v2026.9.4): : Nix Bootstrap, Environment Selectors, and Man Pages

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.9.3...vfox-v2026.9.4)

This release expands the bootstrap package model with Nix support and environment selectors, teaches packslip tools to ship man pages, and adds a task-scoped quiet setting. It also carries a wide batch of packslip, bootstrap, and cross-platform fixes plus a major speedup to dotfiles history rebuilds.

#### Added

- **bootstrap:** Nix is now a built-in `[bootstrap.packages]` manager on Linux and macOS. Declare packages with the `nix:` prefix and apply them through your normal Nix profile; the manager supports use, apply, status JSON, and targeted upgrades while leaving Nix sources, caches, trust, and profile rollback under Nix control. This also adds `mise bootstrap packages export --format nix` to emit a deterministic NixOS module from your `nix:` declarations and `packages use --no-install` to record declarations without touching package managers. ([#&#8203;13013](https://redirect.github.com/jdx/mise/pull/13013) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [bootstrap.packages]
  "nix:ripgrep" = "latest"
  ```
- **bootstrap:** `[bootstrap.packages]` entries gain an `env` selector (a single environment or a list), so a package is only enabled when one of its listed mise environments is active via `-E` or `MISE_ENV`. When both `os` and `env` are set, both must match. Packages for inactive environments stay declared and are protected from pruning. ([#&#8203;12956](https://redirect.github.com/jdx/mise/pull/12956) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [bootstrap.packages]
  "brew:postgresql" = { version = "latest", env = ["dev", "test"] }
  ```
- **packslip:** Packslip-installed tools can now ship man pages declared as static `man` resources, alongside completions and agent skills. While a tool version is active, mise prepends its man root to `MANPATH` and keeps system and caller-defined paths visible, so `man <tool>` works. Existing installs need to be reinstalled to pick up man pages. ([#&#8203;13012](https://redirect.github.com/jdx/mise/pull/13012) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **task:** New `task.quiet` setting (and `MISE_TASK_QUIET`) suppresses mise's own task messages, prefixes, and command-echo headers without hiding task output or affecting other commands. The bundled `output = "quiet"` mode is deprecated in favor of explicit output style plus this setting; removal is scheduled for 2027.9.3. ([#&#8203;12980](https://redirect.github.com/jdx/mise/pull/12980) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Fixed

- **install:** A lazy tool whose `depends` target is also lazy now installs correctly on first use through a shim, `mise x`, or a task; mise installs the provider together with its still-missing configured dependencies instead of failing the preflight. ([#&#8203;12997](https://redirect.github.com/jdx/mise/pull/12997) by [@&#8203;balintant](https://redirect.github.com/balintant))
- **backend:** 32-bit ARM resolution now uses Go's canonical `arm` architecture name for Aqua (so registry replacements apply), and automatic GitHub release asset selection no longer picks a generic `source.tar.gz` when no published binary targets the host. ([#&#8203;13004](https://redirect.github.com/jdx/mise/pull/13004) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** Homebrew bottles with hard-linked Mach-O executables are now signed correctly on macOS. Every hard-link alias is tracked and included in the signing list after relocation, fixing cases like fish where an aliased binary was killed with SIGKILL after a successful install. ([#&#8203;12988](https://redirect.github.com/jdx/mise/pull/12988) by [@&#8203;nettlesh](https://redirect.github.com/nettlesh))
- **packslip:** `mise lock --platform` now verifies the signed release manifest and records the correct URL, checksum, size, and signer for each requested target platform (including Windows x64), so locked installs work across platforms. ([#&#8203;13002](https://redirect.github.com/jdx/mise/pull/13002) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **packslip:** On glibc Linux, an artifact's `glibc_min` is now honored during selection: when the GNU build requires a newer glibc than the host, mise falls back to a matching static musl build if one exists. ([#&#8203;13009](https://redirect.github.com/jdx/mise/pull/13009) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **packslip:** Windows installs now link shims with the correct `.exe` filename, while Unix keeps extensionless names. ([#&#8203;13006](https://redirect.github.com/jdx/mise/pull/13006) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **packslip:** Activated man roots are now scoped to Packslip-backed tool versions, and the caller's original `MANPATH` is included in the environment-cache identity so one cached process cannot serve another caller's `MANPATH`. ([#&#8203;13016](https://redirect.github.com/jdx/mise/pull/13016) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **prune:** `mise prune` no longer exits successfully without pruning when it cannot show a confirmation prompt; it now fails with guidance to pass `--yes` or set `MISE_YES=1`. Follow-up prunes from commands like `mise unuse` still leave installs in place when nobody can answer. ([#&#8203;13003](https://redirect.github.com/jdx/mise/pull/13003) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **generate:** `mise generate tool-stub` now respects a stub's top-level `os` selector when deciding whether to write a Windows `.cmd` launcher, so a linux/macos-only stub no longer produces a launcher even with `--lock`. ([#&#8203;13008](https://redirect.github.com/jdx/mise/pull/13008) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **dotfiles:** History path filters resolve correctly when `HOME` is a symlink, so aliased, canonical, and tilde paths address the same history entry and `show latest`/`diff` no longer report a missing checkpoint. ([#&#8203;12982](https://redirect.github.com/jdx/mise/pull/12982) by [@&#8203;azohra](https://redirect.github.com/azohra))
- **brew:** The published crate now includes the tap formula and cask metadata shims, fixing `cargo install --locked mise` builds from crates.io. ([#&#8203;13001](https://redirect.github.com/jdx/mise/pull/13001) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Changed

- **bootstrap:** `[bootstrap].config_roots` and the `mise bootstrap config-roots` command are now deprecated (hidden from help and removed from public docs), with removal scheduled for mise 2027.3.3. Existing configurations keep working during the compatibility window but emit a warning directing users to global or system bootstrap configuration. ([#&#8203;13010](https://redirect.github.com/jdx/mise/pull/13010) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **registry:** Added basecamp to the registry. ([#&#8203;13024](https://redirect.github.com/jdx/mise/pull/13024))

#### Performance

- **history:** Dotfiles history index rebuilds are dramatically faster, reconstructing checkpoint metadata in-process with gix instead of spawning Git per checkpoint. On an 80-checkpoint, 44-file reproducer this cut a rebuild from roughly 26-28 seconds and thousands of Git processes down to under a second. Removed annotations are now correctly dropped after a rebuild, and SHA-256 history repositories are supported. ([#&#8203;13011](https://redirect.github.com/jdx/mise/pull/13011) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Deprecated

- The task `output = "quiet"` mode (use `task.quiet` instead; removal in 2027.9.3) ([#&#8203;12980](https://redirect.github.com/jdx/mise/pull/12980)).
- `[bootstrap].config_roots` and `mise bootstrap config-roots` (removal in mise 2027.3.3) ([#&#8203;13010](https://redirect.github.com/jdx/mise/pull/13010)).

#### New Contributors

- [@&#8203;balintant](https://redirect.github.com/balintant) made their first contribution in [#&#8203;12997](https://redirect.github.com/jdx/mise/pull/12997)

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.9.3...v2026.9.4>

#### 💚 Sponsor mise

mise is built and maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer at [**entire.io**](https://entire.io/), the title sponsor of his open source work.

If mise saves you or your team time, please consider becoming an [individual or company sponsor](https://jdx.dev/sponsors.html). Your support funds ongoing development and helps keep mise fast, free, and independent.

### [`v2026.9.3`](https://redirect.github.com/jdx/mise/releases/tag/v2026.9.3): : Winget bootstrap, signed vfox plugins, and faster inline commands

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.9.2...vfox-v2026.9.3)

This release brings Windows bootstrap into the package-manager era with WinGet support, extends the packslip signing model to vfox plugins, and speeds up simple tasks by skipping the shell on Unix. It also carries a batch of bootstrap, sandbox, and Windows ARM64 fixes.

##### Added

- **bootstrap:** WinGet is now a built-in `[bootstrap.packages]` manager on Windows, so `mise bootstrap packages` can check status, install, and upgrade apps by exact package ID. It supports version pins, source refresh, and automatic source-agreement acceptance. Scoop, Chocolatey, and package removal remain future work. ([#&#8203;12928](https://redirect.github.com/jdx/mise/pull/12928) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [bootstrap.packages]
  "winget:Microsoft.PowerShell" = "latest"
  ```
- **vfox:** External vfox plugins can now be installed from signed packslip archives instead of a Git clone, reusing the packslip backend's release selection, signature verification, digest checks, and signer pins. This is aimed at non-registry plugins; registry defaults and embedded plugins are unchanged. ([#&#8203;12948](https://redirect.github.com/jdx/mise/pull/12948) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```sh
  mise plugins install vfox:bfs 'packslip:mise-plugins/vfox-bfs#0.1.0'
  ```
- **bootstrap:** Dotfile line edits gain `position = "prepend"` to insert a managed line at the top of a file instead of appending (the default). An existing exact match is left in place, and unrelated bytes, CRLF endings, and a UTF-8 BOM are preserved. ([#&#8203;12941](https://redirect.github.com/jdx/mise/pull/12941) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [dotfiles]
  "/etc/zshrc/zdotdir" = { line = 'ZDOTDIR=$HOME/.config/zsh/', position = "prepend" }
  ```
- **bootstrap:** macOS bootstrap `defaults` now support nested TOML arrays and tables (for example Dock `persistent-apps`), reading and writing preferences through Core Foundation so booleans, numbers, strings, arrays, and dictionaries keep their plist types. Collections apply as whole-value replacements; datetimes and binary plist data are still skipped with a warning. ([#&#8203;12947](https://redirect.github.com/jdx/mise/pull/12947) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Changed

- **bootstrap:** Adopting global configuration or a shared dotfile-history setup repository is now spelled `mise bootstrap --adopt` (and `mise bootstrap remote --adopt`), separate from `mise bootstrap --from` for running a bootstrap project. The dotfiles, history, setup, and services guides were reorganized to lead with usable examples. The previous `--from-git` spelling still works as a hidden alias but warns and is scheduled for removal in mise 2026.10.0. ([#&#8203;12953](https://redirect.github.com/jdx/mise/pull/12953) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Fixed

- **github:** Git operations (clones, fetches, submodules) that GitHub now challenges with HTTP 401 on public repositories can authenticate using mise's existing GitHub token sources via a command-scoped `github.com` HTTPS credential helper. Tokens are kept out of command arguments and URLs, and Enterprise hosts are out of scope. ([#&#8203;12945](https://redirect.github.com/jdx/mise/pull/12945) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **config:** The default inline task shell is now `sh -o errexit -c` so shells that consume the argument right after `-c` (such as FreeBSD sh) run your command instead of trying to execute `-o`. ([#&#8203;12949](https://redirect.github.com/jdx/mise/pull/12949) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **windows:** Fixed two Windows ARM64 issues: stale command-wrapper shims (like a leftover `cargo.exe`) that caused recursive `.exe`/`.cmd` dispatch after shim-mode changes are now pruned, and Aqua ARM64 emulation candidates apply the registry's `amd64` replacement so tools like LuaLS resolve their published `win32-x64` assets. ([#&#8203;12931](https://redirect.github.com/jdx/mise/pull/12931) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** Bootstrap no longer aborts on Arch when `pacman -Q` prints a file advisory for a missing package that shares its name with a directory in the current working directory (for example a `fish/` folder in a dotfiles checkout). ([#&#8203;12932](https://redirect.github.com/jdx/mise/pull/12932) by [@&#8203;nettlesh](https://redirect.github.com/nettlesh))
- **sandbox:** Read-restricted macOS Seatbelt profiles now allow metadata-only access to `/private`, fixing startup failures for load-relative binaries like Ruby 4.0.6 when run under `/private/tmp`. Directory listings and descendant reads stay denied. ([#&#8203;12940](https://redirect.github.com/jdx/mise/pull/12940) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** The macOS notification helper is now built and Developer ID-signed at release time and spawned with a start handshake, so short `dotfiles sync` runs no longer race the helper and macOS accepts it. Unsigned source builds (such as Homebrew) disable notifications and warn during origin setup. ([#&#8203;12946](https://redirect.github.com/jdx/mise/pull/12946) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **vfox:** vfox install phases now flow through the active install progress reporter, mapping noisy download, checksum, attestation, and extraction lines into concise phases instead of scrolling log output. ([#&#8203;12944](https://redirect.github.com/jdx/mise/pull/12944) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **java:** The `java -version` banner shown during install verification now stays inside the interactive install progress row instead of printing to the terminal. ([#&#8203;12943](https://redirect.github.com/jdx/mise/pull/12943) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **version:** The auto-update hint from `mise version` now uses the `mise settings auto_update=true` syntax to match the rest of `mise settings`. ([#&#8203;12957](https://redirect.github.com/jdx/mise/pull/12957) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Performance

- On Unix, plain inline commands (like `node build.js`) now run directly when a conservative planner deems them safe, skipping the shell wrapper. Anything involving shell syntax, quoting, expansion, builtins, ambiguous PATH lookup, explicit shell settings, or sandboxed/audited tasks still uses the shell, and Windows is unchanged. In a microbenchmark of short commands this cut about 11% off elapsed time. ([#&#8203;12950](https://redirect.github.com/jdx/mise/pull/12950) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Registry

- Added cargo-deny, kingfisher, nushell, shellharden, sherif, and tauri-cli. ([#&#8203;12938](https://redirect.github.com/jdx/mise/pull/12938) by [@&#8203;jrandolf](https://redirect.github.com/jrandolf))

##### Documentation

- Restored the prominent project name and pronunciation on the homepage. ([#&#8203;12936](https://redirect.github.com/jdx/mise/pull/12936) by [@&#8203;jdx](https://redirect.github.com/jdx))
- Dropped the "you need Git installed" prerequisite from the install docs. ([#&#8203;12958](https://redirect.github.com/jdx/mise/pull/12958) by [@&#8203;jdx](https://redirect.github.com/jdx))

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.9.2...v2026.9.3>

##### 💚 Sponsor mise

mise is built and maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer at [**entire.io**](https://entire.io/), the title sponsor of his open source work.

If mise saves you or your team time, please consider becoming an [individual or company sponsor](https://jdx.dev/sponsors.html). Your support funds ongoing development and helps keep mise fast, free, and independent.

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC43OS4xIiwidXBkYXRlZEluVmVyIjoiNDQuNzkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2026-09-14 08:03:05 -07:00
renovate[bot] 9c43369ad3 chore(deps): update dependency @types/node to v24.13.4 (#695)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@types/node](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) ([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)) | [`24.13.3` → `24.13.4`](https://renovatebot.com/diffs/npm/@types%2fnode/24.13.3/24.13.4) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fnode/24.13.4?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fnode/24.13.3/24.13.4?slim=true) |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC43OS4xIiwidXBkYXRlZEluVmVyIjoiNDQuNzkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2026-09-14 08:02:50 -07:00
renovate[bot] ca45510e08 chore(deps): update dependency @expo/batcher to v1.1.2 (#691)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| @&#8203;expo/batcher | [`1.0.0` → `1.1.2`](https://renovatebot.com/diffs/npm/@expo%2fbatcher/1.0.0/1.1.2) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@expo%2fbatcher/1.1.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@expo%2fbatcher/1.0.0/1.1.2?slim=true) |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC42MS4zIiwidXBkYXRlZEluVmVyIjoiNDQuNjEuMyIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2026-09-08 08:18:03 -07:00
renovate[bot] 4f5d13d5c7 chore(deps): update jest to v30.5.1 (#693)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@jest/globals](https://redirect.github.com/jestjs/jest) ([source](https://redirect.github.com/jestjs/jest/tree/HEAD/packages/jest-globals)) | [`30.4.1` → `30.5.1`](https://renovatebot.com/diffs/npm/@jest%2fglobals/30.4.1/30.5.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@jest%2fglobals/30.5.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@jest%2fglobals/30.4.1/30.5.1?slim=true) |
| [jest](https://jestjs.io/) ([source](https://redirect.github.com/jestjs/jest/tree/HEAD/packages/jest)) | [`30.4.2` → `30.5.1`](https://renovatebot.com/diffs/npm/jest/30.4.2/30.5.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/jest/30.5.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/jest/30.4.2/30.5.1?slim=true) |

---

### Release Notes

<details>
<summary>jestjs/jest (@&#8203;jest/globals)</summary>

### [`v30.5.1`](https://redirect.github.com/jestjs/jest/blob/HEAD/CHANGELOG.md#3051)

[Compare Source](https://redirect.github.com/jestjs/jest/compare/v30.5.0...v30.5.1)

##### Fixes

- `[jest-config]` Don't warn about global-only options in the config that supplies the global config - the root config a project resolves to, or the first entry of `--projects` when no root config is passed ([#&#8203;16411](https://redirect.github.com/jestjs/jest/pull/16411))
- `[jest-config, jest-types]` Stop accepting `reporters`, `coverageReporters`, `workerIdleMemoryLimit`, `cwd` and `runnerOptions` in a project config - they were silently ignored, and now warn like the other global-only options ([#&#8203;16411](https://redirect.github.com/jestjs/jest/pull/16411))
- `[jest-config, jest-validate]` Warn about `maxWorkers` and `coverageThreshold` in a project config instead of dropping them without a word ([#&#8203;16411](https://redirect.github.com/jestjs/jest/pull/16411))
- `[jest-resolve]` Match `moduleNameMapper` patterns against the specifier as written again (reverting [#&#8203;16390](https://redirect.github.com/jestjs/jest/pull/16390)) ([#&#8203;16417](https://redirect.github.com/jestjs/jest/pull/16417))
- `[jest-runtime]` Resolve package `imports` specifiers like `#dep` under ESM again ([#&#8203;16413](https://redirect.github.com/jestjs/jest/pull/16413))

##### Chore & Maintenance

- `[jest-util]` Name the `testEnvironmentOptions.globalsCleanup` option and link the docs from the `JEST-01` deprecation warning, and document the option's modes ([#&#8203;16404](https://redirect.github.com/jestjs/jest/pull/16404))

### [`v30.5.0`](https://redirect.github.com/jestjs/jest/blob/HEAD/CHANGELOG.md#3050)

[Compare Source](https://redirect.github.com/jestjs/jest/compare/v30.4.1...v30.5.0)

##### Features

- `[@jest/expect-utils, jest-mock]` Add `mockFn.whenCalledWith(...args)` for configuring return values per argument list, with first-class asymmetric-matcher support ([#&#8203;16053](https://redirect.github.com/jestjs/jest/pull/16053))
- `[@jest/expect-utils]` Export `AsymmetricMatcher` and `FunctionParameters` types (previously private to `expect`) ([#&#8203;16053](https://redirect.github.com/jestjs/jest/pull/16053))
- `[jest-circus, jest-core, jest-jasmine2, jest-test-result, jest-types]` `--collectTests` now expands `test.each`/`describe.each` cases and reports per-status counts (skipped/todo via the new `wouldRun` flag for selected tests) plus a summary line that match a real run, including under `--testNamePattern` and `.only`/`fdescribe` focus on both the circus and jasmine2 runners ([#&#8203;16259](https://redirect.github.com/jestjs/jest/pull/16259))
- `[jest-circus, jest-environment, jest-runtime, jest-types]` Add describe-level retries via `jest.retryTimes(..., {entireDescribe: true})` ([#&#8203;16322](https://redirect.github.com/jestjs/jest/pull/16322))
- `[jest-circus, jest-message-util, jest-reporters, jest-types]` Add `retryMessages` to `AssertionResult` and export `formatErrorStack`, so the retry log renders nested `cause` and `AggregateError` sections with code frames instead of serialized `[cause]:`/`[errors]:` markers ([#&#8203;16316](https://redirect.github.com/jestjs/jest/pull/16316))
- `[jest-circus, jest-types]` Add `unhandledErrorsDetailed` to `Circus.RunResult`, so an unhandled rejection reports its `cause` chain and `AggregateError` entries with code frames instead of a pre-serialized stack ([#&#8203;16316](https://redirect.github.com/jestjs/jest/pull/16316))
- `[jest-haste-map]` Replace `NodeWatcher` and `FSEventsWatcher` with `@parcel/watcher` for the non-watchman watch path ([#&#8203;16188](https://redirect.github.com/jestjs/jest/pull/16188))
- `[jest-resolve]` Bump `unrs-resolver` to 1.12.1, remove `jest-pnp-resolver` and unnecessary checks ([#&#8203;15721](https://redirect.github.com/jestjs/jest/pull/15721))
- `[jest-resolve]` Honor Node's `--preserve-symlinks` / `NODE_PRESERVE_SYMLINKS` in the default resolver by passing `symlinks: false` to `unrs-resolver` ([#&#8203;16260](https://redirect.github.com/jestjs/jest/pull/16260))
- `[jest-runtime]` Apply automocking and manual `__mocks__` files to synchronously evaluable ESM graphs on Node 24.9+ - static imports, dynamic `import()` and `require()` of an ESM file now generate an automock from the real module's namespace instead of failing with "Attempting to import a mock without a factory". Graphs that need async evaluation (top-level await) or an async-only resolver or transformer still throw ([#&#8203;16391](https://redirect.github.com/jestjs/jest/pull/16391))
- `[jest-runtime]` Route `process.getBuiltinModule` through the sandbox, so it returns the sandbox `process` and the hooked `node:module` instead of the host's ([#&#8203;16391](https://redirect.github.com/jestjs/jest/pull/16391))
- `[jest-runtime]` Throw an actionable error from `module.register()` and `module.registerHooks()` inside a test - the hooks attached to the loader running Jest itself, never saw the sandboxed requires they were meant for, and stayed registered for every later test file in the worker ([#&#8203;16391](https://redirect.github.com/jestjs/jest/pull/16391))
- `[jest-runtime]` Surface resolution and import-attribute errors in an ESM graph before executing any of its CJS dependencies on Node 24.9+, matching Node's run-nothing-on-a-broken-graph behavior; the legacy loader on older versions keeps its linking-time execution order ([#&#8203;16391](https://redirect.github.com/jestjs/jest/pull/16391))
- `[jest-runtime]` Throw `ERR_SOURCE_PHASE_NOT_DEFINED` with an actionable message for `import source` and `import.source()`, instead of failing at instantiation with V8's bare "Source phase import object is not defined" ([#&#8203;16391](https://redirect.github.com/jestjs/jest/pull/16391))
- `[jest-runtime]` Emit the JSON-without-import-attribute deprecation warning once per test file instead of once per worker, so it is no longer silently swallowed for every file after the first ([#&#8203;16391](https://redirect.github.com/jestjs/jest/pull/16391))
- `[jest-runtime]` Set `import.meta.main` to `true` in the test file and `false` in every module it loads, matching Node 24+ ([#&#8203;16367](https://redirect.github.com/jestjs/jest/pull/16367))
- `[jest-runtime]` Resolve the `module-sync` export condition, so a package that exposes its ESM entry point for `require()` loads the same file Node would ([#&#8203;16336](https://redirect.github.com/jestjs/jest/pull/16336))
- `[jest-snapshot]` Add external snapshot paths to custom reporter failure details ([#&#8203;16374](https://redirect.github.com/jestjs/jest/pull/16374))

##### Fixes

- `[jest-console, jest-reporters]` `CustomConsole` now buffers console output so `TestResult.console` is populated for reporters when `verbose` is enabled, while `GitHubActionsReporter` avoids replaying buffered output in verbose mode ([#&#8203;16155](https://redirect.github.com/jestjs/jest/pull/16155))
- `[expect, jest-message-util, jest-pattern, jest-regex-util, jest-util]` Revert `node:` protocol imports to restore webpack/browser-bundle compatibility ([#&#8203;16167](https://redirect.github.com/jestjs/jest/pull/16167))
- `[expect]` Widen `toMatchObject` and `objectContaining` parameter type from `Record<string, unknown>` to `object` so class instances are accepted ([#&#8203;16196](https://redirect.github.com/jestjs/jest/pull/16196))
- `[jest-circus]` Call a generator test body with the shared test context, so `this` matches what a regular test function receives ([#&#8203;16347](https://redirect.github.com/jestjs/jest/pull/16347))
- `[jest-circus]` Capture the error listeners of the parent process instead of the in-sandbox `process`, so listeners registered before the test file survive teardown and sandbox listeners no longer leak onto the parent ([#&#8203;16347](https://redirect.github.com/jestjs/jest/pull/16347))
- `[jest-circus]` Clear `currentlyRunningTest` after skipped and todo tests ([#&#8203;16342](https://redirect.github.com/jestjs/jest/pull/16342))
- `[jest-circus]` Prevent late `done()` callbacks from affecting later test or hook invocations ([#&#8203;16343](https://redirect.github.com/jestjs/jest/pull/16343))
- `[jest-circus, jest-jasmine2]` Honor `--expand` when formatting `node:assert` failures, instead of always collapsing the diff ([#&#8203;16347](https://redirect.github.com/jestjs/jest/pull/16347))
- `[jest-circus, jest-jasmine2, jest-message-util]` Serialize the inner errors of an `AggregateError` into `failureMessages`, `retryReasons` and `unhandledErrors`, so `--json` output and reporter annotations include them ([#&#8203;16316](https://redirect.github.com/jestjs/jest/pull/16316))
- `[jest-circus, jest-snapshot]` Keep snapshot state and counts correct when a test retries ([#&#8203;16344](https://redirect.github.com/jestjs/jest/pull/16344))
- `[@jest/create-cache-key-function]` Include the caller support flags in the generated key, so a transformer that emits ESM or CJS based on them no longer shares one cache entry between the two ([#&#8203;16331](https://redirect.github.com/jestjs/jest/pull/16331))
- `[@jest/create-cache-key-function]` Include the stringified project config in the generated key, so editing a transformer's own settings invalidates what it cached ([#&#8203;16331](https://redirect.github.com/jestjs/jest/pull/16331))
- `[@jest/transform]` Include the caller support flags in a transform's cache key, so a file transformed both as ESM and as CJS no longer serves one shape's output for the other ([#&#8203;16331](https://redirect.github.com/jestjs/jest/pull/16331))
- `[jest-config]` Add missing `findRelatedTests`, `outputFile`, and `replname` entries to `ValidConfig` so they no longer trigger spurious "Unknown option" warnings ([#&#8203;16224](https://redirect.github.com/jestjs/jest/pull/16224))
- `[jest-config]` Use `--config` for the global config when multiple `--projects` are specified ([#&#8203;16273](https://redirect.github.com/jestjs/jest/pull/16273))
- `[jest-core]` Serialize `bigint` values in `--json` and `--outputFile` output as their literal form (`4n`), instead of failing the run with `TypeError: Do not know how to serialize a BigInt` ([#&#8203;16338](https://redirect.github.com/jestjs/jest/pull/16338))
- `[jest-core]` Do not report a `CustomGC` async resource (used by N-API addons such as napi-rs for per-isolate GC bookkeeping) as an open handle, since it is `napi_unref`'d by the addon and can never keep the event loop alive ([#&#8203;16379](https://redirect.github.com/jestjs/jest/pull/16379))
- `[jest-each]` Keep a `$&`, `` $` ``, `$'` or `$$` inside a `%p` param value out of the replacement, so the title shows the value instead of the text around it ([#&#8203;16338](https://redirect.github.com/jestjs/jest/pull/16338))
- `[jest-each]` Interpolate a `bigint` into a `%j` title as its literal form (`"4n"`) at any depth, instead of throwing `TypeError: Do not know how to serialize a BigInt` while collecting the tests ([#&#8203;16338](https://redirect.github.com/jestjs/jest/pull/16338))
- `[jest-environment, jest-runtime]` Bind `sandboxInjectedGlobals` to the right values when `injectGlobals` is `false`, instead of shifting every one of them by a position ([#&#8203;16377](https://redirect.github.com/jestjs/jest/pull/16377))
- `[jest-environment-node, jest-util]` Only warn about a conflicting `globalsCleanup` mode when one was explicitly configured, and follow the mode that is actually in effect ([#&#8203;16323](https://redirect.github.com/jestjs/jest/pull/16323))
- `[jest-environment-node, jest-util]` Stop resolving lazy globals when setting up an environment, so Node 26's builtin module globals are no longer loaded (and no longer emit their deprecation warnings) for every test file ([#&#8203;16324](https://redirect.github.com/jestjs/jest/pull/16324))
- `[jest-haste-map]` Keep watch mode alive when an outside process briefly makes a file unreadable on Windows, instead of tearing the watcher down on `EPERM` ([#&#8203;16295](https://redirect.github.com/jestjs/jest/pull/16295))
- `[jest-haste-map]` Keep indexing when an outside process holds a file open on Windows, instead of failing the whole crawl on `EPERM` ([#&#8203;16358](https://redirect.github.com/jestjs/jest/pull/16358))
- `[jest-haste-map]` Keep a duplicated manual mock resolving when the file it pointed at is deleted in watch mode ([#&#8203;16360](https://redirect.github.com/jestjs/jest/pull/16360))
- `[jest-haste-map]` Shut the worker farm down when a duplicate manual mock aborts the build under `throwOnModuleCollision` ([#&#8203;16354](https://redirect.github.com/jestjs/jest/pull/16354))
- `[jest-haste-map]` Attach the watchman client's `error` listener before the first command, so a watchman failure falls back to the node crawler instead of crashing on an unhandled `error` event, and always end the client ([#&#8203;16355](https://redirect.github.com/jestjs/jest/pull/16355))
- `[jest-haste-map]` Stop delivering watch events after `WatchmanWatcher` is closed, and route its warnings through the configured console ([#&#8203;16355](https://redirect.github.com/jestjs/jest/pull/16355))
- `[jest-haste-map]` Restore the nested `duplicates` index correctly in `ModuleMap.fromJSON`, so a haste collision reported inside a test worker raises `DuplicateHasteCandidatesError` instead of a `TypeError` ([#&#8203;16353](https://redirect.github.com/jestjs/jest/pull/16353))
- `[jest-haste-map]` Match watched files on a full extension, so `moduleFileExtensions: ['js']` no longer accepts `foo.mjs` ([#&#8203;16352](https://redirect.github.com/jestjs/jest/pull/16352))
- `[jest-haste-map]` Delimit the fields that make up the haste map cache key, so two different option sets cannot hash to the same cache file ([#&#8203;16352](https://redirect.github.com/jestjs/jest/pull/16352))
- `[jest-message-util]` Print the inner errors of an `AggregateError` thrown inside a test ([#&#8203;16316](https://redirect.github.com/jestjs/jest/pull/16316))
- `[jest-message-util]` Indent nested `cause` and `AggregateError` sections of a test failure by one level per depth, so the nesting is legible instead of rendering flat ([#&#8203;16316](https://redirect.github.com/jestjs/jest/pull/16316))
- `[jest-message-util]` Color stack traces line by line so blank lines stay blank ([#&#8203;16316](https://redirect.github.com/jestjs/jest/pull/16316))
- `[jest-message-util]` Detect Jest's own frames without assuming the checkout directory's name, and cover `@jest/*` packages, so stack traces and code frames point at user code ([#&#8203;16326](https://redirect.github.com/jestjs/jest/pull/16326))
- `[jest-mock]` `mockResolvedValue` / `mockRejectedValue` now see all overload return types, so a Promise-returning overload survives even when a later overload returns a non-Promise (e.g. `pg.Client['end']`) ([#&#8203;16237](https://redirect.github.com/jestjs/jest/pull/16237))
- `[@jest-environment/jsdom-abstract]` Make `@types/jsdom` a peer dependency ([#&#8203;16166](https://redirect.github.com/jestjs/jest/pull/16166))
- `[jest-mock]` Remove the leftover own accessor descriptor when restoring a `spyOn` of an inherited getter or setter, so the instance keeps reflecting the prototype ([#&#8203;16226](https://redirect.github.com/jestjs/jest/pull/16226))
- `[jest-resolve]` Include `extensionsToTreatAsEsm` in the `shouldLoadAsEsm` cache key, so projects with different extension lists don't read each other's answers ([#&#8203;16369](https://redirect.github.com/jestjs/jest/pull/16369))
- `[jest-resolve]` Make `getModuleIDAsync` build and cache `data:` URI module IDs the same way as `getModuleID` ([#&#8203;16370](https://redirect.github.com/jestjs/jest/pull/16370))
- `[jest-resolve]` Keep the `node:` prefix when resolving a core module asynchronously, so a builtin that only exists prefixed (`node:sea`, `node:sqlite`, `node:test`, `node:test/reporters`) resolves instead of failing as a missing bare package ([#&#8203;16388](https://redirect.github.com/jestjs/jest/pull/16388))
- `[jest-resolve]` Look up manual mocks for `node:` protocol specifiers under the unprefixed name they are stored as ([#&#8203;16388](https://redirect.github.com/jestjs/jest/pull/16388))
- `[jest-resolve]` Apply `moduleNameMapper` consistently to both spellings of core module specifiers (`fs` vs `node:fs`) ([#&#8203;16390](https://redirect.github.com/jestjs/jest/pull/16390))
- `[jest-resolve]` Keep virtual and ordinary mock module IDs isolated across test files ([#&#8203;16296](https://redirect.github.com/jestjs/jest/pull/16296))
- `[jest-resolve]` Guard missing `require.resolve.paths` ([#&#8203;16052](https://redirect.github.com/jestjs/jest/pull/16052))
- `[jest-resolve, jest-config, jest-runner]` Support a user resolver written as an ES module ([#&#8203;16332](https://redirect.github.com/jestjs/jest/pull/16332))
- `[jest-resolve, jest-runtime]` Throw the CJS parse error for ESM syntax in a `"type": "commonjs"` package or a `.cjs` file instead of loading it as ESM, matching Node ([#&#8203;16368](https://redirect.github.com/jestjs/jest/pull/16368))
- `[@jest/source-map]` Keep source map sources that name a scheme, such as `webpack:///`, instead of resolving them into a path that does not exist ([#&#8203;16327](https://redirect.github.com/jestjs/jest/pull/16327))
- `[@jest/source-map]` Look up `--testLocationInResults` positions at the right column, and keep a mapping to the first column instead of discarding it ([#&#8203;16327](https://redirect.github.com/jestjs/jest/pull/16327))
- `[@jest/source-map]` Warn when a source map cannot be parsed, instead of silently leaving its frames untranslated ([#&#8203;16327](https://redirect.github.com/jestjs/jest/pull/16327))
- `[jest-runner, @jest/source-map]` Keep a source-mapped stack for an error thrown after the test environment was torn down ([#&#8203;16327](https://redirect.github.com/jestjs/jest/pull/16327))
- `[jest-runtime, @jest/source-map]` Keep source maps past teardown and past the next test file's `install`, so a stack from a file no earlier stack mentioned still points at the original source ([#&#8203;16330](https://redirect.github.com/jestjs/jest/pull/16330))
- `[jest-runtime]` Report that no coverage was collected when `getAllV8CoverageInfoCopy` is called after `teardown`, instead of returning an empty result ([#&#8203;16385](https://redirect.github.com/jestjs/jest/pull/16385))
- `[jest-runtime]` Cache a CJS module's parsed exports before walking its re-exports, so two modules that re-export each other no longer overflow the stack when imported from ESM ([#&#8203;16363](https://redirect.github.com/jestjs/jest/pull/16363))
- `[jest-runtime]` Keep a re-exported ES module's parse failure from marking the re-exporting CommonJS file as ESM, so `module.exports = require('./dep.mjs')` loads instead of failing with `module is not defined` ([#&#8203;16363](https://redirect.github.com/jestjs/jest/pull/16363))
- `[jest-runtime]` Scope module mocks instantiated inside `jest.isolateModules`/`isolateModulesAsync` to that block, so a mock first imported there no longer outlives it - matching how CommonJS mocks already behave ([#&#8203;16365](https://redirect.github.com/jestjs/jest/pull/16365))
- `[jest-runtime]` Suspend module isolation while generating an automock, so loading the real module to read its shape no longer populates the isolated registry ([#&#8203;16365](https://redirect.github.com/jestjs/jest/pull/16365))
- `[jest-runtime]` Check a cached ES module's status before `require()` returns it, so a module whose evaluation threw rethrows that error and one left linked by a failed sibling is evaluated instead of returning uninitialized bindings ([#&#8203;16364](https://redirect.github.com/jestjs/jest/pull/16364))
- `[jest-runtime]` Report the original `ERR_REQUIRE_ASYNC_MODULE` when a `require()` of a top-level-await graph is retried, instead of a spurious "concurrent `import()`" error ([#&#8203;16364](https://redirect.github.com/jestjs/jest/pull/16364))
- `[jest-runtime]` Throw the evaluation error when another caller's `import()` of the same module failed while we awaited it, instead of resolving with the errored module ([#&#8203;16364](https://redirect.github.com/jestjs/jest/pull/16364))
- `[jest-runtime]` Mark the result of `require()`ing an ES module that has a default export with `__esModule: true` through a live-binding facade, and serve the same object from `require.cache`, matching Node ([#&#8203;16367](https://redirect.github.com/jestjs/jest/pull/16367))
- `[jest-runtime]` Provide a CommonJS module's exports under the `'module.exports'` named export when imported from ESM, matching Node 23+ ([#&#8203;16367](https://redirect.github.com/jestjs/jest/pull/16367))
- `[jest-runtime]` Give the test file itself a non-null `require.main` ([#&#8203;16367](https://redirect.github.com/jestjs/jest/pull/16367))
- `[jest-runtime]` Populate `module.children` with the modules a file loads, matching Node ([#&#8203;16368](https://redirect.github.com/jestjs/jest/pull/16368))
- `[jest-runtime]` Provide `import.meta.resolve` and `import.meta.jest` in `data:` URI modules, accept any-case mediatype parameters, and use Node's error codes for invalid `data:` URIs ([#&#8203;16368](https://redirect.github.com/jestjs/jest/pull/16368))
- `[jest-runtime]` Key ES modules by full URL, so query and fragment suffixes create the same module instances as Node and show up in `import.meta.url` ([#&#8203;16375](https://redirect.github.com/jestjs/jest/pull/16375))
- `[jest-runtime]` Share modules between overlapping graphs when a CommonJS module `require()`s an ES module mid-load, instead of evaluating shared dependencies twice ([#&#8203;16375](https://redirect.github.com/jestjs/jest/pull/16375))
- `[jest-runtime]` Throw `ERR_REQUIRE_CYCLE_MODULE` like Node when a CommonJS module `require()`s an ES module that is still being loaded, instead of evaluating the module a second time ([#&#8203;16366](https://redirect.github.com/jestjs/jest/pull/16366))
- `[jest-runtime]` Key builtin modules in the ESM registry by one canonical specifier ([#&#8203;16341](https://redirect.github.com/jestjs/jest/pull/16341))
- `[jest-runtime]` `import.meta.resolve()` for a builtin uses its `node:` specifier ([#&#8203;16341](https://redirect.github.com/jestjs/jest/pull/16341))
- `[jest-runtime]` Fall back to native ESM when a `.js` file contains ESM syntax but has no `"type":"module"` marker ([#&#8203;16152](https://redirect.github.com/jestjs/jest/pull/16152))
- `[jest-runtime]` Allow `require()` of ESM-marked files on Node < 24.9 via transform fallback ([#&#8203;16244](https://redirect.github.com/jestjs/jest/pull/16244))
- `[jest-runtime, @jest/transform]` Surface actionable `ERR_REQUIRE_ESM` error for files with untransformed ESM syntax instead of the generic "unexpected token" message ([#&#8203;16244](https://redirect.github.com/jestjs/jest/pull/16244))
- `[jest-runtime]` Support older test environments whose `moduleMocker` does not implement `clearMocksOnScope` ([#&#8203;16169](https://redirect.github.com/jestjs/jest/pull/16169))
- `[jest-runtime]` Apply `jest.unstable_mockModule` when the mocked file itself is `require()`d, not only when it is imported as a dependency ([#&#8203;16389](https://redirect.github.com/jestjs/jest/pull/16389))
- `[jest-runtime]` Apply `jest.unstable_mockModule` to statically imported `data:` URIs on Node 24.9+, matching dynamic `import()` ([#&#8203;16389](https://redirect.github.com/jestjs/jest/pull/16389))
- `[jest-runtime]` Run an async `jest.unstable_mockModule` factory once per module instead of twice, and fail the import instead of crashing the worker when the factory rejects ([#&#8203;16389](https://redirect.github.com/jestjs/jest/pull/16389))
- `[jest-runtime]` Hide a `require(esm)` module that failed to evaluate from `require.cache`, as Node does, instead of exposing a namespace with uninitialized bindings ([#&#8203;16389](https://redirect.github.com/jestjs/jest/pull/16389))
- `[jest-runtime]` Strip the byte-order mark when importing a JSON module, matching `require()` and Node ([#&#8203;16389](https://redirect.github.com/jestjs/jest/pull/16389))
- `[jest-runtime]` Throw `ERR_REQUIRE_ASYNC_MODULE` when `require(esm)` runs under an async-only custom resolver, instead of silently resolving with the default resolver ([#&#8203;16389](https://redirect.github.com/jestjs/jest/pull/16389))
- `[jest-runtime]` Parse imported JSON modules with the test realm's `JSON`, so their objects pass `instanceof Object` inside the test like `require()`d JSON does ([#&#8203;16389](https://redirect.github.com/jestjs/jest/pull/16389))
- `[jest-runtime]` Accept every `file:` URL string in the sandboxed `module.createRequire`, including one with a `localhost` authority, as Node does ([#&#8203;16389](https://redirect.github.com/jestjs/jest/pull/16389))
- `[jest-runtime]` Point at `{virtual: true}` when `jest.mock` or `jest.unstable_mockModule` is given a module that cannot be resolved ([#&#8203;16389](https://redirect.github.com/jestjs/jest/pull/16389))
- `[jest-reporters]` Fix coverage report table formatting in CI/GitHub Actions environments where `process.stdout.columns` is undefined by falling back to the `COLUMNS` env var or `80` columns in CI, preserving existing behaviour in other non-TTY environments ([#&#8203;16227](https://redirect.github.com/jestjs/jest/pull/16227))
- `[jest-runtime]` Support CJS-in-ESM exports via `"module.exports"` named exports ([#&#8203;16277](https://redirect.github.com/jestjs/jest/pull/16277))
- `[jest-snapshot]` Keep a skipped or failed test's hinted snapshots, instead of reporting them obsolete ([#&#8203;16348](https://redirect.github.com/jestjs/jest/pull/16348))
- `[jest-util]` Stop `globsToMatcher` reusing a cached matcher compiled with different picomatch options, and keep its `dot: true` default when `dot` is passed as `undefined` ([#&#8203;16381](https://redirect.github.com/jestjs/jest/pull/16381))
- `[pretty-format]` Move the `react-is` aliases into the `@jest` scope, so they cannot be shadowed by unrelated packages published under the alias names ([#&#8203;16333](https://redirect.github.com/jestjs/jest/pull/16333))

##### Chore & Maintenance

- `[docs]` Document the intentional divergences from Node's module system in the ECMAScript Modules page ([#&#8203;16368](https://redirect.github.com/jestjs/jest/pull/16368))
- `[docs]` Note deprecation of `react-test-renderer` in React Native tutorial and `pretty-format` README ([#&#8203;16294](https://redirect.github.com/jestjs/jest/pull/16294))
- `[docs]` Use `@testing-library/react-native` in the React Native tutorial instead of the deprecated `react-test-renderer` ([#&#8203;16318](https://redirect.github.com/jestjs/jest/pull/16318))
- `[babel-jest, @jest/transform]` Update `babel-plugin-istanbul` to v8 ([#&#8203;16049](https://redirect.github.com/jestjs/jest/pull/16049))
- `[jest-config, @jest/reporters, jest-runtime]` Update `glob` to v13 ([#&#8203;16397](https://redirect.github.com/jestjs/jest/pull/16397))
- `[jest-haste-map]` Refactor massive class into multiple files ([#&#8203;16180](https://redirect.github.com/jestjs/jest/pull/16180))
- `[jest-haste-map]` Drop `walker` dependency; replace hand-rolled directory recursion in the JS crawler and watcher startup with `fdir` ([#&#8203;16187](https://redirect.github.com/jestjs/jest/pull/16187))
- `[jest-haste-map]` Reuse cached metadata for files whose haste name is a known duplicate, instead of re-reading and re-parsing them on every startup ([#&#8203;16351](https://redirect.github.com/jestjs/jest/pull/16351))
- `[jest-haste-map]` Cache the watchman socket path and replace the `watchman --version` probe with `get-sockname`, so warm runs spawn no watchman processes ([#&#8203;16386](https://redirect.github.com/jestjs/jest/pull/16386))
- `[jest-resolve]` Store the per-directory package-type lookup in the cache it reads, so it actually memoizes ([#&#8203;16369](https://redirect.github.com/jestjs/jest/pull/16369))
- `[jest-resolve, jest-runtime]` Cut repeated work on the resolution hot path: hoist the platform-extension list to construction, memoize `isCoreModule` and the options cache-key serialization, skip mapper preparation when no `moduleNameMapper` is configured, run each mapper regex once, and stop re-parsing `NODE_OPTIONS` on every default-resolver call ([#&#8203;16371](https://redirect.github.com/jestjs/jest/pull/16371))
- `[jest-resolve]` Cut warm resolution cost to about a third: reuse one `unrs-resolver` factory per options shape instead of cloning per resolution, compose the factory cache key from per-array cached strings instead of serializing options, and stop constructing an `Error` for misses that `findNodeModule` swallows; add a `__benchmarks__` suite for the default resolver ([#&#8203;16373](https://redirect.github.com/jestjs/jest/pull/16373))
- `[jest-runner, @jest/source-map]` Replace `source-map-support` with an implementation in `@jest/source-map` ([#&#8203;16327](https://redirect.github.com/jestjs/jest/pull/16327))
- `[jest-snapshot]` Load babel, semver and synckit lazily, so requiring the package (which every test process does through `@jest/expect`) no longer loads \~200 modules that only writing inline snapshots needs ([#&#8203;16387](https://redirect.github.com/jestjs/jest/pull/16387))
- `[jest-runtime]` Reduce per-require overhead: skip module ID resolution when no mock can apply, answer core modules before probing for a manual mock, share one `require.cache` proxy across modules, and cache empty files ([#&#8203;16376](https://redirect.github.com/jestjs/jest/pull/16376))
- `[@jest/source-map]` Deprecate `getCallsite` in favour of `SourceMapSupport#getCallsite` ([#&#8203;16327](https://redirect.github.com/jestjs/jest/pull/16327))
- `[jest-runtime]` Avoid magical `null` value in ESM loader ([#&#8203;16160](https://redirect.github.com/jestjs/jest/pull/16160))

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC42MS4zIiwidXBkYXRlZEluVmVyIjoiNDQuNjEuMyIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2026-09-08 08:17:22 -07:00
renovate[bot] 8557eff8db chore(deps): update lint (#694)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [oxfmt](https://oxc.rs/docs/guide/usage/formatter) ([source](https://redirect.github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt)) | [`0.63.0` → `0.66.0`](https://renovatebot.com/diffs/npm/oxfmt/0.63.0/0.66.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/oxfmt/0.66.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/oxfmt/0.63.0/0.66.0?slim=true) |
| [oxlint](https://oxc.rs/docs/guide/usage/linter) ([source](https://redirect.github.com/oxc-project/oxc/tree/HEAD/npm/oxlint)) | [`1.78.0` → `1.81.0`](https://renovatebot.com/diffs/npm/oxlint/1.78.0/1.81.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/oxlint/1.81.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/oxlint/1.78.0/1.81.0?slim=true) |
| [oxlint-config-universe](https://redirect.github.com/expo/oxlint-config-universe) | [`0.0.3` → `0.2.0`](https://renovatebot.com/diffs/npm/oxlint-config-universe/0.0.3/0.2.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/oxlint-config-universe/0.2.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/oxlint-config-universe/0.0.3/0.2.0?slim=true) |

---

### Release Notes

<details>
<summary>oxc-project/oxc (oxfmt)</summary>

### [`v0.66.0`](https://redirect.github.com/oxc-project/oxc/compare/oxfmt_v0.65.0...oxfmt_v0.66.0)

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxfmt_v0.65.0...oxfmt_v0.66.0)

### [`v0.65.0`](https://redirect.github.com/oxc-project/oxc/compare/oxfmt_v0.64.0...oxfmt_v0.65.0)

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxfmt_v0.64.0...oxfmt_v0.65.0)

### [`v0.64.0`](https://redirect.github.com/oxc-project/oxc/blob/HEAD/npm/oxfmt/CHANGELOG.md#0640---2026-08-18)

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxfmt_v0.63.0...oxfmt_v0.64.0)

##### 🚀 Features

- [`c07fe7c`](https://redirect.github.com/oxc-project/oxc/commit/c07fe7c) oxfmt: Support `experimentalOperatorPosition` ([#&#8203;25643](https://redirect.github.com/oxc-project/oxc/issues/25643)) (leaysgur)

##### 📚 Documentation

- [`fed6681`](https://redirect.github.com/oxc-project/oxc/commit/fed6681) oxfmt: Skip expanding overrides options ([#&#8203;25572](https://redirect.github.com/oxc-project/oxc/issues/25572)) (leaysgur)

</details>

<details>
<summary>oxc-project/oxc (oxlint)</summary>

### [`v1.81.0`](https://redirect.github.com/oxc-project/oxc/blob/HEAD/npm/oxlint/CHANGELOG.md#1810---2026-08-31)

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxlint_v1.80.0...oxlint_v1.81.0)

##### 📚 Documentation

- [`d5be037`](https://redirect.github.com/oxc-project/oxc/commit/d5be037) linter/typescript/switch-exhaustiveness-check: Clarify default case comment pattern ([#&#8203;26100](https://redirect.github.com/oxc-project/oxc/issues/26100)) (camc314)

### [`v1.80.0`](https://redirect.github.com/oxc-project/oxc/compare/oxlint_v1.79.0...oxlint_v1.80.0)

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxlint_v1.79.0...oxlint_v1.80.0)

### [`v1.79.0`](https://redirect.github.com/oxc-project/oxc/blob/HEAD/npm/oxlint/CHANGELOG.md#1790---2026-08-18)

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxlint_v1.78.0...oxlint_v1.79.0)

##### 💥 BREAKING CHANGES

- [`8c4552d`](https://redirect.github.com/oxc-project/oxc/commit/8c4552d) linter: \[**BREAKING**] Split react/react-compiler into per-category rules ([#&#8203;25500](https://redirect.github.com/oxc-project/oxc/issues/25500)) (Boshen)

##### 🐛 Bug Fixes

- [`228e8e0`](https://redirect.github.com/oxc-project/oxc/commit/228e8e0) linter: Resolve inactive React compiler rules ([#&#8203;25830](https://redirect.github.com/oxc-project/oxc/issues/25830)) (Boshen)
- [`aa49d86`](https://redirect.github.com/oxc-project/oxc/commit/aa49d86) linter: Allow spread rule options in config types ([#&#8203;25675](https://redirect.github.com/oxc-project/oxc/issues/25675)) (ch3rry)
- [`36f8451`](https://redirect.github.com/oxc-project/oxc/commit/36f8451) linter/eslint/no-eval: Align indirect default with ESLint ([#&#8203;25656](https://redirect.github.com/oxc-project/oxc/issues/25656)) (camc314)
- [`beb724d`](https://redirect.github.com/oxc-project/oxc/commit/beb724d) linter/eslint/no-unused-vars: Report bare underscore parameters ([#&#8203;25663](https://redirect.github.com/oxc-project/oxc/issues/25663)) (camc314)
- [`4004c10`](https://redirect.github.com/oxc-project/oxc/commit/4004c10) linter/eslint/no-irregular-whitespace: Check comments by default ([#&#8203;25660](https://redirect.github.com/oxc-project/oxc/issues/25660)) (camc314)
- [`285820e`](https://redirect.github.com/oxc-project/oxc/commit/285820e) linter/no-large-snapshots: Precompile and document allowed snapshot matchers ([#&#8203;25611](https://redirect.github.com/oxc-project/oxc/issues/25611)) (Mikhail Baev)
- [`4df5835`](https://redirect.github.com/oxc-project/oxc/commit/4df5835) linter: Allow capitalized built-in calls ([#&#8203;25516](https://redirect.github.com/oxc-project/oxc/issues/25516)) (Boshen)

</details>

<details>
<summary>expo/oxlint-config-universe (oxlint-config-universe)</summary>

### [`v0.2.0`](https://redirect.github.com/expo/oxlint-config-universe/releases/tag/v0.2.0)

[Compare Source](https://redirect.github.com/expo/oxlint-config-universe/compare/a082d4f808eaea495b5f62b5bad4342d69de7442...v0.2.0)

#### What's Changed

- fix: fill in some documented gaps with rules that exist by [@&#8203;wschurman](https://redirect.github.com/wschurman) in [#&#8203;1](https://redirect.github.com/expo/oxlint-config-universe/pull/1)
- chore: explicitly enable some rules that are on by default by [@&#8203;wschurman](https://redirect.github.com/wschurman) in [#&#8203;2](https://redirect.github.com/expo/oxlint-config-universe/pull/2)
- fix: re-enable no-control-regex by [@&#8203;wschurman](https://redirect.github.com/wschurman) in [#&#8203;3](https://redirect.github.com/expo/oxlint-config-universe/pull/3)
- chore: explicitly enable more rules that are on by default by [@&#8203;wschurman](https://redirect.github.com/wschurman) in [#&#8203;4](https://redirect.github.com/expo/oxlint-config-universe/pull/4)
- chore: bump version, remove manual changelog for now by [@&#8203;wschurman](https://redirect.github.com/wschurman) in [#&#8203;5](https://redirect.github.com/expo/oxlint-config-universe/pull/5)
- fix: support oxlint 1.79 by [@&#8203;hirbod](https://redirect.github.com/hirbod) in [#&#8203;6](https://redirect.github.com/expo/oxlint-config-universe/pull/6)
- chore: modernize tooling — Yarn 4, generated types, npm trusted publishing by [@&#8203;hirbod](https://redirect.github.com/hirbod) in [#&#8203;7](https://redirect.github.com/expo/oxlint-config-universe/pull/7)
- fix: push version commit with bot PAT in publish workflow by [@&#8203;hirbod](https://redirect.github.com/hirbod) in [#&#8203;8](https://redirect.github.com/expo/oxlint-config-universe/pull/8)
- chore: drop production environment from publish workflow by [@&#8203;hirbod](https://redirect.github.com/hirbod) in [#&#8203;9](https://redirect.github.com/expo/oxlint-config-universe/pull/9)

#### New Contributors

- [@&#8203;wschurman](https://redirect.github.com/wschurman) made their first contribution in [#&#8203;1](https://redirect.github.com/expo/oxlint-config-universe/pull/1)
- [@&#8203;hirbod](https://redirect.github.com/hirbod) made their first contribution in [#&#8203;6](https://redirect.github.com/expo/oxlint-config-universe/pull/6)

**Full Changelog**: <https://github.com/expo/oxlint-config-universe/commits/v0.2.0>

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC42MS4zIiwidXBkYXRlZEluVmVyIjoiNDQuNjEuMyIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2026-09-08 08:17:12 -07:00
renovate[bot] ba3ba9ea89 chore(deps): update dependency jdx/mise to v2026.9.2 (#692)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [jdx/mise](https://redirect.github.com/jdx/mise) | minor | `v2026.8.15` → `v2026.9.2` |

---

### Release Notes

<details>
<summary>jdx/mise (jdx/mise)</summary>

### [`v2026.9.2`](https://redirect.github.com/jdx/mise/releases/tag/v2026.9.2): : Packslip Backend, SSH Relay, and Reimagined Install Progress

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.9.1...vfox-v2026.9.2)

This is a large release headlined by the new stable `packslip:` backend for installing tools from a vendor's own signed release manifest, a read-only GitHub SSH relay for remote onboarding, redesigned install progress for both terminals and CI logs, and a rebuilt dotfiles-tracking model for bootstrap. It also carries dozens of fixes across shims, tasks, brew, Go, npm, and the schema, plus two security fixes.

##### Highlights

- The `packslip:` backend is now generally available: install tools directly from a project's cryptographically signed release manifest, with signer pinning, trusted stampers, host-requirement checks, shell completions, and agent skills all driven from the same signed source.
- Install output was reimagined end to end. Interactive terminals get a live region with a fractional install-wide bar and permanent completion lines, while CI logs, pipes, and AI-agent terminals get a compact append-only reporter instead of thousands of scrolling rows.
- Bootstrap grows a read-only GitHub SSH relay and `--from-git` onboarding, an AUR package manager, and a rebuilt dotfiles-history model that tracks files through ordinary Git commits synchronized with an origin.

##### Added

- **packslip:** New `packslip:` backend installs tools from a vendor-published, signed release manifest (a sigstore bundle) that names every artifact with its digest, platform, format, and executables. The tool name is a pin, like a `known_hosts` entry: `packslip:github.com/owner/repo` accepts only a packslip signed by that repository's release workflow, and signature, log entry, statement, digest, and size are all verified before anything is unpacked. Custom hosts and monorepo tools are supported. The backend is no longer experimental. ([#&#8203;12778](https://redirect.github.com/jdx/mise/pull/12778), [#&#8203;12811](https://redirect.github.com/jdx/mise/pull/12811) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [tools]
  "packslip:github.com/oxc-project/oxc/oxlint" = "latest"
  ```
- **packslip:** Signer pinning remembers, per project, the signer that signed the first accepted release in a `pins.toml` file (like SSH's known\_hosts) and records the signer in `mise.lock`, so a later release signed by anyone else, or one that weakens provenance, is refused. `mise packslip pins` lists what is pinned and `mise packslip forget <project>` accepts an announced key rotation. ([#&#8203;12783](https://redirect.github.com/jdx/mise/pull/12783) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **packslip:** New `packslip.stampers` setting lets you require that a trusted host has stamped a version before mise offers or installs it (a scanning service, a mirror, or eventually the registry), with a per-tool `trust = "vendor"` override. ([#&#8203;12782](https://redirect.github.com/jdx/mise/pull/12782) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **packslip:** Declared host requirements (OS/glibc minimums, shared libraries, required commands) are checked before download; confirmed failures refuse the install, gaps mise cannot verify warn only, and `ignore_requirements = true` overrides hard failures. `latest` also resolves from verified vendor recommendations. ([#&#8203;12804](https://redirect.github.com/jdx/mise/pull/12804), [#&#8203;12805](https://redirect.github.com/jdx/mise/pull/12805) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **packslip:** A tool's packslip can ship shell completions and agent skills. `mise completion <shell> --tool <name>` prints (or `--install` writes a version-aware stub for) completions for whichever version is active in the current directory, and `mise skills ls` / `mise skills sync` link a tool's Agent Skills into `.claude/skills` at the pinned version. Completions are also loaded automatically in activated shells. ([#&#8203;12779](https://redirect.github.com/jdx/mise/pull/12779), [#&#8203;12780](https://redirect.github.com/jdx/mise/pull/12780), [#&#8203;12848](https://redirect.github.com/jdx/mise/pull/12848) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **install:** Redesigned install progress. Interactive terminals now show a live region with an install-wide fractional bar, per-tool phase/elapsed/artifact rows, dependency-wait rows ("waiting for node\@&#8203;24.20.0"), and permanent completion lines written into scrollback. `prune`, `uninstall`, and upgrade removals reuse the same session, so pruning hundreds of versions no longer floods the screen. ([#&#8203;12906](https://redirect.github.com/jdx/mise/pull/12906) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **install:** CI logs, pipes, and AI-agent terminals (`CLAUDECODE`/`AI_AGENT`) get a compact append-only reporter: one permanent line per finished tool plus a periodic snapshot of the bar, active phases, transfer rates, and queue count, instead of hundreds of scrolling status rows. Resolver hosts and retry progress are surfaced too. ([#&#8203;12902](https://redirect.github.com/jdx/mise/pull/12902), [#&#8203;12907](https://redirect.github.com/jdx/mise/pull/12907) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **ssh:** New `mise ssh` runs ordinary OpenSSH sessions with optional session-scoped, read-only GitHub access, and `mise bootstrap remote --from-git` onboards a remote host from a Git repo. The initiating machine keeps the credential and brokers authorized clone/fetch and REST reads over an SSH Unix-socket forward; no token is installed on the target. ([#&#8203;12830](https://redirect.github.com/jdx/mise/pull/12830) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```sh
  mise ssh devbox --github-relay-read-only --github-relay-repo jdx/dotfiles
  ```
- **bootstrap:** `mise bootstrap --from-git <GIT_URL>` clones a repository-backed global config into `$MISE_CONFIG_DIR` so its `config.toml`, `conf.d/`, and tasks apply on the first bootstrap and stay active afterward. ([#&#8203;12715](https://redirect.github.com/jdx/mise/pull/12715) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** New `aur:` package manager installs from the Arch User Repository via `yay` (preferred) or `paru`, with foreign-package status detection so repo packages with colliding names cannot satisfy an AUR request. Bootstrap can also declare packages as absent to remove them, and now reports which root declarations were selected. ([#&#8203;12718](https://redirect.github.com/jdx/mise/pull/12718), [#&#8203;12716](https://redirect.github.com/jdx/mise/pull/12716), [#&#8203;12770](https://redirect.github.com/jdx/mise/pull/12770) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** Rebuilt dotfiles tracking. Files enrolled with `mode = "track"` stay in place while mise commits changes to a separate bare Git store with checkpoints, rollback/undo, and optional origin synchronization, including encryption of shared files before storage. ([#&#8203;12918](https://redirect.github.com/jdx/mise/pull/12918) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **activate:** New `activate_shims = false` (`MISE_ACTIVATE_SHIMS=false`) keeps tool shim directories off PATH during activation and hooks without changing your auto-install or lazy-tool settings. Command wrappers such as mr-boxington's `cargo` keep working. ([#&#8203;12926](https://redirect.github.com/jdx/mise/pull/12926) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **core:** Rust tools accept `mr_boxington = true` and `mise use` gains a repeatable `--tool-option KEY=VALUE` flag, so setting up the Cargo wrapper is a single command. ([#&#8203;12908](https://redirect.github.com/jdx/mise/pull/12908) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```sh
  mise use --tool-option mr_boxington=true rust mr-boxington
  ```
- **ruby:** mise now reads Bundler's `ruby file: ".ruby-version"` form from a `Gemfile` (resolving the path next to the Gemfile), so projects that pin Ruby through a sibling file resolve correctly. ([#&#8203;12914](https://redirect.github.com/jdx/mise/pull/12914) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **self-update:** New global-only `self_update.repository` and `self_update.api_url` settings let organizations point manual updates, out-of-date hints, and automatic updates at a curated GitHub or GitHub Enterprise release mirror. Project config cannot redirect updates, and artifacts still pass the embedded-signature check. ([#&#8203;12735](https://redirect.github.com/jdx/mise/pull/12735) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **backend:** `install_env` is now applied when resolving and downloading tools, not just at build time. ([#&#8203;12777](https://redirect.github.com/jdx/mise/pull/12777) by [@&#8203;rabadin](https://redirect.github.com/rabadin))
- **brew:** mise can now evaluate ordinary third-party taps. ([#&#8203;12774](https://redirect.github.com/jdx/mise/pull/12774) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Fixed

- **shim:** Lazy tool names now match on Windows, and `mise-shim.exe` resolves correctly through a symlinked `mise.exe`. ([#&#8203;12699](https://redirect.github.com/jdx/mise/pull/12699) by [@&#8203;jdx](https://redirect.github.com/jdx), [#&#8203;12915](https://redirect.github.com/jdx/mise/pull/12915) by [@&#8203;acooler15](https://redirect.github.com/acooler15))
- **activate:** Paths are translated for the calling Windows POSIX shell, and PowerShell pipeline input is now forwarded. ([#&#8203;12736](https://redirect.github.com/jdx/mise/pull/12736), [#&#8203;12925](https://redirect.github.com/jdx/mise/pull/12925) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **nushell:** The activation prelude preserves the PATH list type. ([#&#8203;12754](https://redirect.github.com/jdx/mise/pull/12754) by [@&#8203;Junaid-PK](https://redirect.github.com/Junaid-PK))
- **task:** Task globs no longer loop on symlinks, `sources` accepts a single string, task status is preserved when the cache audit tracer fails, and POSIX shells no longer have PATH pre-converted (the shell already does it). ([#&#8203;12711](https://redirect.github.com/jdx/mise/pull/12711), [#&#8203;12769](https://redirect.github.com/jdx/mise/pull/12769) by [@&#8203;jdx](https://redirect.github.com/jdx), [#&#8203;12530](https://redirect.github.com/jdx/mise/pull/12530) by [@&#8203;risu729](https://redirect.github.com/risu729), [#&#8203;12696](https://redirect.github.com/jdx/mise/pull/12696) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **npm:** Global installs now work with pnpm 12. ([#&#8203;12717](https://redirect.github.com/jdx/mise/pull/12717) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **go:** Private modules resolve via Go, and loong64 uses the correct artifact architecture. ([#&#8203;12731](https://redirect.github.com/jdx/mise/pull/12731) by [@&#8203;kejne](https://redirect.github.com/kejne), [#&#8203;12767](https://redirect.github.com/jdx/mise/pull/12767) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **brew:** A tapped install now explains why it found no API metadata, and all tap formula layouts are supported. brew-cask packages with `auto_updates` enabled are upgraded correctly. ([#&#8203;12645](https://redirect.github.com/jdx/mise/pull/12645) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743), [#&#8203;12837](https://redirect.github.com/jdx/mise/pull/12837) by [@&#8203;soodoh](https://redirect.github.com/soodoh), [#&#8203;12857](https://redirect.github.com/jdx/mise/pull/12857) by [@&#8203;himkt](https://redirect.github.com/himkt))
- **github:** Releases with no assets are omitted from the version list, and OAuth-refresh warnings are silenced during env export and shown only once when a refresh is rejected. ([#&#8203;12572](https://redirect.github.com/jdx/mise/pull/12572) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743), [#&#8203;12855](https://redirect.github.com/jdx/mise/pull/12855), [#&#8203;12911](https://redirect.github.com/jdx/mise/pull/12911) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **config:** Every newly added `.tool-versions` line is now terminated with a newline. ([#&#8203;12740](https://redirect.github.com/jdx/mise/pull/12740) by [@&#8203;dylanpulver](https://redirect.github.com/dylanpulver))
- **env:** Bootstrap shims for lazy tools are created when printing the env, and duplicate unset directives are removed from hook output. ([#&#8203;12726](https://redirect.github.com/jdx/mise/pull/12726) by [@&#8203;hktitof](https://redirect.github.com/hktitof), [#&#8203;12707](https://redirect.github.com/jdx/mise/pull/12707) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **http:** A failed download's partial file is flushed before giving up, and cache compression finishes before files are published. ([#&#8203;12823](https://redirect.github.com/jdx/mise/pull/12823) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743), [#&#8203;12894](https://redirect.github.com/jdx/mise/pull/12894) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** Tera templates render in hooks, tildes expand in managed file sources, and the relative config dir is pinned during reexec. ([#&#8203;12727](https://redirect.github.com/jdx/mise/pull/12727), [#&#8203;12768](https://redirect.github.com/jdx/mise/pull/12768), [#&#8203;12721](https://redirect.github.com/jdx/mise/pull/12721) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **backend:** Disabled plugins are excluded from shorthand resolution, and installed plugins are included in plugin completion. ([#&#8203;12927](https://redirect.github.com/jdx/mise/pull/12927), [#&#8203;12720](https://redirect.github.com/jdx/mise/pull/12720) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **rust:** Incomplete rustup toolchains are reconciled. ([#&#8203;12771](https://redirect.github.com/jdx/mise/pull/12771) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **oci/vfox/aqua:** OCI reused tool layers preserve their paths, vfox preserves the tool name from legacy lockfiles, and aqua handles repository transfers in attestations. ([#&#8203;12749](https://redirect.github.com/jdx/mise/pull/12749), [#&#8203;12745](https://redirect.github.com/jdx/mise/pull/12745), [#&#8203;12766](https://redirect.github.com/jdx/mise/pull/12766) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **sandbox:** `SYS_fork`/`SYS_vfork` are omitted on aarch64. ([#&#8203;12807](https://redirect.github.com/jdx/mise/pull/12807) by [@&#8203;jamescassell](https://redirect.github.com/jamescassell))
- **dotfiles:** A stale directory the deeper walk already removed is tolerated. ([#&#8203;12697](https://redirect.github.com/jdx/mise/pull/12697) by [@&#8203;vladbisceanu](https://redirect.github.com/vladbisceanu))
- **schema:** Absent bootstrap packages, inline dotfile content, and missing built-in deps providers are now modeled. ([#&#8203;12785](https://redirect.github.com/jdx/mise/pull/12785) by [@&#8203;nettlesh](https://redirect.github.com/nettlesh), [#&#8203;12738](https://redirect.github.com/jdx/mise/pull/12738), [#&#8203;12739](https://redirect.github.com/jdx/mise/pull/12739) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **lockfile:** The deferred provenance baseline is dropped once auto-lock verifies the upgrade. ([#&#8203;12825](https://redirect.github.com/jdx/mise/pull/12825) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Security

- **http:** URL replacements no longer forward authorization headers or URL credentials when an HTTPS origin is rewritten to HTTP; such credential-carrying downgrades are refused, and errors omit URLs so embedded credentials cannot leak through diagnostics. ([#&#8203;12879](https://redirect.github.com/jdx/mise/pull/12879) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **self-update:** Self-update now requires secure release sources. ([#&#8203;12737](https://redirect.github.com/jdx/mise/pull/12737) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### New Contributors

- [@&#8203;acooler15](https://redirect.github.com/acooler15) made their first contribution in [#&#8203;12915](https://redirect.github.com/jdx/mise/pull/12915)
- [@&#8203;soodoh](https://redirect.github.com/soodoh) made their first contribution in [#&#8203;12837](https://redirect.github.com/jdx/mise/pull/12837)
- [@&#8203;jamescassell](https://redirect.github.com/jamescassell) made their first contribution in [#&#8203;12807](https://redirect.github.com/jdx/mise/pull/12807)
- [@&#8203;nettlesh](https://redirect.github.com/nettlesh) made their first contribution in [#&#8203;12785](https://redirect.github.com/jdx/mise/pull/12785)
- [@&#8203;Junaid-PK](https://redirect.github.com/Junaid-PK) made their first contribution in [#&#8203;12754](https://redirect.github.com/jdx/mise/pull/12754)
- [@&#8203;kejne](https://redirect.github.com/kejne) made their first contribution in [#&#8203;12731](https://redirect.github.com/jdx/mise/pull/12731)
- [@&#8203;dylanpulver](https://redirect.github.com/dylanpulver) made their first contribution in [#&#8203;12740](https://redirect.github.com/jdx/mise/pull/12740)
- [@&#8203;Svector-anu](https://redirect.github.com/Svector-anu) made their first contribution in [#&#8203;12714](https://redirect.github.com/jdx/mise/pull/12714)
- [@&#8203;vladbisceanu](https://redirect.github.com/vladbisceanu) made their first contribution in [#&#8203;12697](https://redirect.github.com/jdx/mise/pull/12697)

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.9.1...v2026.9.2>

##### 💚 Sponsor mise

mise is built and maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer at [**entire.io**](https://entire.io/), the title sponsor of his open source work.

If mise saves you or your team time, please consider becoming an [individual or company sponsor](https://jdx.dev/sponsors.html). Your support funds ongoing development and helps keep mise fast, free, and independent.

### [`v2026.9.1`](https://redirect.github.com/jdx/mise/releases/tag/v2026.9.1): : Bootstrap firewall limiting, scoped locked mode, and lazy tools in tasks

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.9.0...vfox-v2026.9.1)

This release expands bootstrap system setup with firewall rate limiting and more systemd directives, adds a scoped locked mode for mixing distribution and user tool policies, and fixes lazy tools so they install when invoked from tasks and `mise x`.

#### Added

- **bootstrap:** New `action = "limit"` for incoming TCP firewall rules rate-limits connections per source. It maps to UFW's native limiting and bounded per-source IPv4/IPv6 nftables meters; firewalld fails closed since it cannot express safe per-source limiting. Limit rules are treated as preserving SSH access by the lockout guard. ([#&#8203;12669](https://redirect.github.com/jdx/mise/pull/12669) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** Bootstrap systemd units now support `requires` (`Requires=`), repeatable `environment_file` (`EnvironmentFile=`), `nice`, and `umask`, with validation for nice ranges (-20 to 19) and octal umasks. Note that `requires` does not imply ordering; use `after` for that. ([#&#8203;12683](https://redirect.github.com/jdx/mise/pull/12683) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **config:** New global-only `locked_scopes` setting (also `MISE_LOCKED_SCOPES`) lets you scope locked mode to specific config sources, so a distribution can ship rolling or lazy tools through `/etc/mise/config.toml` without forcing users to maintain a system lockfile. Defaults to all three scopes to preserve existing behavior. ([#&#8203;12667](https://redirect.github.com/jdx/mise/pull/12667) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [settings]
  locked_scopes = ["project", "global"]
  ```

#### Fixed

- **task:** Lazy tools (`lazy = true`) now install when their command is invoked from a `mise run` task or `mise x`, matching the behavior of an activated shell. mise inserts the shim farms after real tool paths for lazy toolsets and reconciles missing bootstrap shims for hand-edited declarations, so tasks no longer fail with `command: not found`. ([#&#8203;12687](https://redirect.github.com/jdx/mise/pull/12687) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **env:** Environment variables configured for removal (`false` values and unset directives) are now consistently unset across `mise exec`, tasks, `mise env`, hook-env, deps, and tool stubs, so inherited or secret variables are no longer leaked back into child processes. ([#&#8203;12664](https://redirect.github.com/jdx/mise/pull/12664) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **shim:** `mise reshim` can now safely target shared executable directories like `~/.local/bin` or `/usr/local/bin`. It identifies mise-owned shims and only replaces or prunes those, leaving unmanaged files and symlinks in place, and `--force` rebuilds mise-owned shims rather than wiping the whole directory. ([#&#8203;12675](https://redirect.github.com/jdx/mise/pull/12675) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **brew-cask:** Cask binaries that reference their surrounding extracted payload now keep that payload beside the staged binary, so tools like `codex` that exec helpers or read manifests from their own tree continue working after temporary install files are cleaned up. ([#&#8203;12686](https://redirect.github.com/jdx/mise/pull/12686) by [@&#8203;azohra](https://redirect.github.com/azohra))
- **upgrade:** Fixed recursive deferred tool pruning that could hang new terminals when prompt hooks triggered a full tracked-config scan. Deferred cleanup now takes a non-blocking lock and is skipped for shell-integration and `--no-config` commands. ([#&#8203;12676](https://redirect.github.com/jdx/mise/pull/12676) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **install:** Install failures caused by a missing marker file now name the affected file and operation instead of reporting a bare OS error. ([#&#8203;12624](https://redirect.github.com/jdx/mise/pull/12624) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **install:** `mise install --dry-run` for an s3 tool that only declares a URL for another platform now reports the missing platform and available options instead of falsely claiming it would install. ([#&#8203;12641](https://redirect.github.com/jdx/mise/pull/12641) by [@&#8203;hktitof](https://redirect.github.com/hktitof))

#### Changed

- **registry:** Registry command (shim) names are now inferred from the preferred Aqua package at build time, removing redundant `bins` declarations from hundreds of registry entries. Explicit `bins` are kept only where mise intentionally exposes a different command set, and `mise registry --json` exposes the inferred names. ([#&#8203;12668](https://redirect.github.com/jdx/mise/pull/12668) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### New Contributors

- [@&#8203;azohra](https://redirect.github.com/azohra) made their first contribution in [#&#8203;12686](https://redirect.github.com/jdx/mise/pull/12686)
- [@&#8203;hktitof](https://redirect.github.com/hktitof) made their first contribution in [#&#8203;12641](https://redirect.github.com/jdx/mise/pull/12641)

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.9.0...v2026.9.1>

#### 💚 Sponsor mise

mise is built and maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer at [**entire.io**](https://entire.io/), the title sponsor of his open source work.

If mise saves you or your team time, please consider becoming an [individual or company sponsor](https://jdx.dev/sponsors.html). Your support funds ongoing development and helps keep mise fast, free, and independent.

### [`v2026.9.0`](https://redirect.github.com/jdx/mise/releases/tag/v2026.9.0): : Lazy Tool Shims

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.8.16...vfox-v2026.9.0)

This release introduces lazy tool shims, which install a tool automatically the first time one of its commands is invoked, plus an opt-in Erlang precompiled OS override and a fix for nested `mise` calls during postinstall hooks.

#### Added

- **tools:** Declare a tool as lazy in `[tools]` with `lazy = true` and mise generates bootstrap shims into its normal user/system shim farms. The provider is installed only when one of its commands is first called, then executes immediately; subsequent calls run the real binary without another mise dispatch. Registry tools derive their command names from registry `bins`, while explicit or non-registry backends declare them with `lazy_bins`. A bare `mise install` skips lazy declarations; use `mise install --include-lazy` to provision them all. New global path settings (`shims_dir`/`MISE_SHIMS_DIR`, `system_installs_dir`/`MISE_SYSTEM_INSTALLS_DIR`, `system_shims_dir`/`MISE_SYSTEM_SHIMS_DIR`) and `mise reshim --system` support system-scoped and collocated layouts. ([#&#8203;12594](https://redirect.github.com/jdx/mise/pull/12594) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [tools]
  node = { version = "24", lazy = true }
  "github:example/acme" = { version = "1.2.3", lazy = true, lazy_bins = ["acme", "acmectl"] }
  ```
- **erlang:** New `erlang.precompiled_os` setting (and `MISE_ERLANG_PRECOMPILED_OS`) opts into a specific Bob Ubuntu build target—including Ubuntu 26.04—on compatible glibc distributions. Automatic target detection is unchanged, and lockfiles now preserve the selected OS for reproducible builds. Note the portability constraints around shared-library SONAMEs documented with the setting. ([#&#8203;12637](https://redirect.github.com/jdx/mise/pull/12637) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Fixed

- **install:** Postinstall hooks now activate the exact tool version being installed, so nested `mise` invocations (such as `mise which` or `mise ls`) resolve that version instead of an older discoverable one—or losing the tool entirely when it was declared through a custom config path with `mise use --path`. ([#&#8203;12635](https://redirect.github.com/jdx/mise/pull/12635) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Registry

- Added `engram` ([#&#8203;12480](https://redirect.github.com/jdx/mise/pull/12480) by [@&#8203;AndryOre](https://redirect.github.com/AndryOre))
- Added `gentle-ai` ([aqua:Gentleman-Programming/gentle-ai](https://redirect.github.com/Gentleman-Programming/gentle-ai)) ([#&#8203;12444](https://redirect.github.com/jdx/mise/pull/12444) by [@&#8203;AndryOre](https://redirect.github.com/AndryOre))

#### New Contributors

- [@&#8203;AndryOre](https://redirect.github.com/AndryOre) made their first contribution in [#&#8203;12444](https://redirect.github.com/jdx/mise/pull/12444)

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.8.16...v2026.9.0>

#### 💚 Sponsor mise

mise is maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer for [**entire.io**](https://entire.io), the title sponsor of the [jdx.dev](https://jdx.dev) open source tools. Development is funded by sponsors.

If mise saves you or your team time, please consider sponsoring at [jdx.dev](https://jdx.dev/sponsors.html). Individual and company sponsorships keep mise fast, free, and independent.

### [`v2026.8.16`](https://redirect.github.com/jdx/mise/releases/tag/v2026.8.16): : Command wrappers and Windows shell fixes

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.8.15...vfox-v2026.8.16)

This release adds configurable command wrappers for intercepting ordinary commands, alongside a batch of shell and PATH fixes that make Windows activation more reliable and clearer upgrade and error messaging.

#### Added

- **shims:** New `[wrappers]` config lets you intercept an ordinary command name with a different command, arguments, and environment. Wrappers take precedence over mise-managed tools, and mise strips its dispatch directories before delegating so the underlying tool still resolves from mise or the system. Works with normal activation, `activate --shims`, and `mise exec`; managed wrapper shims are refreshed by `mise reshim`. ([#&#8203;12617](https://redirect.github.com/jdx/mise/pull/12617) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [tools]
  mr-boxington = "1.1.0"

  [wrappers.cargo]
  command = "mbx"
  env = { MBX_CARGO_SHIM_MODE = "1" }
  ```

#### Fixed

- **elvish:** `mise env` and `mise activate --shims` now emit valid Elvish quoting instead of bash's. This fixes several corruptions that mostly surfaced on Windows: stray quotes and the wrong PATH separator in `activate --shims`, apostrophes and `!` arriving with backslashes, and `\n` sequences in paths like `C:\nodejs` being turned into newlines. ([#&#8203;12584](https://redirect.github.com/jdx/mise/pull/12584) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **fish:** `mise env -s fish` now splits PATH on the host's separator and recognizes the `Path` spelling on Windows, so semicolon-separated Windows paths are no longer severed at each drive letter. Empty PATH segments are dropped rather than adding the current directory. ([#&#8203;12582](https://redirect.github.com/jdx/mise/pull/12582) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **http:** The "No URL for platform" error now lists every platform key a tool actually declares in `mise.toml`, including typos and unrecognized names, instead of falling back to a misleading "requires 'url' option" message with an empty available list. ([#&#8203;12580](https://redirect.github.com/jdx/mise/pull/12580) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **upgrade:** When tools are current for their configured version ranges but newer releases exist beyond those ranges, `mise upgrade` no longer prints "All tools are up to date". It now lists the affected tools with their current and available versions plus the config source, and points to `mise upgrade --bump`. ([#&#8203;12613](https://redirect.github.com/jdx/mise/pull/12613) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```text
  Newer versions are available but do not match the configured version ranges:
    node 26.7.0 → 26.8.1 (~/.config/mise/config.toml)
  Run `mise upgrade --bump` to update the configuration and upgrade.
  ```
- **task:** Double-dash task arguments are now preserved with `double_dash="preserve"`. The first `--` separator was previously consumed by the outer CLI parser and lost before reaching the task. ([#&#8203;12628](https://redirect.github.com/jdx/mise/pull/12628) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **npm:** Embedded Aube npm installs are now isolated as a standalone one-package workspace, so a `pnpm-workspace.yaml` above `MISE_DATA_DIR` can no longer redirect the install into the outer workspace or mutate it. This mainly affects GitLab CI layouts where the data dir lives under `CI_PROJECT_DIR`. ([#&#8203;12630](https://redirect.github.com/jdx/mise/pull/12630) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** Repository status checks now honor the global `--jobs` limit, so `mise bootstrap` status, apply, and update run these read-only git checks with bounded concurrency. ([#&#8203;12629](https://redirect.github.com/jdx/mise/pull/12629) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Changed

- **registry:** `acli`, `mimirtool`, and `specstory` are no longer restricted to Linux and macOS and can now be installed and run on Windows. ([#&#8203;12627](https://redirect.github.com/jdx/mise/pull/12627) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **registry:** `llama.cpp` resolution now includes prereleases, since upstream began flagging its rolling `bNNNN` builds as GitHub prereleases. The existing minimum-release-age and `b` version prefix behavior is unchanged. ([#&#8203;12614](https://redirect.github.com/jdx/mise/pull/12614) by [@&#8203;jdx](https://redirect.github.com/jdx))

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.8.15...v2026.8.16>

#### 💚 Sponsor mise

mise is maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer for [**entire.io**](https://entire.io), the title sponsor of the [jdx.dev](https://jdx.dev) open source tools. Development is funded by sponsors.

If mise saves you or your team time, please consider sponsoring at [jdx.dev](https://jdx.dev/sponsors.html). Individual and company sponsorships keep mise fast, free, and independent.

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC42MS4zIiwidXBkYXRlZEluVmVyIjoiNDQuNjkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2026-09-08 08:16:51 -07:00
renovate[bot] e57cd9e6ca chore(deps): update redis docker digest to 298e5b3 (#688)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-31 11:57:17 -07:00
renovate[bot] cab81a25e0 chore(deps): update postgres:18 docker digest to 4ef4dbc (#687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-31 11:56:56 -07:00
renovate[bot] ada9f90a1c chore(deps): update ci workflows (#689)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-31 11:52:40 -07:00
renovate[bot] ee194b7110 chore(deps): update redis docker digest to 691577a (#683)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-24 11:59:46 -07:00
renovate[bot] da5f3b08e7 chore(deps): update dependency @tsconfig/bases to v1.0.27 (#684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-24 11:59:26 -07:00
renovate[bot] ddb3e848dc chore(deps): update dependency jdx/mise to v2026.8.11 (#685)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-24 11:58:39 -07:00
renovate[bot] b79c67f88c chore(deps): update dependency uuid to v14.0.2 (#686)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-24 11:58:20 -07:00
renovate[bot] fbdd599845 chore(deps): update postgres:18 docker digest to 06cad38 (#679)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-17 09:45:28 -07:00
renovate[bot] fbb161ecd2 chore(deps): update dependency pg to v8.23.0 (#681)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-17 09:45:05 -07:00
renovate[bot] f1b13a9840 chore(deps): update ci workflows (#680)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-17 09:44:57 -07:00
renovate[bot] 3be085cd8f chore(deps): update lint (#682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-17 09:44:05 -07:00
renovate[bot]andWill Schurman b5f6cb6e4e chore(deps): update lint (#678)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Will Schurman <[email protected]>
2026-08-10 09:47:36 -07:00
renovate[bot] 2e5f9ce7f5 chore(deps): update postgres:18 docker digest to a02db8c (#675)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-10 09:43:16 -07:00
renovate[bot] 38122c8bd2 chore(deps): update redis docker digest to 344e394 (#676)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-10 09:42:18 -07:00
renovate[bot] 96ead4f360 chore(deps): update dependency jdx/mise to v2026.8.3 (#677)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-10 09:33:24 -07:00
renovate[bot] 7cc580550b chore(deps): update dependency ip-address to v10.2.2 [security] (#674)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-04 08:19:24 -07:00
Will Schurman 🦦 a4740446e0 chore: treat typedoc warnings as errors to keep evergreen (#672) 2026-08-03 16:04:07 -07:00
Will Schurman 🦦 d12496dc1b chore: fix typedoc warnings (#671) 2026-08-03 16:04:07 -07:00
Will Schurman 613d5d7424 v0.66.0 2026-08-03 09:28:42 -07:00
renovate[bot] dd93c0144e chore(deps): update redis docker digest to a7efd4e (#667)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-03 09:11:41 -07:00
renovate[bot] 0137c84baf chore(deps): update lint (#669)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-03 09:10:51 -07:00
renovate[bot] f196b0dcfb chore(deps): update ci workflows (#668)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-03 09:02:49 -07:00
renovate[bot] 6ec73542af chore(deps): update node.js to v24.18.1 (#670)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-03 09:02:01 -07:00
Gabe Debes 59e05de4dc feat!: emit a dedicated metric event for count queries (#662)
# Why

Count queries reported their result through the load event, so a `COUNT(*)` returning 8k looked like we loaded 8k entities.

# How

Give counts their own event (`logDataManagerCountEvent`) so the count number stops being read as entities loaded. Breaking: `IEntityMetricsAdapter` implementers need to add the method; I'm happy to go for another path, non-breaking change if you prefer, but this felt more fundamentally correct 

<img width="480" height="240" alt="giphy" src="https://github.com/user-attachments/assets/4630fefc-cca4-42ca-afc4-22b9919df4a8" />

# Test Plan

CI. Updated the metrics unit test to check the counts route to the new event.
2026-07-27 09:01:57 -07:00
renovate[bot] 017642a1b2 chore(deps): update redis docker digest to c88d347 (#663)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| redis | digest | `234c902` → `c88d347` |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yODAuMCIsInVwZGF0ZWRJblZlciI6IjQzLjI4MC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-07-27 07:47:44 -07:00
renovate[bot] 44e18b2011 chore(deps): update dependency ctix to v2.8.2 (#664)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [ctix](https://imjuni.github.io/ctix) ([source](https://redirect.github.com/imjuni/ctix)) | [`2.8.1` → `2.8.2`](https://renovatebot.com/diffs/npm/ctix/2.8.1/2.8.2) | ![age](https://developer.mend.io/api/mc/badges/age/npm/ctix/2.8.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/ctix/2.8.1/2.8.2?slim=true) |

---

### Release Notes

<details>
<summary>imjuni/ctix (ctix)</summary>

### [`v2.8.2`](https://redirect.github.com/imjuni/ctix/releases/tag/v2.8.2)

[Compare Source](https://redirect.github.com/imjuni/ctix/compare/v2.8.1...v2.8.2)

#### What's Changed

- chore(release): prepare v2.8.2 by [@&#8203;imjuni](https://redirect.github.com/imjuni) in [#&#8203;202](https://redirect.github.com/imjuni/ctix/pull/202)

**Full Changelog**: <https://github.com/imjuni/ctix/compare/v2.8.1...v2.8.2>

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yODAuMCIsInVwZGF0ZWRJblZlciI6IjQzLjI4MC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-07-27 07:47:33 -07:00
renovate[bot] a2fd6d3f3f chore(deps): update ci workflows (#665)
> ℹ️ **Note**
> 
> This PR body was truncated due to platform limits.

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/checkout](https://redirect.github.com/actions/checkout) | action | minor | `v6.0.3` → `v6.1.0` |
| [actions/setup-node](https://redirect.github.com/actions/setup-node) | action | minor | `v6.4.0` → `v6.5.0` |
| [jdx/mise](https://redirect.github.com/jdx/mise) |  | patch | `v2026.7.5` → `v2026.7.14` |
| [jdx/mise-action](https://redirect.github.com/jdx/mise-action) | action | patch | `v4.2.0` → `v4.2.3` |

---

### Release Notes

<details>
<summary>actions/checkout (actions/checkout)</summary>

### [`v6.1.0`](https://redirect.github.com/actions/checkout/compare/v6.0.3...v6.1.0)

[Compare Source](https://redirect.github.com/actions/checkout/compare/v6.0.3...v6.1.0)

</details>

<details>
<summary>actions/setup-node (actions/setup-node)</summary>

### [`v6.5.0`](https://redirect.github.com/actions/setup-node/releases/tag/v6.5.0)

[Compare Source](https://redirect.github.com/actions/setup-node/compare/v6.4.0...v6.5.0)

##### What's Changed

- Update [@&#8203;actions/cache](https://redirect.github.com/actions/cache) to 5.1.0 and add security overrides for undici and fast-xml-parser by [@&#8203;HarithaVattikuti](https://redirect.github.com/HarithaVattikuti) in [#&#8203;1579](https://redirect.github.com/actions/setup-node/pull/1579)

**Full Changelog**: <https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0>

</details>

<details>
<summary>jdx/mise (jdx/mise)</summary>

### [`v2026.7.14`](https://redirect.github.com/jdx/mise/releases/tag/v2026.7.14): : Multi-asset GitHub installs, safer defaults, and Windows pip

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.7.13...v2026.7.14)

This release adds overlay installs for GitHub-based tools, closes a config-trust security gap around default shell arguments, and lands a wide batch of correctness fixes across tasks, lockfiles, npm, Swift, brew casks, and Windows Python.

##### Added

- **github:** the GitHub/GitLab/Forgejo release backend now accepts `additional_asset_patterns`, so a single install can overlay multiple release archives from the same tag into one installation directory. Each supplemental artifact is locked and verified (checksums and provenance) on its own, and `--locked` fails if the recorded set no longer matches. This models release layouts like Ollama's optional ROCm archive without making a large payload unconditional. ([#&#8203;11272](https://redirect.github.com/jdx/mise/pull/11272) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [tools."github:ollama/ollama"]
  version = "latest"
  additional_asset_patterns = ["ollama-linux-amd64-rocm.tgz"]
  ```
- **npm:** new `allow_low_downloads` tool option lets an embedded aube install bypass the weekly-download popularity gate for the requested package only, so curated tools like `bibtex-tidy` and `purty` install again without `npm.shell_out`. Transitive dependencies still hit the gate. ([#&#8203;11305](https://redirect.github.com/jdx/mise/pull/11305) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **env:** structured env files (`env._.file` loading JSON/YAML/TOML) support an explicit `expand = true` option for shell-style variable expansion, including references to earlier values. See Fixed below for the default-behavior change. ([#&#8203;11269](https://redirect.github.com/jdx/mise/pull/11269) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Fixed

- **env:** values loaded from JSON/YAML/TOML env files are literal by default again, fixing a regression where every value was shell-expanded when `env_shell_expand` was on. That corrupted literals such as bcrypt-style `$6$salt$hash` and could pull in matching process-environment values. Opt back into expansion with `expand = true`. ([#&#8203;11269](https://redirect.github.com/jdx/mise/pull/11269) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **python:** `pip` is now usable on fresh Windows installs. mise synthesizes `pip.cmd`/`pip3.cmd` wrappers, adds the install's `Scripts` directory to PATH so pip-installed console scripts resolve, and fixes default-package installation on Windows. ([#&#8203;11278](https://redirect.github.com/jdx/mise/pull/11278) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **github:** `.exe` release assets are now preferred on Windows, so tools that ship both an extensionless and `.exe` binary no longer install the non-runnable one and fail with "cannot find binary path". ([#&#8203;11257](https://redirect.github.com/jdx/mise/pull/11257) by [@&#8203;gologames](https://redirect.github.com/gologames))
- **github:** a rejected GitHub token (401) now produces an actionable error that names the token source (gh CLI, `github_tokens.toml`, OAuth, env var, etc.) and includes GitHub's response and a remediation hint, instead of a bare `401 Unauthorized`. ([#&#8203;11236](https://redirect.github.com/jdx/mise/pull/11236) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))
- **backend:** an archive containing a single binary with an OS/arch suffix (e.g. `gdscript-formatter-macos-aarch64`) is now renamed to its clean name on PATH, matching the existing behavior for single-file downloads. ([#&#8203;11232](https://redirect.github.com/jdx/mise/pull/11232) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))
- **brew:** brew-cask installs handle more real-world casks — completion stanzas and `system_command` in lifecycle blocks are supported, and artifact links into root-owned directories like `/usr/local/bin` now elevate through mise's sudo policy instead of failing with permission errors. Fixes installing `docker-desktop`. ([#&#8203;11273](https://redirect.github.com/jdx/mise/pull/11273) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **lockfile:** `prefix:` tool requests now honor the locked version on `mise install` instead of silently re-resolving to the newest match. Constrained upgrades via `mise upgrade`/`mise lock --bump` still work. ([#&#8203;11255](https://redirect.github.com/jdx/mise/pull/11255) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **aqua:** cross-platform lockfiles for aqua HTTP packages preserve a reachable `v`-prefixed URL, fixing 404s where an entry locked to another platform's unprefixed URL. ([#&#8203;11267](https://redirect.github.com/jdx/mise/pull/11267) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **swift:** Swift lockfile entries now record the target Linux distro (`ubuntu24.04`, `fedora39`, `ubi9`, ...) so checksums are no longer verified across mismatched distro tarballs, and `mise lock` writes meaningful entries and re-locks correctly after changing `swift.platform`. ([#&#8203;11299](https://redirect.github.com/jdx/mise/pull/11299) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **task:** with a git worktree checked out inside the main checkout, mise no longer loads the enclosing monorepo root's tasks into the nested root's namespace. Env, tools, and vars still inherit as before. ([#&#8203;11283](https://redirect.github.com/jdx/mise/pull/11283) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **task:** changing a task's `run` command, `sources`, or `outputs` now invalidates its cached state, so tasks are no longer incorrectly skipped after such edits. ([#&#8203;11288](https://redirect.github.com/jdx/mise/pull/11288) by [@&#8203;rabadin](https://redirect.github.com/rabadin))
- **task:** a task's source hash is now persisted only after a successful run, so a failed run no longer marks stale sources as up to date. ([#&#8203;11296](https://redirect.github.com/jdx/mise/pull/11296) by [@&#8203;rabadin](https://redirect.github.com/rabadin))
- **completions:** shell completion no longer offers mise's own global flags after a task name (where they aren't valid), and a task flag that shares a name with a mise flag now completes its own values correctly. ([#&#8203;11284](https://redirect.github.com/jdx/mise/pull/11284) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **npm:** npm package versions with very large numeric components (up to `Number.MAX_SAFE_INTEGER`) now sort correctly, fixing cases where a `0.0.*` build could be picked over a newer stable release. ([#&#8203;11280](https://redirect.github.com/jdx/mise/pull/11280) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **npm:** aube trust-downgrade failures now surface a clearer explanation, and aube's own progress display no longer competes with mise's output (bumped to 1.33.1). ([#&#8203;11292](https://redirect.github.com/jdx/mise/pull/11292), [#&#8203;11308](https://redirect.github.com/jdx/mise/pull/11308) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **npm:** aube `allow_builds` is now serialized as a map. ([#&#8203;11262](https://redirect.github.com/jdx/mise/pull/11262) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **version:** the update-check cache now negative-caches, so machines that can't reach the network (or run a build newer than the latest release) stop re-running the full check — including building an HTTP client and parsing the CA trust store — on every invocation. This is a significant speedup for commands like `mise --version` in those cases. ([#&#8203;11285](https://redirect.github.com/jdx/mise/pull/11285) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **env:** remaining `std::env::vars()` call sites now use `vars_safe()`. ([#&#8203;11309](https://redirect.github.com/jdx/mise/pull/11309) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))

##### Changed

- **usage:** every mise command now declares its effect on the system — read-only, modifies state, or destructive — surfaced in the command reference. This requires usage 4.0 (`min_usage_version` bumped), so shell completions and doc rendering now depend on it. ([#&#8203;11306](https://redirect.github.com/jdx/mise/pull/11306) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Security

- **config:** the Unix/Windows default file and inline shell-argument settings are now global-only. Because local config is loaded before trust evaluation, an untrusted repository could previously influence how commands from trusted sources were executed. Global config and `MISE_*` environment variables still apply. Reported by [@&#8203;arpitjain099](https://redirect.github.com/arpitjain099). ([#&#8203;11293](https://redirect.github.com/jdx/mise/pull/11293) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Performance

- **cli:** the clap command tree is no longer rebuilt twice on every invocation. ([#&#8203;11297](https://redirect.github.com/jdx/mise/pull/11297) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Documentation

- **npm:** documented Socket integrations. ([#&#8203;11268](https://redirect.github.com/jdx/mise/pull/11268) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **backend:** corrected the `bin_path`/`asset_pattern` template variable docs. ([#&#8203;11298](https://redirect.github.com/jdx/mise/pull/11298) by [@&#8203;jdx](https://redirect.github.com/jdx))
- Retargeted a dead clap.rs link to docs.rs/clap. ([#&#8203;11194](https://redirect.github.com/jdx/mise/pull/11194) by [@&#8203;Bartok9](https://redirect.github.com/Bartok9))
- Added a generated `llms.txt` index for AI agents. ([#&#8203;11300](https://redirect.github.com/jdx/mise/pull/11300) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Registry

- Added `checkstyle` ([#&#8203;11287](https://redirect.github.com/jdx/mise/pull/11287) by [@&#8203;zeitlinger](https://redirect.github.com/zeitlinger)), `grok` ([#&#8203;11291](https://redirect.github.com/jdx/mise/pull/11291)), and `tak` ([#&#8203;11307](https://redirect.github.com/jdx/mise/pull/11307)) by [@&#8203;jdx](https://redirect.github.com/jdx).

##### New Contributors

- [@&#8203;gologames](https://redirect.github.com/gologames) made their first contribution in [#&#8203;11257](https://redirect.github.com/jdx/mise/pull/11257)

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.7.13...v2026.7.14>

##### 💚 Sponsor mise

mise is maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer for [**entire.io**](https://entire.io), the title sponsor of the [jdx.dev](https://jdx.dev) open source tools. Development is funded by sponsors.

If mise saves you or your team time, please consider sponsoring at [jdx.dev](https://jdx.dev/sponsors.html). Individual and company sponsorships keep mise fast, free, and independent.

### [`v2026.7.13`](https://redirect.github.com/jdx/mise/releases/tag/v2026.7.13): : Multi-binary renaming, cask completions, and go.mod support

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.7.12...vfox-v2026.7.13)

This release expands archive backends to rename multiple binaries from a single release, adds Homebrew cask shell completions and `go.mod` version-file support, and fixes a broad batch of task, install, lockfile, and language-plugin correctness issues.

#### Added

- **backend:** `rename_exe` now accepts a table mapping source patterns to target names, so an archive that ships several executables can expose all of them cleanly on PATH instead of only one. The existing string form is unchanged. ([#&#8203;11231](https://redirect.github.com/jdx/mise/pull/11231) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [tools."github:DanielGavin/ols"]
  version = "latest"
  rename_exe = { "ols-*" = "ols", "odinfmt-*" = "odinfmt" }
  ```
- **brew:** brew-cask now installs shell completions (declared `bash_completion`/`fish_completion`/`zsh_completion` files and `generate_completions_from_executable` for bash, zsh, fish, and pwsh) instead of skipping them, tracking them in cask receipts and installed-state checks. ([#&#8203;11198](https://redirect.github.com/jdx/mise/pull/11198) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **go:** `go.mod` can now be used as an idiomatic version file. A `toolchain goX.Y.Z` directive resolves as an exact pin, while a `go X.Y` minimum resolves to the latest matching patch. It is opt-in and unchanged unless enabled per-tool. ([#&#8203;11213](https://redirect.github.com/jdx/mise/pull/11213) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))

  ```sh
  mise settings add idiomatic_version_file_enable_tools go
  ```
- **task:** PowerShell task shells (`pwsh`/`powershell`) now run with `-NoProfile` by default, matching how mise spawns POSIX shells, so a profile that mutates `PATH` (such as a mise activation snippet) can no longer shadow a task's own tools and cause confusing "cannot find binary path" errors. Opt out with the new `windows_powershell_no_profile` setting (`MISE_WINDOWS_POWERSHELL_NO_PROFILE=false`). ([#&#8203;11199](https://redirect.github.com/jdx/mise/pull/11199) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Fixed

- **env:** `_.path`/`_.file`/`_.source` directives within a single `[env]._` block are now resolved in written order, so a `_.path` can reference a variable exported by a `_.source` written before it. ([#&#8203;11163](https://redirect.github.com/jdx/mise/pull/11163) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **install:** failed installs no longer print a contradictory "installed but not activated" hint before the real error. ([#&#8203;11227](https://redirect.github.com/jdx/mise/pull/11227) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **install:** logical state mutations during install are now serialized to avoid races. ([#&#8203;11207](https://redirect.github.com/jdx/mise/pull/11207) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **lockfile:** upgrading a locked `github:` tool to a newer version that is already installed on disk no longer triggers a false supply-chain "provenance regression" error. ([#&#8203;11230](https://redirect.github.com/jdx/mise/pull/11230) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **npm:** aube install failures now surface the full cause chain instead of an opaque "failed to resolve dependencies" message, with mise-native remediation guidance (`trust_policy_excludes` or `npm.shell_out`) for trust-downgrade errors. ([#&#8203;11226](https://redirect.github.com/jdx/mise/pull/11226) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **task:** inline task definitions now follow a consistent first-wins precedence model across local, `conf.d`, and monorepo configs, so lower-precedence metadata no longer leaks into script tasks and the winning config's context is preserved. ([#&#8203;11103](https://redirect.github.com/jdx/mise/pull/11103) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **task:** source freshness now tracks files correctly for workspace-rooted `sources` patterns used in nested subproject tasks, so edits inside a subproject trigger a rerun. ([#&#8203;11202](https://redirect.github.com/jdx/mise/pull/11202) by [@&#8203;rabadin](https://redirect.github.com/rabadin))
- **brew:** cask upgrades handle structured `preflight_steps`/`postflight_steps` for move/remove operations, and cleanup-only `zap` `pkgutil` IDs are no longer treated as install receipts (which could leave pkg cask status permanently missing). ([#&#8203;11197](https://redirect.github.com/jdx/mise/pull/11197) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **brew:** cask upgrades no longer fail when removing a protected app backup (e.g. docker-desktop) hits "Permission denied", recovering permissions the way Homebrew does before retrying. ([#&#8203;11219](https://redirect.github.com/jdx/mise/pull/11219) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **python:** a `python3` executable is now provided on Windows. ([#&#8203;11212](https://redirect.github.com/jdx/mise/pull/11212) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **ruby:** custom `ruby.precompiled_url = "owner/repo"` sources now fetch release metadata directly from GitHub instead of the restricted versions host, avoiding 403 warnings. ([#&#8203;11154](https://redirect.github.com/jdx/mise/pull/11154) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **ruby:** Gemfile version pins with multi-digit segments (e.g. `ruby "3.4.10"`) are now parsed correctly. ([#&#8203;11229](https://redirect.github.com/jdx/mise/pull/11229) by [@&#8203;capnregex](https://redirect.github.com/capnregex))
- **bootstrap:** `./`-prefixed relative `[bootstrap.repos]` paths no longer display with a leading `./` component. ([#&#8203;11214](https://redirect.github.com/jdx/mise/pull/11214) by [@&#8203;lilienblum](https://redirect.github.com/lilienblum))
- **http:** invalid URLs now return an error instead of panicking. ([#&#8203;11160](https://redirect.github.com/jdx/mise/pull/11160) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))
- **settings:** avoid a panic when a parent settings key is an inline table. ([#&#8203;11184](https://redirect.github.com/jdx/mise/pull/11184) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))
- **self-update:** the updater's TLS backend now matches the feature it was built with, fixing failures when compiled with `native-tls`. ([#&#8203;10834](https://redirect.github.com/jdx/mise/pull/10834) by [@&#8203;bltavares](https://redirect.github.com/bltavares))

#### Deprecated

- **python:** the legacy `virtualenv` tool option is deprecated in favor of the `_.python.venv` env directive. It now emits a warning and is scheduled for removal around `2027.7.0`. ([#&#8203;11234](https://redirect.github.com/jdx/mise/pull/11234) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))

#### Documentation

- **python:** clarify that `uv_venv_auto` requires a `uv.lock` file. ([#&#8203;11223](https://redirect.github.com/jdx/mise/pull/11223) by [@&#8203;jdx](https://redirect.github.com/jdx))
- Fix dead links in the contributing guide and the aqua-registry/pipx installation docs. ([#&#8203;11123](https://redirect.github.com/jdx/mise/pull/11123), [#&#8203;11167](https://redirect.github.com/jdx/mise/pull/11167) by [@&#8203;Bartok9](https://redirect.github.com/Bartok9))

#### New Contributors

- [@&#8203;capnregex](https://redirect.github.com/capnregex) made their first contribution in [#&#8203;11229](https://redirect.github.com/jdx/mise/pull/11229)
- [@&#8203;rabadin](https://redirect.github.com/rabadin) made their first contribution in [#&#8203;11202](https://redirect.github.com/jdx/mise/pull/11202)

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.7.12...v2026.7.13>

#### 💚 Sponsor mise

mise is maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer for [**entire.io**](https://entire.io), the title sponsor of the [jdx.dev](https://jdx.dev) open source tools. Development is funded by sponsors.

If mise saves you or your team time, please consider sponsoring at [jdx.dev](https://jdx.dev/sponsors.html). Individual and company sponsorships keep mise fast, free, and independent.

### [`v2026.7.12`](https://redirect.github.com/jdx/mise/releases/tag/v2026.7.12): : Safe mode, node-free npm, and a built-in OCI push client

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.7.11...vfox-v2026.7.12)

This release centers on running mise safely against untrusted config, shipping node-free `npm:` version resolution and installs, and replacing external tools (skopeo/crane, gpg) with built-in, dependency-free implementations. Along the way it fixes a `prefer_offline` timeout regression, several config/env correctness issues, and platform-specific problems on Windows and macOS.

#### Highlights

- New `MISE_SAFE=1` mode turns mise into an inert config reader so automation (CI, Renovate) can run `mise lock --bump` against untrusted branches without trust prompts or arbitrary code execution.
- The `npm:` backend no longer needs node or npm installed to resolve or install packages — version metadata is fetched over HTTP and installs run in-process via the embedded aube package manager.
- `mise oci push` gained a full built-in registry client: no more skopeo/crane, with blob dedup, chunked uploads, retries, cross-repo mounts, layer reuse from prior images, and multi-arch image indexes.

#### Added

- **Safe mode:** a new global-only `safe` setting (`MISE_SAFE=1`) makes mise refuse or ignore anything that would let project config execute code or inject environment. Blocked (with errors): template `exec()`/`read_file()`, `_.source`, hooks, tasks, asdf plugin scripts, and plugin installs. Ignored: project `[env]`, `_.path`, `_.file`, `[shell_alias]`, and `[settings]`. HTTP-based version resolution, lockfile refresh/bump, and already-installed/embedded plugins keep working. Because a config loaded this way is inert, safe mode also skips the trust requirement. ([#&#8203;11146](https://redirect.github.com/jdx/mise/pull/11146), [#&#8203;11151](https://redirect.github.com/jdx/mise/pull/11151) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```sh
  MISE_SAFE=1 mise lock --bump --dry-run --json   # detect updates with zero code execution, no trust needed
  ```
- **`mise lock --bump`:** advance fuzzy lockfile selectors (`latest`, `lts`, prefixes like `"20"`) to the newest matching remote versions without installing anything and without touching `mise.toml`. Exact pins stay put. Pairs with `--json` and `--dry-run` for a CI job that opens lockfile-only PRs. ([#&#8203;11145](https://redirect.github.com/jdx/mise/pull/11145) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```console
  $ mise lock --bump --dry-run --json
  [{ "name": "node", "backend": "core:node", "old_versions": ["22.14.0"], "new_versions": ["22.15.0"] }]
  ```
- **Node-free `npm:` backend:** `mise ls-remote npm:*` and `latest` resolution now query the npm registry directly over HTTP, and installs run in-process through the embedded aube package manager. node is only needed to *run* installed tools (and their lifecycle scripts), not to install them. User `~/.npmrc`, scoped registries, auth, and `minimum_release_age` are honored. A new `npm.shell_out` setting (`MISE_NPM_SHELL_OUT`) routes both metadata and installs through the npm CLI for setups relying on npm-only config like `cafile` or client certs. ([#&#8203;11147](https://redirect.github.com/jdx/mise/pull/11147), [#&#8203;11149](https://redirect.github.com/jdx/mise/pull/11149) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **Built-in OCI registry client:** `mise oci push` no longer requires skopeo or crane. It uses an in-tree OCI Distribution client that uploads only blobs the registry is missing, resolves credentials the way docker/podman do (`docker login`/`podman login` is all the setup needed), and answers registry auth challenges so private base-image pulls via `--from` also work. `mise oci run --engine docker` now streams the image into `docker load` instead of needing skopeo. The `--tool` flag was removed. ([#&#8203;11132](https://redirect.github.com/jdx/mise/pull/11132) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **OCI push robustness:** large layers upload in 64 MiB chunks, transient failures retry with mise's standard backoff, base layers are mounted cross-repo (`?mount=`) when they already live on the destination registry (zero bytes transferred), and pushes/pulls now show byte progress. A new `oci.insecure_registries` setting opts non-loopback registries into plain HTTP. ([#&#8203;11141](https://redirect.github.com/jdx/mise/pull/11141) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **OCI layer reuse:** `mise oci push` reuses unchanged tool layers from the previously pushed image (or `--cache-from REF`), skipping the tar/gzip build entirely — reused tools don't even need to be installed locally. `--no-cache` forces a full rebuild. ([#&#8203;11142](https://redirect.github.com/jdx/mise/pull/11142) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **Multi-arch OCI images:** `mise oci push --update-index` points a tag at an OCI image index and upserts the current platform while preserving entries pushed by other architectures, so one runner per arch can assemble a multi-arch tag. ([#&#8203;11144](https://redirect.github.com/jdx/mise/pull/11144) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **Built-in signature verification:** Node.js and Swift download signatures are now verified in-process with the pure-Rust rPGP crate against the bundled keyrings, dropping the external `gpg` binary dependency. Note: previously, if `gpg` was missing and the setting was unset, verification was silently skipped; now verification always runs when enabled — opt out with `gpg_verify = false`. ([#&#8203;11148](https://redirect.github.com/jdx/mise/pull/11148) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **Relative bootstrap repo paths:** `[bootstrap.repos]` destination keys can now be relative paths, resolved against the declaring project's root (with guards preventing escape outside the project tree), making a project's `mise.toml` portable across machines. ([#&#8203;11155](https://redirect.github.com/jdx/mise/pull/11155) by [@&#8203;lilienblum](https://redirect.github.com/lilienblum))

#### Fixed

- **http:** `mise lock`, `ls-remote`, `outdated`, and `upgrade` no longer get capped to a single 3s no-retry attempt when `prefer_offline` is set — they honor the configured `fetch_remote_versions_timeout` and retry budget again, fixing spurious timeouts. Interactive fast paths keep their bounded behavior. ([#&#8203;11190](https://redirect.github.com/jdx/mise/pull/11190) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **lock:** unfiltered `mise lock` no longer prunes OS-restricted tool entries (e.g. `os = ["macos"]`) when run on a platform that doesn't target them, keeping shared lockfiles stable across platforms. ([#&#8203;11175](https://redirect.github.com/jdx/mise/pull/11175) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **env:** values loaded from `_.file` env files now resolve references to variables defined in earlier files or `[env]` blocks instead of collapsing to empty, fixing flaky dotenv behavior. ([#&#8203;11158](https://redirect.github.com/jdx/mise/pull/11158) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))
- **env:** `[env] _.source` now works on Windows by resolving a real POSIX bash (Git Bash / MSYS2, honoring `MISE_BASH_PATH`) instead of the WSL launcher or a missing `bash.exe`, and correctly filters MSYS runtime noise and converts prepended PATH entries back to Windows form. ([#&#8203;11097](https://redirect.github.com/jdx/mise/pull/11097) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **config:** `trusted_config_paths` (`MISE_TRUSTED_CONFIG_PATHS`) now overrides configs previously added to the ignore list, so a settings-trusted config is actually discovered and loaded. ([#&#8203;11152](https://redirect.github.com/jdx/mise/pull/11152) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **config:** `env`/`vars` array parsing is now consistent — accidental array forms for `vars` and task `env`/`vars` are rejected with clear errors, while the intentional `[[env]]` and directive-level arrays (`_.source`, `_.file`, `_.path`) remain supported. ([#&#8203;11060](https://redirect.github.com/jdx/mise/pull/11060) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **config:** `mise config set` no longer panics when descending into a non-table value. ([#&#8203;11153](https://redirect.github.com/jdx/mise/pull/11153) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))
- **config:** clearer warnings for unsupported semver ranges. ([#&#8203;11176](https://redirect.github.com/jdx/mise/pull/11176) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **hook-env:** entering a directory with an untrusted config now prints a single concise warning (`... is not trusted, run 'mise trust' to enable it`) instead of the full error chain. Explicit commands still show the full trust error and prompt. ([#&#8203;11159](https://redirect.github.com/jdx/mise/pull/11159) by [@&#8203;lilienblum](https://redirect.github.com/lilienblum))
- **activate:** `mise activate zsh` no longer hangs non-interactive login shells under Rosetta — a v2026.7.11 regression where env-state snapshotting autoloaded the `zsh/parameter` module via `dlopen`. ([#&#8203;11188](https://redirect.github.com/jdx/mise/pull/11188) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **brew-cask:** case-insensitive app bundle lookup (fixing casks like `yaak`) and correct resolution of preflight-generated wrapper scripts on the extract stage (fixing `vlc`). ([#&#8203;11164](https://redirect.github.com/jdx/mise/pull/11164) by [@&#8203;donbeave](https://redirect.github.com/donbeave))
- **brew:** cask binaries can symlink into `/usr/local` on arm64. ([#&#8203;11174](https://redirect.github.com/jdx/mise/pull/11174) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **backend:** qualified prerelease suffixes are now detected correctly. ([#&#8203;11179](https://redirect.github.com/jdx/mise/pull/11179) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **completions:** avoid a network lookup during usage completion. ([#&#8203;11169](https://redirect.github.com/jdx/mise/pull/11169) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **link:** clear a stale incomplete marker when linking a tool. ([#&#8203;11150](https://redirect.github.com/jdx/mise/pull/11150) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))
- **shim:** unresolved Windows exe-shim dispatch (e.g. a project-scoped tool run outside the project) now shows the actionable "no version is set" guidance instead of an opaque "cannot find binary path". ([#&#8203;11189](https://redirect.github.com/jdx/mise/pull/11189) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **vfox:** preserve inner quotes when a vfox Lua command dispatches through `cmd.exe` on Windows, fixing commands such as the Yarn 2+ install flow. ([#&#8203;11166](https://redirect.github.com/jdx/mise/pull/11166) by [@&#8203;finalchild](https://redirect.github.com/finalchild))
- **github/gitlab/forgejo:** statically-linked `linux-musl` assets are now considered a fallback for Android/Termux when a package publishes no Android-specific asset, so tools like starship, bat, and hk can install there. ([#&#8203;10730](https://redirect.github.com/jdx/mise/pull/10730) by [@&#8203;bltavares](https://redirect.github.com/bltavares))
- **deps:** avoid installing tools during dry runs, and enforce the experimental gate for task providers. ([#&#8203;11016](https://redirect.github.com/jdx/mise/pull/11016), [#&#8203;11177](https://redirect.github.com/jdx/mise/pull/11177) by [@&#8203;risu729](https://redirect.github.com/risu729))

#### Documentation

- Document environment-variable backing for file-task arguments and flags, including CLI/env/default precedence. ([#&#8203;11165](https://redirect.github.com/jdx/mise/pull/11165) by [@&#8203;zeitlinger](https://redirect.github.com/zeitlinger))

#### Breaking Changes

- `mise oci push --tool` has been removed. Use `mise oci build -o ./img` followed by `skopeo copy` if you need the old escape hatch. ([#&#8203;11132](https://redirect.github.com/jdx/mise/pull/11132))
- Node/Swift signature verification now always runs when enabled (external `gpg` is no longer required). If you relied on a missing `gpg` binary to skip verification, set `gpg_verify = false` explicitly. ([#&#8203;11148](https://redirect.github.com/jdx/mise/pull/11148))
- The unreleased `npm.use_npm_view` setting is replaced by `npm.shell_out` (`MISE_NPM_SHELL_OUT`). ([#&#8203;11149](https://redirect.github.com/jdx/mise/pull/11149))

#### New Contributors

- [@&#8203;donbeave](https://redirect.github.com/donbeave) made their first contribution in [#&#8203;11164](https://redirect.github.com/jdx/mise/pull/11164)
- [@&#8203;lilienblum](https://redirect.github.com/lilienblum) made their first contribution in [#&#8203;11159](https://redirect.github.com/jdx/mise/pull/11159)

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.7.11...v2026.7.12>

#### 💚 Sponsor mise

mise is maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer for [**entire.io**](https://entire.io), the title sponsor of the [jdx.dev](https://jdx.dev) open source tools. Development is funded by sponsors.

If mise saves you or your team time, please consider sponsoring at [jdx.dev](https://jdx.dev/sponsors.html). Individual and company sponsorships keep mise fast, free, and independent.

### [`v2026.7.11`](https://redirect.github.com/jdx/mise/releases/tag/v2026.7.11): : Task Source Tracking and Activation Speedups

[Compare Source](https://redirect.github.com/jdx/mise/compare/v2026.7.7...vfox-v2026.7.11)

This release sharpens task handling around remote files, global config, and tool selectors, trims redundant work from shell activation, and fixes several bootstrap and platform-specific edge cases.

> \[!NOTE]
> This is the first published release since v2026.7.7 — versions 2026.7.8 through 2026.7.10 were tagged but never published due to a release pipeline issue. Their changes are included here; see [**Also in this release**](#also-in-this-release-v202678v2026710) below.

#### Added

- **config:** structured tool definitions now accept `version`, `path`, `prefix`, and `ref` selectors consistently in root `[tools]`, inline task definitions, task templates, and file-task headers. Exactly one selector is required, and conflicting, missing, or non-string selectors now fail with a clear configuration error instead of silently defaulting or panicking. ([#&#8203;11069](https://redirect.github.com/jdx/mise/pull/11069) by [@&#8203;risu729](https://redirect.github.com/risu729))

  ```toml
  node = { version = "20" }
  go = { prefix = "1.22" }
  python = { ref = "main" }
  shellcheck = { path = "/opt/shellcheck" }
  ```

#### Fixed

- **task:** remote HTTP and Git-backed task files now have their `#MISE` headers parsed, so metadata like `tools`, `description`, and `hide` and inline TOML overrides are honored just like local file tasks. Git-backed task files are also made executable after cloning and on cache hits. ([#&#8203;11111](https://redirect.github.com/jdx/mise/pull/11111) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))
- **task:** file tasks loaded from user-global or system configs are now correctly treated as global regardless of their include path, so custom global scripts appear under `mise tasks --global`, stay out of `--local`, and use the invoking project root. ([#&#8203;11106](https://redirect.github.com/jdx/mise/pull/11106) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **task:** `mise tasks info`, extended listings, task help, and the MCP tasks resource now report every configuration source contributing to a task, with a new `config_sources` array in JSON output. Changing metadata in a TOML overlay now correctly invalidates the merged file task's cache. ([#&#8203;11098](https://redirect.github.com/jdx/mise/pull/11098) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **watch:** `mise --env <profile> watch` now propagates the selected environment to the watched task, so profile-specific tasks resolve correctly on each rerun. ([#&#8203;11114](https://redirect.github.com/jdx/mise/pull/11114) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))
- **http:** raw executables (such as PHP PHARs like Composer) installed via the HTTP backend are now created readable as well as executable (mode `0755` rather than `0711`), fixing "Could not open input file" errors for interpreters run by non-owner users. ([#&#8203;11135](https://redirect.github.com/jdx/mise/pull/11135) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **java:** `mise latest java@<vendor>` now prefers the base vendor variant over dashed specializations and sorts multi-feature vendor prefixes correctly. ([#&#8203;11109](https://redirect.github.com/jdx/mise/pull/11109) by [@&#8203;roele](https://redirect.github.com/roele))
- **config:** reject conflicting tool selectors at parse time (see Added). ([#&#8203;11069](https://redirect.github.com/jdx/mise/pull/11069) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **bootstrap:** systemd timers can now reference a managed service by its bare TOML key (e.g. `unit = "dotfiles-maintain"`), which resolves to `dev.mise.<name>.service`. Fully qualified names like `nginx.service` are still written verbatim. ([#&#8203;11128](https://redirect.github.com/jdx/mise/pull/11128) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** Homebrew casks that declare `auto_updates` are now accepted instead of failing with an unsupported-lifecycle error. ([#&#8203;11107](https://redirect.github.com/jdx/mise/pull/11107) by [@&#8203;casparbreloh](https://redirect.github.com/casparbreloh))
- **bootstrap:** macOS `defaults` handling now treats a missing key or domain as unset rather than erroring during initial setup. ([#&#8203;11118](https://redirect.github.com/jdx/mise/pull/11118) by [@&#8203;roele](https://redirect.github.com/roele))
- **windows:** `mise uninstall` now removes dangling runtime version pointer files (like `16` or `latest`) and cleans up the now-empty tool directory. ([#&#8203;11095](https://redirect.github.com/jdx/mise/pull/11095) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))

#### Performance

- **activate:** the redundant initial prompt hook that ran immediately after activation is now skipped for bash, zsh, and fish when the session is unchanged, avoiding an extra `mise` process on shell startup. ([#&#8203;11130](https://redirect.github.com/jdx/mise/pull/11130), [#&#8203;11131](https://redirect.github.com/jdx/mise/pull/11131), [#&#8203;11134](https://redirect.github.com/jdx/mise/pull/11134) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **backend:** archive extraction and macOS `hdiutil`/`pkgutil` waits now run via `block_in_place`, so parallel installs no longer starve download progress and other tasks of runtime threads. ([#&#8203;11136](https://redirect.github.com/jdx/mise/pull/11136) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Registry

- Allow the reviewed `@nubjs/nub` postinstall lifecycle script to run via the npm backend so upgrades keep the `~/.nub/shims` hardlinks refreshed; default-deny behavior is preserved for all other packages. ([#&#8203;11126](https://redirect.github.com/jdx/mise/pull/11126) by [@&#8203;risu729](https://redirect.github.com/risu729))

#### Documentation

- **environments:** clarify mise directory variables ([#&#8203;11127](https://redirect.github.com/jdx/mise/pull/11127) by [@&#8203;jdx](https://redirect.github.com/jdx))
- Fix dead VersionFox and MCP documentation links ([#&#8203;11117](https://redirect.github.com/jdx/mise/pull/11117) by [@&#8203;Bartok9](https://redirect.github.com/Bartok9))
- Add an example showing how to alias multiple tools from one GitHub release ([#&#8203;11110](https://redirect.github.com/jdx/mise/pull/11110) by [@&#8203;LukasKnuth](https://redirect.github.com/LukasKnuth))

***

#### Also in this release: v2026.7.8–v2026.7.10

Due to a release pipeline issue, versions 2026.7.8 through 2026.7.10 were tagged but never published. This is the first release since [v2026.7.7](https://redirect.github.com/jdx/mise/releases/tag/v2026.7.7), so it also ships everything below.

##### Added

- **bootstrap:** package manager plugins — vfox Lua plugins can now act as system package managers, with a declarative `[bootstrap.plugins]` config and `mise bootstrap plugins apply|status` commands. Declared plugins install before built-in packages, then plugin-managed packages apply once host tools are available. ([#&#8203;11023](https://redirect.github.com/jdx/mise/pull/11023), [#&#8203;11024](https://redirect.github.com/jdx/mise/pull/11024) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** new `mise bootstrap repos update` and `mise bootstrap repos exec` commands. Unpinned `[bootstrap.repos]` entries are intentionally never pulled by declarative apply; `repos update` gives an explicit fetch + fast-forward path (with dry-run and path filtering), and `repos exec` runs a command across configured checkouts. `mise bootstrap --update` now also updates repos. ([#&#8203;11022](https://redirect.github.com/jdx/mise/pull/11022) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **registry:** opt-in floating registries — with `registry_floating`, mise fetches the latest released registry metadata (and the current aqua registry) instead of the release-pinned copies. Useful on distributions whose mise package lags behind releases while registry entries keep changing. ([#&#8203;10971](https://redirect.github.com/jdx/mise/pull/10971) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **backend:** exact-version fast path — when a request is an exact semver version, the cargo, npm, go, gem, pipx, and dotnet backends now skip fetching the remote version list entirely and let the underlying installer validate it. This speeds up exact pins without a lockfile, first-time `mise install [email protected]`, and `mise x [email protected]`. ([#&#8203;11013](https://redirect.github.com/jdx/mise/pull/11013) by [@&#8203;Turbo87](https://redirect.github.com/Turbo87), [#&#8203;11070](https://redirect.github.com/jdx/mise/pull/11070) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **task:** dependencies whose templated names render to an empty string are now skipped, so templates can conditionally disable a dependency. Empty templated dependency *args* are omitted as well. ([#&#8203;11057](https://redirect.github.com/jdx/mise/pull/11057) by [@&#8203;risu729](https://redirect.github.com/risu729), [#&#8203;11055](https://redirect.github.com/jdx/mise/pull/11055) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Fixed

- **http:** mise now fails fast when the network is unavailable: DNS resolver failures are treated as non-retryable, a process-local circuit opens after hard DNS/connection failures, and remote-version lookups are capped at 3 seconds under `prefer_offline`. Shims, shell activation, and `mise x` make one bounded attempt instead of grinding through full retry schedules per endpoint. ([#&#8203;11066](https://redirect.github.com/jdx/mise/pull/11066) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **config:** stricter, clearer configuration validation: `env` directive value aliases ([#&#8203;11062](https://redirect.github.com/jdx/mise/pull/11062)), `env.mise` directives ([#&#8203;11053](https://redirect.github.com/jdx/mise/pull/11053)), and the experimental monorepo root key ([#&#8203;11052](https://redirect.github.com/jdx/mise/pull/11052)) are deprecated, and non-string `postinstall` hooks are rejected at parse time ([#&#8203;11061](https://redirect.github.com/jdx/mise/pull/11061)) (all by [@&#8203;risu729](https://redirect.github.com/risu729))
- **hooks:** hook definitions are validated more strictly — unknown table fields ([#&#8203;11047](https://redirect.github.com/jdx/mise/pull/11047)) and nested hook arrays ([#&#8203;11051](https://redirect.github.com/jdx/mise/pull/11051)) are rejected, and the spawned-scripts form is deprecated ([#&#8203;11043](https://redirect.github.com/jdx/mise/pull/11043)) (all by [@&#8203;risu729](https://redirect.github.com/risu729))
- **schema:** a large batch of JSON schema corrections so editors validate real configs correctly: OCI copy entries ([#&#8203;11009](https://redirect.github.com/jdx/mise/pull/11009)), bootstrap shell activation ([#&#8203;11004](https://redirect.github.com/jdx/mise/pull/11004)) and bootstrap hooks ([#&#8203;11039](https://redirect.github.com/jdx/mise/pull/11039)), structured task tool options ([#&#8203;11021](https://redirect.github.com/jdx/mise/pull/11021)), required watch file patterns ([#&#8203;11040](https://redirect.github.com/jdx/mise/pull/11040)), plugin manifest fields ([#&#8203;11035](https://redirect.github.com/jdx/mise/pull/11035)), root hook definitions ([#&#8203;11041](https://redirect.github.com/jdx/mise/pull/11041)), integer types for integer settings ([#&#8203;11037](https://redirect.github.com/jdx/mise/pull/11037)), array-typed deps provider paths ([#&#8203;11064](https://redirect.github.com/jdx/mise/pull/11064)) (all by [@&#8203;risu729](https://redirect.github.com/risu729)), and systemd timer/lifecycle options ([#&#8203;11050](https://redirect.github.com/jdx/mise/pull/11050) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **dry runs** no longer cause side effects: task setup is skipped ([#&#8203;11015](https://redirect.github.com/jdx/mise/pull/11015)), install hooks are previewed instead of executed ([#&#8203;11010](https://redirect.github.com/jdx/mise/pull/11010)), and `mise prune --dry-run` preserves runtime symlinks ([#&#8203;11017](https://redirect.github.com/jdx/mise/pull/11017)) (all by [@&#8203;risu729](https://redirect.github.com/risu729))
- **bootstrap:** Homebrew cask metadata is preserved ([#&#8203;11012](https://redirect.github.com/jdx/mise/pull/11012)) and macOS metadata files in cask artifacts are ignored ([#&#8203;11063](https://redirect.github.com/jdx/mise/pull/11063)); repo origins are compared transport-agnostically so `git@` vs `https://` remotes don't trigger spurious mismatches ([#&#8203;11034](https://redirect.github.com/jdx/mise/pull/11034) by [@&#8203;jeremy](https://redirect.github.com/jeremy)); static systemd units no longer break `bootstrap status` ([#&#8203;11056](https://redirect.github.com/jdx/mise/pull/11056) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **backend:** prerelease versions without a separator (like `1.2.3rc1`) are detected correctly ([#&#8203;11032](https://redirect.github.com/jdx/mise/pull/11032) by [@&#8203;jdx](https://redirect.github.com/jdx)); cargo sparse-index version discovery is restored ([#&#8203;11011](https://redirect.github.com/jdx/mise/pull/11011) by [@&#8203;Turbo87](https://redirect.github.com/Turbo87)); go resolves direct package module prefixes ([#&#8203;11054](https://redirect.github.com/jdx/mise/pull/11054) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **install:** `false` install env values are treated as removals ([#&#8203;11033](https://redirect.github.com/jdx/mise/pull/11033) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **lockfile:** platform-specific tool option variants are preserved instead of being collapsed ([#&#8203;10999](https://redirect.github.com/jdx/mise/pull/10999) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **shell:** hyphenated tool names work in shell integrations ([#&#8203;10961](https://redirect.github.com/jdx/mise/pull/10961) by [@&#8203;risu729](https://redirect.github.com/risu729)), and bash `deactivate` no longer errors on empty array expansions under `set -u` ([#&#8203;11026](https://redirect.github.com/jdx/mise/pull/11026) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **shim:** recursion is prevented when the directory env is filtered ([#&#8203;10982](https://redirect.github.com/jdx/mise/pull/10982) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **task:** file tasks resolve source files relative to the task directory ([#&#8203;11073](https://redirect.github.com/jdx/mise/pull/11073) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562)), and tasks generated from templates inherit the template's `output` setting ([#&#8203;11059](https://redirect.github.com/jdx/mise/pull/11059) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **launchd:** `bootout` EIO for not-loaded agents is tolerated on macOS ([#&#8203;10965](https://redirect.github.com/jdx/mise/pull/10965) by [@&#8203;hsbt](https://redirect.github.com/hsbt))
- **nix:** `TZDIR` is set so timezone tests pass in the sandbox build ([#&#8203;11019](https://redirect.github.com/jdx/mise/pull/11019) by [@&#8203;coryzibell](https://redirect.github.com/coryzibell))

##### Performance

- **startup:** \~2ms shaved off every invocation — config lookup no longer compiles \~200 glob patterns per startup (literal filenames are stat'd directly), and the clap command tree is built once instead of twice. `mise version` drops from 8.1ms to 6.6ms, `hook-env` from 10.9ms to 9.3ms. ([#&#8203;11025](https://redirect.github.com/jdx/mise/pull/11025) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **release:** release binaries for linux-x64 and macos-arm64 are now built with profile-guided optimization, trained on a hermetic offline workload covering config load, toolset resolution, `hook-env`, env rendering, tasks, and exec — \~10% faster on hot paths and \~4% smaller binaries ([#&#8203;11031](https://redirect.github.com/jdx/mise/pull/11031) by [@&#8203;jdx](https://redirect.github.com/jdx)). macOS tarballs now target macOS 12 ([#&#8203;11027](https://redirect.github.com/jdx/mise/pull/11027) by [@&#8203;jdx](https://redirect.github.com/jdx)).
- **runtime:** default tokio worker threads are capped at 16, avoiding oversized thread pools on many-core machines ([#&#8203;11029](https://redirect.github.com/jdx/mise/pull/11029) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Refactor

- tar extraction, inspection, and verification are consolidated onto `jdx-tar`, removing the system `tar` fallback and a duplicate tar implementation — consistent archive behavior across platforms ([#&#8203;11028](https://redirect.github.com/jdx/mise/pull/11028) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Registry

- add zmx ([github:neurosnap/zmx](https://redirect.github.com/neurosnap/zmx)) ([#&#8203;11006](https://redirect.github.com/jdx/mise/pull/11006) by [@&#8203;offbyone](https://redirect.github.com/offbyone))
- docker-compose switched from the aqua to the github backend ([#&#8203;10823](https://redirect.github.com/jdx/mise/pull/10823) by [@&#8203;konono](https://redirect.github.com/konono))
- eza switched from the asdf to the vfox backend ([#&#8203;11068](https://redirect.github.com/jdx/mise/pull/11068) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Documentation

- **templates:** clarify `exec` behavior in dry runs ([#&#8203;11018](https://redirect.github.com/jdx/mise/pull/11018) by [@&#8203;risu729](https://redirect.github.com/risu729))

#### New Contributors

- [@&#8203;Bartok9](https://redirect.github.com/Bartok9) made their first contribution in [#&#8203;11117](https://redirect.github.com/jdx/mise/pull/11117)
- [@&#8203;LukasKnuth](https://redirect.github.com/LukasKnuth) made their first contribution in [#&#8203;11110](https://redirect.github.com/jdx/mise/pull/11110)
- [@&#8203;jeremy](https://redirect.github.com/jeremy) made their first contribution in [#&#8203;11034](https://redirect.github.com/jdx/mise/pull/11034)
- [@&#8203;Turbo87](https://redirect.github.com/Turbo87) made their first contribution in [#&#8203;11011](https://redirect.github.com/jdx/mise/pull/11011)
- [@&#8203;coryzibell](https://redirect.github.com/coryzibell) made their first contribution in [#&#8203;11019](https://redirect.github.com/jdx/mise/pull/11019)

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.7.7...v2026.7.11>

#### 💚 Sponsor mise

mise is maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer for [**entire.io**](https://entire.io), the title sponsor of the [jdx.dev](https://jdx.dev) open source tools. Development is funded by sponsors.

If mise saves you or your team time, please consider sponsoring at [jdx.dev](https://jdx.dev/sponsors.html). Individual and company sponsorships keep mise fast, free, and independent.

### [`v2026.7.7`](https://redirect.github.com/jdx/mise/releases/tag/v2026.7.7): : Monorepo deps, systemd timers, and OAuth reliability

[Compare Source](https://redirect.github.com/jdx/mise/compare/v2026.7.6...v2026.7.7)

This release extends dependency providers and systemd bootstrap to monorepo and timer-based workflows, and hardens GitHub OAuth token refreshes and cache clearing against concurrent mise processes.

#### Added

- **deps:** experimental `mise deps --monorepo` runs dependency providers across every explicitly configured `[monorepo].config_roots` entry, aligned with `mise install --monorepo`. Provider IDs are qualified by config root (e.g. `//apps/api:uv`) so repeated provider names across subprojects no longer collide, and mise installs the union of tools declared across participating roots before running providers. Plain `mise deps` remains scoped to the current config root. ([#&#8203;10975](https://redirect.github.com/jdx/mise/pull/10975) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  monorepo_root = true

  [monorepo]
  config_roots = ["apps/*", "packages/*"]
  ```

  ```bash
  mise deps --monorepo
  mise deps --monorepo --only //apps/api:uv
  ```
- **bootstrap:** manage systemd user timers alongside services via `[bootstrap.linux.systemd.units]`. Timer entries render to `.timer` units with scheduling (`on_boot_sec`, `on_unit_active_sec`, `on_unit_inactive_sec`, `on_calendar`), persistence, and `timers.target` linkage. Services gain optional directives including `type`, `remain_after_exit`, `exec_stop`, start/stop timeouts, `no_new_privileges`, and `private_tmp`. When a unit name switches between service and timer, the stale sibling unit is stopped, disabled, and removed on apply. ([#&#8203;10984](https://redirect.github.com/jdx/mise/pull/10984) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Fixed

- **github:** serialize OAuth token refreshes across mise processes. Single-use refresh tokens were only guarded by an in-process mutex, so parallel mise runs could burn the same token or overwrite the cache with stale data. The full read-refresh-write cycle now holds a filesystem lock, re-reads the cache after acquiring it, and writes atomically. Rejected refreshes now surface a sanitized error with reauthorization guidance instead of falling back silently to anonymous requests. ([#&#8203;10995](https://redirect.github.com/jdx/mise/pull/10995) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **cache:** `mise cache clear` no longer fails with `DirectoryNotEmpty` when another mise process (e.g. `hook-env`) recreates cache files mid-removal. Removal now retries with bounded backoff and tolerates concurrently recreated entries, while permission and other errors still propagate. ([#&#8203;10993](https://redirect.github.com/jdx/mise/pull/10993) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **github:** skip OS package and installer assets (`.apk`, `.deb`, `.rpm`, DMG/PKG, MSI/MSIX/AppX and sidecars) during automatic asset selection for the `github:`, `gitlab:`, and `forgejo:` backends, so releases containing only unsupported packages now report that no platform asset matches instead of installing an unusable file. Expli

> ✂ **Note**
> 
> PR body was truncated to here.


</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yODAuMCIsInVwZGF0ZWRJblZlciI6IjQzLjI4MC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-07-27 07:46:49 -07:00
renovate[bot] 211232d254 chore(deps): update dependency jscodeshift to v17.4.0 (#666) 2026-07-27 07:46:25 -07:00
renovate[bot] 7d1e567cc4 chore(deps): update postgres:18 docker digest to 3a82e1f (#657) 2026-07-22 10:34:32 -07:00
renovate[bot] 0fb7ba1a36 chore(deps): update redis docker digest to 234c902 (#658) 2026-07-22 10:34:20 -07:00
renovate[bot] 64ef6556d0 chore(deps): update dependency @tsconfig/bases to v1.0.26 (#659) 2026-07-22 10:34:06 -07:00
renovate[bot] f7e1739440 chore(deps): update dependency @types/supertest to v7.2.1 (#660) 2026-07-22 10:33:46 -07:00
Will Schurman 🦦 0dd046d043 chore: update command for release process (#655) 2026-07-13 12:56:04 -07:00
renovate[bot] e6fbffcfee chore(deps): update postgres:18 docker digest to 22c89fe (#651) 2026-07-13 10:14:40 -07:00
renovate[bot] 0619bd1738 chore(deps): update dependency @types/node to v24.13.3 (#652) 2026-07-13 10:14:36 -07:00
renovate[bot] 2454e5b91a chore(deps): update dependency jdx/mise to v2026.7.5 (#653) 2026-07-13 10:14:31 -07:00
renovate[bot] a40f0f4574 chore(deps): update dependency typedoc to v0.28.20 (#654) 2026-07-13 10:14:27 -07:00
Will Schurman 2e8707dd32 v0.65.0 2026-07-10 13:03:50 -07:00
Gabe Debes df75910cbd feat: accept array of parent entity classes in cascade deletion rule (#650) 2026-07-09 16:46:02 -07:00
renovate[bot] 06688ff789 chore(deps): update jest (#649) 2026-07-06 09:40:51 -07:00
renovate[bot] 957387b0d3 chore(deps): update dependency jdx/mise to v2026.7.0 (#648) 2026-07-06 09:39:34 -07:00
renovate[bot] cd353dd058 chore(deps): update dependency @types/node to v24.13.2 (#647) 2026-07-06 09:39:14 -07:00
renovate[bot] b604ff7bf2 chore(deps): update dependency uuid to v14.0.1 (#646) 2026-07-06 09:38:47 -07:00
renovate[bot] 795c231dae chore(deps): update postgres:18 docker digest to 4aabea7 (#644) 2026-06-29 08:34:29 -07:00
renovate[bot] f8eaf094b6 chore(deps): update redis docker digest to 2838d55 (#645) 2026-06-29 08:34:18 -07:00
Will Schurman 🦦 f4201afa86 chore: update renovate groups for new linter (#643) 2026-06-22 10:10:49 -07:00
renovate[bot] 4e07e761c0 chore(deps): update dependency oxfmt to v0.55.0 (#641)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-22 09:22:40 -07:00
renovate[bot] ab0eb5525f chore(deps): update dependency pg to v8.22.0 (#642)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-22 09:20:35 -07:00
renovate[bot] a5e1738f5f chore(deps): update ci workflows (#639) 2026-06-22 09:19:49 -07:00
renovate[bot] 9b8427a211 chore(deps): update dependency ioredis to v5.11.1 (#640) 2026-06-22 09:18:35 -07:00
renovate[bot] 4badcf8a77 chore(deps): update redis docker digest to 0a97239 (#636) 2026-06-15 08:39:47 -07:00
renovate[bot] f0c01e4414 chore(deps): update ci workflows to v2026.6.10 (#637) 2026-06-15 08:39:28 -07:00
renovate[bot] d8a6c1d8bd chore(deps): update dependency prettier to v3.8.4 (#638) 2026-06-15 08:39:07 -07:00
renovate[bot] abe20b9168 chore(deps): update postgres:18 docker digest to 29ee7bb (#635) 2026-06-15 08:38:52 -07:00
renovate[bot] 1c17dc0f37 fix(deps): update dependency koa to v3.2.1 (#634) 2026-06-08 08:44:45 -07:00
renovate[bot] 5bd968f698 chore(deps): update dependency @tsconfig/bases to v1.0.25 (#632) 2026-06-08 08:34:20 -07:00
renovate[bot] 55be94ba64 chore(deps): update dependency ctix to v2.8.1 (#633) 2026-06-08 08:33:44 -07:00
renovate[bot] 2063fc715c chore(deps): update ci workflows (#631) 2026-06-08 08:21:13 -07:00
renovate[bot] 4454602f22 chore(deps): update dependency jdx/mise to v2026.5.18 (#630) 2026-06-01 14:54:22 -07:00
renovate[bot] 42a7912dc5 chore(deps): update redis docker digest to aa049e6 (#629) 2026-06-01 14:54:13 -07:00
renovate[bot] dd8c710e82 chore(deps): update dependency @isaacs/ttlcache to v2.1.5 (#625) 2026-05-25 09:54:24 -07:00
renovate[bot] 2912764a6a chore(deps): update redis docker digest to 4d25e2f (#624) 2026-05-25 09:39:50 -07:00
renovate[bot] 6879bbde9e chore(deps): update dependency @types/koa to v3.0.3 (#626) 2026-05-25 09:39:30 -07:00
renovate[bot] e89e8f7b6d chore(deps): update dependency jdx/mise to v2026.5.15 (#627) 2026-05-25 09:38:55 -07:00
Will Schurman 🦦 d5a36d1748 chore: upgrade oxlint and add rules for previous eslint rules (#623) 2026-05-22 11:09:08 -07:00
Will Schurman 3f10a9e70e v0.64.0 2026-05-22 08:42:44 -07:00
Will Schurman 🦦 5561a6fcae fix: correctness of TSMockitoExtensions isEqualWithEntityAware for SerializableKeyMaps of different length (#614)
# Why

A bug exists where the ts-mockito extension entity aware matcher would report map equality even when the maps were of different lengths (assuming shorter map had all the same elements as first part of longer map).

# How

Fix by preemptively checking length. Also fix recursive call to use `isEqualWithEntityAware` in case nested objects need it.

# Test Plan

Run updated test.
2026-05-21 10:40:28 -07:00
Will Schurman 🦦 f300d8e55a fix: fix possible composite key collision in StubDatabaseAdapter test utility (#613)
# Why

The stub adapters were doing a simple `:` join, so it was possible for two rows to collide in the stub adapter during tests, if value has the delimiter.

# How

Fix is to use JSON.stringify so that only truly same rows map to the same thing.

# Test Plan

Run new tests.
2026-05-21 10:37:27 -07:00
Will Schurman 🦦 ecf1608f7b chore: update comment for IEntityMetricsAdapter for implementors (#612)
# Why

Just a comment update to make it clearer that IEntityMetricsAdapter implementations must not throw since it'll cause adverse effects for the library.

In a follow-up, it may be good to defend against this, but it's not a straightforward fix.

# How

Update comment.

# Test Plan

Proofread.
2026-05-21 10:34:33 -07:00
Will Schurman 🦦 0eb8989c49 fix: pick method crash when object has hasOwnProperty key (#611)
# Why

`pick` was calling `object.hasOwnProperty(prop)`, but if the object had a property `hasOwnProperty`, it'd be used.

# How

Fix is to use the Object prototype's method.

# Test Plan

Run added test.
2026-05-21 10:31:30 -07:00
Will Schurman 🦦 e8299e2db6 fix: fix behavior of AuthorizationResultBasedEntityAssociationLoader when field value is empty string (#610)
# Why

The bug is that if the column in the current entity that contains the referenced entity's `id` is an empty string, the load method would erroneously return null. This correct the behavior to correctly try to load the entity by ID, which correctly fails since the field is invalid.

# How

Fix check.

# Test Plan

Add new test.
2026-05-21 10:28:28 -07:00
Will Schurman 🦦 292d89167e feat: support numeric enums in StrictEnumField (#609)
# Why

Typescript enums come in two flavors: https://www.typescriptlang.org/docs/handbook/enums.html

Previously, the validation here only worked for string enums.

# How

Update logic to make it work for both string and numeric enums.

# Test Plan

Run new test.
2026-05-21 10:25:28 -07:00
Will Schurman 🦦 20219752b9 feat: validate GenericRedisCacheContext (#608)
# Why

#601 exposed another validation gap in assumptions made about cache keys. We need certain assumptions to be true, namely:
- delimiter is nonzero-length (otherwise issue #601 fixes still would exist)
- delimiter doesn't include escape character (otherwise escape breaks)
- ttlSecondsPositive and negative are positive integers
- cacheKeyVersionsToInvalidateFn returns > 0 versions (otherwise silently doesn't invalidate)

# How

Add validation.

# Test Plan

Add test.
2026-05-21 10:22:29 -07:00
Will Schurman 🦦 717d20367f fix: skip when AllowIfAllSubRulesAllowPrivacyPolicyRule is empty (#607)
# Why

In places where a rule list is dynamically composed, it's likely possible for this list to be empty. Due to the way it's written, it would erroneously allow in this case.

# How

Add check for case and skip.

# Test Plan

Add test.
2026-05-21 10:19:32 -07:00
Will Schurman 🦦 46072a37dd fix: correct cycle guard behavior for deletion (#606)
# Why

There is a bug in self-referential (or transitively-self-referential) cascade cycle detection.

From claude:
```
In AuthorizationResultBasedDeleteMutator:

- deleteInternalAsync (line 802) runs the authorize check, calls
processEntityDeletionForInboundEdgesAsync, runs validators/triggers, deletes the row, and schedules
invalidation callbacks and afterCommit triggers.
- The cycle guard is at the top of processEntityDeletionForInboundEdgesAsync (line 932) — if
(processedEntityIdentifiers.has(entity.getUniqueIdentifier())) return;.

Trace a cycle (A↔B cascade-delete):
1. deleteInternalAsync(A, visited=∅) → runs authorize+triggers+DB delete for A →
processEntityDeletionForInboundEdgesAsync(A, visited=∅), marks A visited, finds B, calls
deleteInternalAsync(B, visited={A}).
2. deleteInternalAsync(B, visited={A}) → runs authorize+triggers+DB delete for B →
processEntityDeletionForInboundEdgesAsync(B, visited={A}), marks B visited, finds A, calls deleteInternalAsync(A,
visited={A,B}).
3. deleteInternalAsync(A, visited={A,B}) → runs authorize+triggers+DB delete for A again → only when it gets into
processEntityDeletionForInboundEdgesAsync does the visited check finally short-circuit.

So beforeDelete/afterDelete/beforeAll/afterAll, the database delete, the post-commit invalidation, and
afterCommit triggers all fire twice for A. The DB delete is idempotent (row's already gone) but triggers
with side effects (emit events, write audit rows, etc.) double-fire.
```

# How

Fix is to move cycle detection to to of `deleteInternalAsync`, thus guarding all checks for already-run entities.

# Test Plan

- Run new test
- Existing test was passing due a stale entity being passed into deleter. The why is a bit nuanced. Here's claude:

```
The original test setup

  const categoryA = await CategoryEntity.creator(viewerContext).createAsync();
  const categoryB = await CategoryEntity.creator(viewerContext)
    .setField('parent_category_id', categoryA.getID())
    .createAsync();
  await CategoryEntity.updater(categoryA)
    .setField('parent_category_id', categoryB.getID())
    .updateAsync();

  After these three statements, the database state is:
  - A: {id: uuidA, parent_category_id: uuidB} (after update)
  - B: {id: uuidB, parent_category_id: uuidA}

  But the local variable categoryA still holds the original entity instance from line 1, where
  parent_category_id was null. updateAsync() returns a new entity, but that return value is discarded — the
  local categoryA was never rebound. So categoryA.getAllDatabaseFields() in the test returns {id: uuidA,
  parent_category_id: null}.

  Why the test passed before the fix

  When the test calls CategoryEntity.deleter(categoryA).deleteAsync(), the outermost mutator's this.entity
  is the stale categoryA (with parent_category_id = null).

  Trace through the OLD cycle behavior for CASCADE_DELETE_INVALIDATE_CACHE_ONLY (so skipDatabaseDeletion =
  true on cascade calls):

  1. Outer A mutator runs. this.entity = stale A. Traverses inbound edges of A, finds B.
  2. Middle B mutator runs (cascade). this.entity = freshly-loaded B from DB (correct fields). Traverses
  inbound edges of B, finds A → cascades to A again.
  3. Inner A mutator runs (cycle re-entry). this.entity = freshly-loaded A from DB, with parent_category_id
  = uuidB (the post-update value).

  3. Under the OLD code, the cycle guard at the top of processEntityDeletionForInboundEdgesAsync stops the
  traversal from recursing further, but the inner deleteInternalAsync still runs validators, triggers, and
  registers a post-commit invalidation callback using this freshly-loaded A's fields.

  After commit, the post-commit invalidation callbacks fire:
  - Outer A registers invalidation for {id: uuidA, parent_category_id: null} → invalidates (id, uuidA) only
  (null isn't a cache key).
  - Middle B registers invalidation for {id: uuidB, parent_category_id: uuidA} → invalidates (id, uuidB) and
   (parent_category_id, uuidA).
  - Inner A registers invalidation for {id: uuidA, parent_category_id: uuidB} → invalidates (id, uuidA) and
  (parent_category_id, uuidB).

  The test asserts that (parent_category_id, uuidB) is invalidated, and it was — because of the inner A's
  fresh-loaded fields. The OLD bug (running deletion side effects twice) was accidentally compensating for
  the stale entity reference held by the test.

  Why the test failed after the fix

  With the cycle guard moved to the top of deleteInternalAsync, the inner A call returns immediately without
   registering any invalidation. Only the outer A's invalidation runs, with the stale parent_category_id =
  null, so (parent_category_id, uuidB) is never invalidated → still HIT → assertion fails.

  This isn't a regression in real-world usage. In real code, callers don't keep stale references after
  updating, because:
  - The idiomatic pattern is const x = await Entity.updater(x)...updateAsync() (rebinding).
  - Non-cycle deletions of stale references already produced incomplete invalidation in the OLD code — the
  cascade re-load only happened to "rescue" the cycle path.

  After the fix, cycle and non-cycle paths behave consistently: the entity instance the caller hands to
  deleter() is the source of truth for what gets invalidated.

  The test edit

  const initialCategoryA = await CategoryEntity.creator(viewerContext).createAsync();
  const categoryB = await CategoryEntity.creator(viewerContext)
    .setField('parent_category_id', initialCategoryA.getID())
    .createAsync();
  const categoryA = await CategoryEntity.updater(initialCategoryA)
    .setField('parent_category_id', categoryB.getID())
    .updateAsync();

  categoryA now binds to the return value of updateAsync(), so it carries the post-update fields. The cycle
  behavior remains under test — the assertion that both (parent_category_id, uuidA) and (parent_category_id,
   uuidB) are MISS still verifies that the cascade correctly invalidated B's and A's cache keys. The fix to
  the test reflects correct API usage, and the cycle handling is independently confirmed by the new fires
  delete-side-effects exactly once per entity in a cycle test.
```
2026-05-21 10:16:27 -07:00
Will Schurman 🦦 4ac1868d98 fix: handle potential infinite recursion for canViewerDeleteAsync (#605)
# Why

In the same way that `processEntityDeletionForInboundEdgesAsync` has detection for reference cycles, this preemptive check utility method needs the same detection mechanism.

# How

Add the same detection mechanism as `processedEntityIdentifiers`

# Test Plan

Add test case for this that fails before change, succeeds after.
2026-05-21 10:13:27 -07:00
Will Schurman 🦦 b1ed31b185 feat: clean up transactional dataloader at transaction end (#604)
# Why

One way memory can grow unbounded within a single request is via transactional dataloaders. If a single request executes N transactions, each one accumulates it's own dataloader state.

Though this has the same semantics as regular dataloaders (which belong to companion provider which is scoped to request by application and discarded after response is sent), they can accumulate more.

# How

The fix is to clear the transactional dataloader at the end of the transaction (or nested transaction), no matter whether the transaction succeeded or rolled-back.

# Test Plan

Add test for condition.
2026-05-21 10:10:28 -07:00
Will Schurman 🦦 38ba558080 fix: correct negative caching behavior in GenericSecondaryEntityCache (#603)
# Why

Please describe the motivation for this PR, and link to relevant GitHub issues, forums posts, or feature requests.

# How

How did you build this feature or fix this bug?

# Test Plan

Please describe how you tested this change and how a reviewer could reproduce your test, especially if this PR does not include automated tests!
2026-05-21 10:07:28 -07:00
Will Schurman 🦦 a920ed0090 fix: update cache key generation for secondary caches (#602)
# Why

Similar to #601, it was possible for a misconfigured application to have collisions in cache keys for secondary caches. Because they likely share the same redis instance at the application level, these keys need similar treatment for separating them logically from normal loader keys plus consistent delimiter escaping.

# How

Hoist final key creation from application level to library level and apply consistent delimiter escaping and namespacing.

# Test Plan

Run new test.
2026-05-21 10:04:28 -07:00
Will Schurman 🦦 df49b12a2e fix!: replace makeKeyFn with delimiter to strengthen guarantee of proper escaping (#601)
# Why

Since cache key construction function is application-supplyable, there's potential that it doesn't escape properly, causing potential cache collisions.

# How

Change the API to only allow specifying the delimiter, and hoist code into the package to handle delimiter escape.

# Test Plan

Run new test to ensure no collisions on something that would've previously collided.
2026-05-21 10:01:28 -07:00
Will Schurman 🦦 22d7298c84 fix: prevent collision in companion when entities share same name or same table name (#600)
# Why

There's nothing preventing two entity classes from sharing the same class name. While not common in practice, this preemptively defends against the issue of collisions in companion maps.

Secondarily, table data coordinators could collide, even if they have different flavors of database and cache adapters. Since those are used to construct the coordinator, include their flavors in the map key.

# How

Update key generation code.

# Test Plan

Run new tests.
2026-05-21 09:58:28 -07:00
Will Schurman 🦦 5ef16f4340 chore: remove unnecessary ViewerScopedEntityMutatorFactory (#599)
# Why

ViewerScopedEntityMutatorFactory existed to make both the loader and mutator patterns more aligned. But it doesn't make as much sense for mutators since for update and delete, the viewerContext used is the one from the entity being updated/deleted instead of the one for the viewer scoped factory. So, in practice the one from the factory was only being used for create mutations.

The simplest change is to just remove the ViewerScopedEntityMutatorFactory to reduce confusion.

# How

Remove class.

# Test Plan

CI
2026-05-21 09:56:08 -07:00
renovate[bot] d0e15580cc chore(deps): update dependency ctix to v2.8.0 (#619) 2026-05-21 08:53:55 -07:00
renovate[bot] a7368a26e2 chore(deps): update dependency @tsconfig/bases to v1.0.24 (#620) 2026-05-21 08:49:21 -07:00
renovate[bot] 7c330773bb chore(deps): update dependency oxfmt to v0.50.0 (#621) 2026-05-21 08:48:17 -07:00
renovate[bot] 0e55f8066e chore(deps): update ci workflows (#618) 2026-05-21 08:44:00 -07:00
renovate[bot] 7786c266d9 chore(deps): update postgres:18 docker digest to 8ff36f3 (#616) 2026-05-21 08:43:23 -07:00
renovate[bot] a70b27a136 chore(deps): update dependency @types/node to v24.12.4 (#617) 2026-05-21 08:37:38 -07:00
renovate[bot] 9dda1bb981 fix(deps): update dependency @apollo/server to v5.5.1 (#598) 2026-05-11 08:15:31 -07:00
renovate[bot] 9c7c0de98f chore(deps): update dependency @types/node to v24.12.3 (#597) 2026-05-11 08:15:01 -07:00
renovate[bot] 05f9bcc6ce chore(deps): update redis docker digest to 0c34149 (#596) 2026-05-11 08:14:27 -07:00
renovate[bot] 0b1fdbe207 chore(deps): update dependency ip-address to v10.1.1 [security] (#594) 2026-05-11 08:14:17 -07:00
renovate[bot] 5fc76ead20 chore(deps): update postgres:18 docker digest to 7e32e98 (#595) 2026-05-11 08:14:07 -07:00
renovate[bot] 98eaaa0298 fix(deps): update dependency uuid to v14 [security] (#593) 2026-04-27 09:11:04 -07:00
renovate[bot] 797e33bd7b chore(deps): update dependency oxfmt to v0.46.0 (#592) 2026-04-27 08:29:38 -07:00
renovate[bot] 4c42f7be8f chore(deps): update postgres:18 docker digest to 7848165 (#589) 2026-04-27 08:26:27 -07:00
renovate[bot] 6add3fb767 chore(deps): update redis docker digest to 832d778 (#590) 2026-04-27 08:26:20 -07:00
renovate[bot] 7d7457a98c chore(deps): update dependency jdx/mise to v2026.4.24 (#591) 2026-04-27 08:25:59 -07:00
renovate[bot] fccf3eb0de chore(deps): update dependency oxlint to v1.60.0 (#588) 2026-04-20 09:34:10 -07:00
renovate[bot] 6ea3dae767 chore(deps): update dependency typedoc to v0.28.19 (#585) 2026-04-20 09:29:22 -07:00
renovate[bot] 604353645d chore(deps): update dependency oxfmt to v0.45.0 (#587) 2026-04-20 08:29:49 -07:00
renovate[bot] 7a3a239313 chore(deps): update ci workflows (#583) 2026-04-20 08:27:55 -07:00
renovate[bot] 36c58cfdf9 chore(deps): update dependency typescript to v6.0.3 (#586) 2026-04-20 08:27:39 -07:00
renovate[bot] a0bb5b9874 chore(deps): update dependency prettier to v3.8.3 (#584) 2026-04-20 08:26:34 -07:00
renovate[bot] 9d851edf9f chore(deps): lock file maintenance (#581) 2026-04-14 16:57:22 -07:00
Will Schurman 🦦 4519909be8 fix: upgrade knex and remove undefined value bindings (#582) 2026-04-14 16:46:07 -07:00
renovate[bot] 1365b11ccb chore(deps): update dependency typescript to v6 (#580) 2026-04-13 14:06:33 -07:00
renovate[bot] 226e1bfb98 chore(deps): update codecov/codecov-action action to v6 (#579) 2026-04-13 13:20:24 -07:00
renovate[bot] b4ed49b289 chore(deps): update dependency oxlint-config-universe to v0.0.3 (#574) 2026-04-13 13:10:00 -07:00
renovate[bot] 5df53a0a7f chore(deps): update dependency prettier to v3.8.2 (#575) 2026-04-13 13:09:51 -07:00
renovate[bot] b8a1f97ea8 chore(deps): update dependency jdx/mise to v2026.4.11 (#573) 2026-04-13 13:09:27 -07:00
renovate[bot] fbd06e4368 chore(deps): update dependency oxfmt to v0.44.0 (#576) 2026-04-13 13:08:56 -07:00
renovate[bot] 4c569b6012 chore(deps): update dependency oxlint-tsgolint to v0.20.0 (#577) 2026-04-13 13:07:30 -07:00
renovate[bot] f039e71041 fix(deps): update dependency koa to v3.2.0 (#578) 2026-04-13 13:06:46 -07:00
renovate[bot] f60b439939 chore(deps): pin dependencies (#569) 2026-04-13 10:56:13 -07:00
renovate[bot] 570ef6f37d chore(deps): update postgres:18 docker digest to 52e6ffd (#570) 2026-04-13 10:55:56 -07:00
renovate[bot] 26a45d3489 chore(deps): update redis docker digest to 1f07381 (#571) 2026-04-13 10:55:45 -07:00
renovate[bot] 976f6e6434 chore(deps): update dependency @types/node to v24.12.2 (#572) 2026-04-13 10:55:34 -07:00
Will Schurman 🦦 9a4f08e31a feat: add count equivalent for knex loader methods (#565) 2026-04-10 17:45:57 -07:00
Will Schurman 🦦 2240e94d08 chore: use oxfmt for import ordering (#564)
# Why

Part 2 of #559.

# How

Use sortImports, and update lint to also run format for eslint parity.

# Test Plan

`yarn lint`
2026-04-08 15:02:23 -07:00
Will Schurman 🦦 a41a95b38d chore: switch to oxfmt/oxlint (#559)
# Why

oxfmt/oxlint has near parity with prettier/eslint but is about 10x faster (10s -> 1s). 

This uses `oxlint-config-universe` equivalent of `eslint-config-universe` as the base. There are known differences between eslint and oxlint, documented in the readme of `oxlint-config-universe`.

The missing rules which we can live without temporarily in exchange for the speedups:
- `naming-convention` (async suffix, PascalCase, UPPER_CASE)
- `no-restricted-properties` (.only/.skip)
- `no-restricted-exports` (no default exports)
- `tsdoc/syntax`
- object-shorthand
- no-dupe-args, no-octal, no-octal-escape
- n/no-deprecated-api

I've created a task to track: #567.

# How

Have claude convert. Use `oxlint-config-universe` and manually audit resulting config against old eslint config. Note down known differences in #567.

In stacked PR, use oxfmt for import ordering as suggested by readme in `oxlint-config-universe`

# Test Plan

`yarn lint`
2026-04-08 14:59:26 -07:00
renovate[bot] 9b642b31c8 fix(deps): update dependency @koa/router to v15.4.0 (#563) 2026-04-06 11:13:03 -07:00
renovate[bot] 64b646786b chore(deps): update dependency jdx/mise to v2026.4.5 (#561) 2026-04-06 11:11:59 -07:00
renovate[bot] 623ab6cf8a chore(deps): update yarn to v4.13.0 (#562) 2026-04-06 11:11:02 -07:00
renovate[bot] 5988712332 fix(deps): update dependency graphql to v16.13.2 (#560) 2026-04-06 11:10:45 -07:00
renovate[bot] 1ed43ddaa5 chore(deps): update dependency lodash-es to v4.18.1 [security] (#557) 2026-04-06 11:10:16 -07:00
James Ide dbd1cc8479 v0.63.0 2026-04-02 17:46:56 -07:00
Will Schurman 🦦 977b790ba7 feat!: remove deprecated loadManyByRawWhereClauseAsync (#556) 2026-04-02 08:11:41 -07:00
Will Schurman 🦦 2693fbc000 fix: remove unnecessary RETURNING * from database adapter update (#555) 2026-04-02 08:11:24 -07:00
renovate[bot] 4709f53b45 chore(deps): update redis docker digest to 009cc37 (#548) 2026-03-30 10:40:28 -07:00
renovate[bot] f4496b9cc7 chore(deps): update ci workflows (#549) 2026-03-30 10:40:09 -07:00
renovate[bot] 15ebe7d584 chore(deps): update dependency ioredis to v5.10.1 (#550) 2026-03-30 10:39:59 -07:00
renovate[bot] 0d00605a93 chore(deps): update dependency lerna to v9.0.7 (#551) 2026-03-30 10:39:38 -07:00
renovate[bot] 73d45fdca3 chore(deps): update node.js to v24.14.1 (#553) 2026-03-30 10:38:40 -07:00
renovate[bot] 3421a746be chore(deps): update dependency typedoc to v0.28.18 (#552) 2026-03-30 10:38:28 -07:00
renovate[bot] 5ae965b736 fix(deps): update dependency @apollo/server to v5.5.0 [security] (#547) 2026-03-27 10:41:59 -07:00
David Mokos bfa703e4ed fix: expose fieldName and fieldValue on EntityInvalidFieldValueError (#546) 2026-03-27 04:58:19 -07:00
Will Schurman 🦦 174d6c602b feat: expose full transaction config on EntityTransactionalQueryContext (#545) 2026-03-25 09:06:27 -07:00
renovate[bot] 0c0c6a3496 chore(deps): update redis docker digest to 315270d (#542) 2026-03-23 09:30:28 -07:00
renovate[bot] 3f103e1dbd chore(deps): update postgres:18 docker digest to a9abf42 (#541) 2026-03-23 09:29:54 -07:00
renovate[bot] d90dce72c2 chore(deps): update ci workflows (#543) 2026-03-23 09:29:21 -07:00
renovate[bot] d800024283 chore(deps): update dependency @types/koa to v3.0.2 (#544) 2026-03-23 09:29:06 -07:00
Will Schurman 🦦 b6c950900d chore: add docblocks for some things (#539) 2026-03-18 16:03:34 -07:00
Will Schurman 4965cc2388 v0.62.0 2026-03-16 11:45:34 -07:00
Will Schurman 🦦 e3cee07091 fix: only throw EntityDatabaseAdapterPaginationCursorInvalidError from cursor decoding logic (#537)
# Why

Noticed that this could throw a TypeError if the decoded JSON wasn't an object:

> TypeError: Cannot use 'in' operator to search for 'id' in blah

# How

Add check that this is an object type ahead of using `in` operator.

# Test Plan

Add new tests for this, run them.
2026-03-16 11:36:22 -07:00
Will Schurman 🦦 0a1d1b5972 fix: remove unused arg from EntityInvalidationUtils (#536)
# Why

This argument was unused, remove it.

# How

Was checking whether EntityInvalidationUtils was the same instance (or same behaviorally) across multiple entities backed by the same table (share a config, different TSelectedFields). Being able to remove this arg proves it since the other args aren't entity-type-specific and are only configuration-specitic.

# Test Plan

`yarn tsc`
2026-03-16 11:33:23 -07:00
renovate[bot] db887d3d7d chore(config): migrate Renovate config (#538) 2026-03-16 11:31:59 -07:00
Will Schurman 🦦 2214f7d19a chore: add docblocks for SQLExpression overload methods (#534)
# Why

Noticed while using these methods in the Expo server application that they were missing docblocks due to vscode preferring the docblock for the overload over the object property.

# How

This adds overload-specific docblocks to ensure they are present in vscode, and since they're overload-specific they are more accurate as well.

# Test Plan

`yarn tsc`
2026-03-16 11:29:57 -07:00
Will Schurman 🦦 4f4243d314 chore: update renovate to batch and schedule to decrease attention cost (#528) 2026-03-16 11:28:09 -07:00
renovate[bot] a505216608 chore(deps): lock file maintenance (#535) 2026-03-15 20:26:52 -07:00
renovate[bot] bc40501535 chore(deps): update dependency jdx/mise to v2026.3.9 (#532) 2026-03-15 11:34:32 -07:00
renovate[bot] 790e75160b chore(deps): update dependency lerna to v9.0.6 (#533) 2026-03-15 11:34:06 -07:00
renovate[bot] cd38019789 chore(deps): update jdx/mise-action action to v4 (#531) 2026-03-13 10:53:04 -07:00
renovate[bot] e1c47de3a7 chore(deps): update jest monorepo to v30.3.0 (#530) 2026-03-13 09:04:20 -07:00
Will Schurman a6f1dd7232 v0.61.0 2026-03-12 12:34:47 -07:00
Will Schurman 🦦 112d4dc9b3 feat: update the SQLExpression and SQLFragmentHelpers APIs (#516) 2026-03-11 16:06:29 -07:00
renovate[bot] f33d388b48 chore(deps): update dependency jdx/mise to v2026.3.8 (#529) 2026-03-11 16:05:28 -07:00
Nick Novitski f138841a59 chore: run jest without babel 2026-03-11 13:02:25 -07:00
Nick Novitski 090c046bbc chore: use combined @tsconfig/bases package 2026-03-11 08:29:23 -07:00
Nick Novitski 81906aed16 chore: Run tests with ESM 2026-03-11 08:25:40 -07:00
Nick Novitski ea9c45fade chore: replace lodash with lodash-es 2026-03-11 08:21:24 -07:00
Nick Novitski 2fc88cb204 chore!: ESM 2026-03-11 08:17:28 -07:00
Nick Novitski e4133a865e chore: generate barrel files with file endings 2026-03-11 08:14:24 -07:00
Nick Novitski f75ed7f749 chore: allow relative imports with ts file extension 2026-03-11 08:11:07 -07:00
renovate[bot] 6bfde96bc3 chore(deps): update dependency jdx/mise to v2026.3.7 (#527) 2026-03-10 18:43:42 -07:00
Nick Novitski 575d8545f7 chore: enable consistent-type-imports lint rule 2026-03-10 09:50:21 -07:00
Nick Novitski 1aae31b0bf chore: use @expo/nullthrows in tests 2026-03-09 17:37:34 -07:00
renovate[bot] 6cbe50a048 chore(deps): update dependency eslint to v9.39.4 (#525) 2026-03-09 16:59:01 -07:00
renovate[bot] 0b4385698c chore(deps): update dependency jdx/mise to v2026.3.6 (#515) 2026-03-09 14:39:22 -07:00
renovate[bot] 1a8535f983 fix(deps): update dependency graphql to v16.13.1 (#510) 2026-03-09 11:18:02 -07:00
renovate[bot] 1f66b9f8f3 chore(deps): update dependency @types/node to v24.12.0 (#512) 2026-03-09 11:17:08 -07:00
renovate[bot] 6a07e6bd66 chore(deps): update dependency pg to v8.20.0 (#511) 2026-03-09 11:16:20 -07:00
Nick Novitski 9202ecbb23 chore: remove @types/uuid package
This is outdated; `uuid` ships with types.
2026-03-09 11:06:52 -07:00
Nick Novitski a3085ed911 Fix renovate updates
Mise can manage yarn versions, but the free renovate doesn't know to use
that configuration.  After this commit, the yarn version is set by the
packageManager field in the root package.json file, and downloaded by
corepack in all environments.
2026-03-09 11:06:33 -07:00
renovate[bot] 934e5f3199 chore(deps): update dependency jdx/mise to v2026.3.5 (#509) 2026-03-08 22:20:26 -07:00
Will Schurman e36dc81bd2 v0.60.0 2026-03-06 09:24:05 -08:00
renovate[bot] 2fc9d9c196 chore(deps): update jdx/mise-action action to v3.6.3 (#508) 2026-03-06 09:22:15 -08:00
Will Schurman 🦦 535b6811e0 feat: Add SQLExpression class that supports fluent comparison method API (#506) 2026-03-06 08:58:32 -08:00
renovate[bot] 792c6c851b chore(deps): update dependency ctix to v2.7.5 (#507)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [ctix](https://imjuni.github.io/ctix) ([source](https://redirect.github.com/imjuni/ctix)) | [`2.7.2` → `2.7.5`](https://renovatebot.com/diffs/npm/ctix/2.7.2/2.7.5) | ![age](https://developer.mend.io/api/mc/badges/age/npm/ctix/2.7.5?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/ctix/2.7.2/2.7.5?slim=true) |

---

### Release Notes

<details>
<summary>imjuni/ctix (ctix)</summary>

### [`v2.7.5`](https://redirect.github.com/imjuni/ctix/releases/tag/v2.7.5)

[Compare Source](https://redirect.github.com/imjuni/ctix/compare/v2.7.4...v2.7.5)

#### What's Changed

- fix(creating): correct exclusion filter logic in filename processing by [@&#8203;imjuni](https://redirect.github.com/imjuni) in [#&#8203;190](https://redirect.github.com/imjuni/ctix/pull/190)
- chore(version): bump version to 2.7.5 by [@&#8203;imjuni](https://redirect.github.com/imjuni) in [#&#8203;191](https://redirect.github.com/imjuni/ctix/pull/191)

**Full Changelog**: <https://github.com/imjuni/ctix/compare/v2.7.4...v2.7.5>

### [`v2.7.4`](https://redirect.github.com/imjuni/ctix/releases/tag/v2.7.4)

#### What's Changed

- fix(creating): resolve duplicate content in index.ts files during create mode by [@&#8203;imjuni](https://redirect.github.com/imjuni) in [#&#8203;185](https://redirect.github.com/imjuni/ctix/pull/185)
- chore(version): bump version to 2.7.3 by [@&#8203;imjuni](https://redirect.github.com/imjuni) in [#&#8203;186](https://redirect.github.com/imjuni/ctix/pull/186)
- fix: resolve duplicate exports and case sensitivity issues by [@&#8203;imjuni](https://redirect.github.com/imjuni) in [#&#8203;187](https://redirect.github.com/imjuni/ctix/pull/187)
- feat(config): remove index.ts file to streamline exports by [@&#8203;imjuni](https://redirect.github.com/imjuni) in [#&#8203;188](https://redirect.github.com/imjuni/ctix/pull/188)
- chore(version): bump version to 2.7.4 by [@&#8203;imjuni](https://redirect.github.com/imjuni) in [#&#8203;189](https://redirect.github.com/imjuni/ctix/pull/189)

**Full Changelog**: <https://github.com/imjuni/ctix/compare/v2.7.2...v2.7.4>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My41Ni4wIiwidXBkYXRlZEluVmVyIjoiNDMuNTYuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2026-03-05 14:19:37 -08:00
renovate[bot] 038b2bff76 chore(deps): update dependency jdx/mise to v2026.3.3 (#505) 2026-03-04 14:57:09 -08:00
renovate[bot] 599fccaf73 chore(deps): update dependency jdx/mise to v2026.3.2 (#504) 2026-03-04 09:10:14 -08:00
renovate[bot] c361dfd7ad chore(deps): update dependency @tsconfig/node24 to v24.0.4 (#503) 2026-03-03 22:26:43 -08:00
renovate[bot] 41bdcf464e chore(deps): update jdx/mise-action action to v3.6.2 (#500) 2026-03-03 22:11:50 -08:00
renovate[bot] a07429fa98 chore(deps): update node.js to v24.14.0 (#501) 2026-03-03 22:11:35 -08:00
renovate[bot] a653ce137c chore(deps): update actions/setup-node action to v6.3.0 (#502) 2026-03-03 22:11:13 -08:00
Nick Novitski 7bea8fbccf chore: provide node and yarn with mise 2026-03-03 16:30:41 -08:00
Will Schurman bfe1aad035 v0.59.0 2026-03-03 14:33:29 -08:00
Will Schurman 🦦 b931daa9a0 feat: add support to SQLFragment for = ANY(?) (#498) 2026-03-03 14:15:00 -08:00
renovate[bot] 72b3c80d12 fix(deps): update dependency @koa/router to v15.3.1 (#497) 2026-03-03 08:51:43 -08:00
renovate[bot] 57e9e06601 chore(deps): update dependency lerna to v9.0.5 (#496) 2026-03-03 08:15:52 -08:00
renovate[bot] 82978f8364 chore(deps): update dependency @types/node to v24.11.0 (#495) 2026-03-03 08:14:02 -08:00
Will Schurman 7dab49a36d v0.58.0 2026-03-02 12:04:25 -08:00
Will Schurman 🦦 21f2f6f461 fix: escape all matching portions of parts of cacher cache keys (#486) 2026-03-02 10:02:17 -08:00
renovate[bot] 4126dcd8f2 chore(deps): lock file maintenance (#493) 2026-03-02 08:04:18 -08:00
renovate[bot] 6889178e60 chore(deps): update dependency ioredis to v5.10.0 (#494) 2026-03-02 07:54:41 -08:00
renovate[bot] a0d3d30265 chore(deps): update dependency @types/node to v24.10.15 (#491) 2026-03-02 07:54:05 -08:00
renovate[bot] d0825c0446 chore(deps): update redis docker digest to 1c054d5 (#492) 2026-03-02 07:51:59 -08:00
renovate[bot] e8440f11c0 chore(deps): update dependency eslint-plugin-tsdoc to v0.5.2 (#488) 2026-03-02 07:51:43 -08:00
renovate[bot] baa1674629 chore(deps): update dependency @types/supertest to v7 (#490) 2026-03-02 07:51:24 -08:00
renovate[bot] 25520c68e8 chore(deps): update dependency pg to v8.19.0 (#489) 2026-03-02 07:50:48 -08:00
renovate[bot] 905e8522b3 chore(deps): update postgres:18 docker digest to 69e8582 (#487) 2026-03-02 07:50:20 -08:00
Will Schurman 🦦 e263bd218c fix: fix some security review suggestions (#485) 2026-02-27 17:59:51 -08:00
Will Schurman e876cfb27b v0.57.0 2026-02-27 17:30:00 -08:00
Will Schurman b9db8a4717 v0.56.0 2026-02-27 17:27:31 -08:00
Will Schurman 🦦 459a5ff850 feat: add entityField SQL helper (#481)
# Why

Asked claude to do an API usability review and it noted that forcing the user to context-switch between entity fields and database fields for SQLFragments was an unfortunate pattern.

# How

This PR provides a SQLFragment helper, `entityField<TFields>('fieldName')` which does the conversion for the user so they don't need to look up the underlying database field.

Secondarily, this provides type safety to ensure SQLFragments for one entity aren't used for SQLFragments of another entity (unless types overlap via shared config).

# Test Plan

Add feature and tests and examples.
2026-02-27 15:44:23 -08:00
Will Schurman 🦦 beda973c0c chore: raw -> unsafeRaw (#480)
# Why

Asked claude to do a security review of entity-database-adapter-knex and renaming `raw` to `unsafeRaw` was one suggestion.

# How

Rename.

# Test Plan

`yarn tsc`
2026-02-27 15:40:10 -08:00
renovate[bot] b258dad2ed fix(deps): update dependency koa to v3.1.2 [security] (#484) 2026-02-27 09:28:08 -08:00
renovate[bot] 976e2c3386 fix(deps): update dependency graphql to v16.13.0 (#479) 2026-02-27 09:02:48 -08:00
Will Schurman 🦦 e32c4c2bcc fix: correct pagination behavior for standard pagination with descending ordering clauses (#477)
# Why

This bug was subtle but important, and can be demonstrated best by looking at the new test case added `performs forward pagination with descending order`.

The bug was that cursor comparison must be derived from both direction but also ASC/DESC of the order bys.

Example:
```
obj1(t=1)
obj2(t=2)
obj3(t=3)
obj4(t=4)

> loadPage(first: 2, orderBy: t DESC)
obj4
obj3

> loadPage(first: 2, orderBy: t DESC, after: obj3)
```

the issue is that the cursor comparison for this second load would be:
```
(t) > (obj3.t)
```
which yields only obj4, which is incorrect.

# How

The fix is to determine cursor comparator by both the order by direction and the pagination direction, thus making the cursor from the example above:
```
(t) < (obj3.t)
```

The second part of this is to require all orderBys to have the same ordering for pagination, otherwise this new assumption breaks since cursor comparison then would be unable to be done with a single comparator. While there are ways we can handle different order by directions, we'd have to not use a cursor which adds a massive amount of complexity.

A small other fix was to set the `id` tiebreaker column order to the same as the other orders.

# Test Plan

Add new test case.
2026-02-26 18:35:23 -08:00
Will Schurman 🦦 2c4c649a4b fix: correct precedence parentheses for SQLFragment joins (#476)
# Why

There's a bug in where clause construction, which used SQLFragmentHelpers.and: https://github.com/expo/entity/blob/main/packages/entity-database-adapter-knex/src/internal/EntityKnexDataManager.ts#L303

The bug is that given something like:
```
where: sql`account_id = ${accountId}`,
searchWhere: sql`something ILIKE term OR similarity(term, ...) OR ...`
```
it would produce
```
WHERE account_id = ${accountId} AND something ILIKE term OR similarity(term, ...) OR ...
```

which has the incorrect parentheses and therefore incorrect precedence.

This exposed more bugs in the precedence in `or`, `and`, `join`, etc methods.

# How

The fix is multifold:
- Make the public `join` method only be used for comma-separation joining. Rename it to `joinWithCommaSeparator`.
- Wrap each group in `or` and `and` in parentheses to ensure correct precedence.
    ```
    > SQLFragment.or(sql`account_id = ${accountId}`, sql`app_id = ${appId}`
    sql`(account_id = ${accountId}) OR (app_id = ${appId})`
    ```

# Test Plan

Run existing tests to ensure nothing breaks, update unit tests and inspect output SQL statements to ensure they are correct parentheses. Especially the `builds complex queries with multiple helpers` test case.
2026-02-26 18:31:23 -08:00
Will Schurman 🦦 898f71ce08 feat: add method to get pagination cursor for single entity (#475)
# Why

In a decent number of tests in the Expo server application, it's useful to test pagination from a specific point. It may also be useful for production code in an thus far unbeknownst way.

To do this, one must be able to generate the cursor given an entity. This PR adds such a method.

# How

Add the method that creates a cursor.

# Test Plan

Run the new test.
2026-02-26 18:27:23 -08:00
Will Schurman 🦦 90ae6e299b fix: correct discriminated union type for forward/backward pagination (#474)
# Why

When testing this downstream, I noticed that it was allowed in types to specify first & before / last & after which aren't supported in the pagination.

# How

Fix the types by being explicit about the other fields.

# Test Plan

`yarn tsc`
2026-02-26 18:23:23 -08:00
Will Schurman 🦦 53b0604152 feat: support nullable search fields with postgres transforms (#470)
# Why

#468 added nulls support for regular order bys, but explicitly disabled it for search pagination due to it adding quite a lot of complexity. It also fixed a bug where using a nullable field in pagination in general would break the cursor condition and omit rows (any tuple containing NULL would be dropped due to postgres behavior).

But applications still need to be able to search fields that are nullable, they just need to provide a mechanism for dictating how to cast them as non-nullable. This PR provides such a mechanism.

# How

Allow the user to specify a function to craft the search field SQLFragment from nullable fields. This is required to be a somewhat awkward shape since the library needs to retain control over the field name due to table aliasing in the cursor.

# Test Plan

Run new tests.
2026-02-26 18:19:23 -08:00
Will Schurman 🦦 018a40bc01 fix: Apply same viewer context subclass type mechanism to knexLoader and related methods (#472)
# Why

When these were installed extension functions, we could get away without the `TMViewerContext2` technique. But now that these are loader methods on the `PostgresEntity` subclass, we need this again (it's the same as Entity and ReadonlyEntity).

This type allows the typechecker to behave correctly when a subclass of ViewerContext is used.

# How

Add generic the same as ReadonlyEntity/Entity.

# Test Plan

`yarn tsc` in application that uses ViewerContext subclass and knexLoader.
2026-02-26 18:15:19 -08:00
renovate[bot] 6849468d15 chore(deps): update postgres:18 docker digest to 5558651 (#478) 2026-02-26 17:08:34 -08:00
renovate[bot] c60f4cd455 chore(deps): update dependency @types/lodash to v4.17.24 (#471) 2026-02-26 14:10:28 -08:00
renovate[bot] 820fe88217 chore(deps): update postgres:18 docker digest to a688ae5 (#473) 2026-02-26 14:10:03 -08:00
Will Schurman 🦦 6c0838dc57 feat: add support for nulls ordering in order by clauses (#468) 2026-02-25 15:15:15 -08:00
renovate[bot] 5c11b598c4 chore(deps): update redis docker digest to 5cb00b0 (#467) 2026-02-24 18:47:25 -08:00
Will Schurman 🦦 c946630a82 chore: deprecate loadManyByRawWhereClause (#460)
# Why

Now that `loadManyBySQL` supports all the features of this method (including raw order by), deprecate it.

# How

Mark as deprecated in docblock.

# Test Plan

`yarn tsc`
2026-02-24 16:16:23 -08:00
Will Schurman 🦦 15adeb88cf feat!: convert all raw order bys to use same mechanism and syntax (#459)
# Why

#457 added a new mechanism for specifying ORDER BY clauses using SQLFragment that aligns a lot better with the loader APIs than having separate `orderByRaw`/`orderByFragment` fields in the query modifiers.

# How

Replace all with the new consistent order by in order field, update code to handle that.

One neat side-effect is that we no longer need the hard-to-follow direction-specific ordering in deep methods like `buildTrigramExactMatchPriority` since now we can have those just produce a SQLFragment and reverse the ordering the same as all the other pagination methods.

# Test Plan

Run all tests.
2026-02-24 16:12:25 -08:00
Will Schurman 🦦 9940fcbed2 chore: SQLFragment.toDebugString -> getDebugString (#458)
# Why

Small change to the debug method for SQLFragment. Noticed claude made it a `get toDebugString`, which doesn't quite fit within the patterns in this repo.

# How

Rename to `getDebugString()` method for consistency.

# Test Plan

`yarn tsc`
2026-02-24 16:08:23 -08:00
Will Schurman 🦦 0615ffacff feat: Add raw sqlfragment orderBy capability for pagination (#457)
# Why

One missing piece from pagination is the ability to order by a SQL fragment. This is useful for cases like needing to apply postgres transforms or coalescing to the columns in the order by clause.

Note that this, in combination with `loadBySQL` will completely replace the need for `loadManyByRawWhereClause` (including it's orderByRaw variant), as well as `orderByFragment` in the new pagination code, which is done in a PR upstack.

# How

Make order by specifications either be able to use SQLFragment to specify the field selection but not the ASC/DESC since the framework still needs to retain control of those for pagination purposes.

# Test Plan

Run new added tests.
2026-02-24 16:04:31 -08:00
renovate[bot] 5f3f31bb7c chore(deps): update postgres:18 docker digest to 9b5bd94 (#466) 2026-02-24 16:03:20 -08:00
renovate[bot] 097ac65fc4 chore(deps): update redis docker digest to db01931 (#465) 2026-02-24 16:03:06 -08:00
Will Schurman 🦦 e71ae5fcba feat: Add authorization action to EntityPrivacyPolicyRuleEvaluationContext (#464) 2026-02-24 15:55:28 -08:00
renovate[bot] 3065f360db chore(deps): update node.js to v24.14.0 (#463) 2026-02-24 11:46:37 -08:00
renovate[bot] c3fe15e655 chore(deps): update redis docker digest to dc123d5 (#461) 2026-02-23 17:02:33 -08:00
renovate[bot] 2d41777be0 chore(deps): update dependency eslint to v9.39.3 (#456) 2026-02-23 07:40:49 -08:00
renovate[bot] 45da844a5c chore(deps): lock file maintenance (#455) 2026-02-23 07:36:40 -08:00
Will Schurman 🦦 4dc156c7a8 fix: explicitly document behavior when cursor entity no longer exists (#453)
# Why

The one downside of using id-based cursor pagination and subqueries (added in #422, #431) is that it risks the row referenced by the cursor being deleted between pagination requests. It's an edge case though to be clear. Encoding the full cursor alleviates this, but has it's own downsides since trigram similarity isn't encodable and neither are things like Date objects. It is still preferable to use ID-based cursors for all pagination.

But it becomes the library's responsibility to explicitly document what the behavior is when a row referenced by the cursor is no longer present. This PR does this.

# How

When the cursor row is no longer present, the tuple evaluates to `NULL`, which produces a empty page.

The alternative to this behavior is to run an id check query ahead of the pagination query, and throw an error if it doesn't exist, or even return the first page of data if it doesn't exist. Both of these are less optimal since they cause unexpected behavior during results consumption.

So we keep the behavior as is and explicitly document it.

# Test Plan

Run new tests.
2026-02-17 14:56:23 -08:00
Will Schurman 🦦 cc78029117 fix: eliminate some unnecessary any casts (#448)
# Why

Did a brief audit of `any` casts in the library and removed ones that are no longer needed or could be replaced with a better concrete type.

# How

Grep for `as any`, fix.

# Test Plan

`yarn tsc`
2026-02-17 14:52:24 -08:00
Will Schurman 🦦 517d57b5df fix!: change adaptor -> adapter for consistency (#452)
# Why

As part of #451, claude noticed that we used both american and british english variants of "adapter"/"adaptor". This makes them consistent by making them all "adapter".

# How

Update name. Technically this is a breaking change but the nature of the library allows `tsc` to catch it in all application code that uses the library.

# Test Plan

`yarn tsc`
2026-02-17 14:48:25 -08:00
Will Schurman 🦦 c933db1f8a chore: docblock improvements (#451)
# Why

Asked claude to audit the docblocks for incorrect or missing info. This is what it mentioned as being good improvements.

# How

Update docblocks.

# Test Plan

Proofread.
2026-02-17 14:44:24 -08:00
Will Schurman 🦦 d9377a290b fix: Reorganize constructionUtils to make non-public (#454)
# Why

In #410 we split out `EntityLoaderUtils` into two classes:
- EntityInvalidationUtils - functions for invalidating entity caches from application code when underlying data is mutated outside of the entity framework
- EntityConstructorUtils - functions for constructing and authorizing entities.

Originally, these were both public due to them being the same class. Now, with them separated, we can better restrict access to entity construction which should only be done within the framework itself.

# How

Make constructor utils private everywhere except EntityLoaderFactory, which is deep enough in the abstraction that it's not immediately usable by top-level entity APIs (static methods on Entity/ReadonlyEntity, loader classes).

# Test Plan

`yarn tsc`, CI
2026-02-17 14:40:57 -08:00
Will Schurman 🦦 457f58ad2c fix: update integration test to use BlahEntity.knexLoader syntax (#444)
# Why

Part 4 of #441. This reverts the test changes to use the better pattern, though both work equivalently and are both tested.

# How

Update tests.

# Test Plan

Run tests
2026-02-16 13:26:21 -08:00
Will Schurman 🦦 ed6b2e0553 fix: revert change to codemod now that Entity.knexLoader is supported again via subclass (#443)
# Why

Part 3 of the refactor from #441.

This reverts the codemod back to use the `BlahEntity.knexLoader(vc)` syntax.

# How

`git revert`

# Test Plan

Run the tests in codemod.
2026-02-16 13:22:22 -08:00
Will Schurman 🦦 40b409f6f8 feat: add PostgresEntity/ReadonlyPostgresEntity classes that expose knexLoader methods (#442)
# Why

This is part 2 of the refactor described in #441.

It adds subclasses of Entity and ReadonlyEntity that supply the static `knexLoader` methods to keep the ergonomics of the extension but without the monkey-patching.

# How

Add subclasses, add test.

# Test Plan

Run test.
2026-02-16 13:18:22 -08:00
Will Schurman 🦦 51d632da16 fix: refactor install method to instead be free functions (#441)
# Why

Asked claude extended thinking to analyze the `installExtensions` monkey-patching strategy we went with in  #410 and compare it to other typescript ORM libraries.

It said essentially that it was ok, but most modern libraries choose not to do it that way, and instead recommended "free functions" (TIL about that term) to instantiate the loaders. (`knexLoader(BlahEntity, vc)`)

Working with it even more, I came up with an even better method on top of free functions to keep the `BlahEntity.knexLoader(vc)` syntax. The strategy is to:
1. Convert to free functions (this PR).
2. Add `PostgresEntity`/`ReadonlyPostgresEntity` subclasses that application entities can extend that implement the static extension methods.
3. Revert the codemod change from this PR to keep the codemod to prefer the `BlahEntity.knexLoader(vc)` syntax since most applications should use the subclasses (though it's not strictly necessary if they prefer the free function at callsites).
4. Revert the codemod in the integration tests to express better syntax pattern.

# How

Claude did most of this, but broadly speaking the strategy is to have the free functions, and then instead of patching the prototypes/classes for singleton caching, use a WeakMap where the core package singleton is the key and the value is the new knex-specific object.

# Test Plan

Run all tests.
2026-02-16 13:14:21 -08:00
Will Schurman 🦦 3d4aa0e9ef fix: Add default for TSelectedFields for application-instantiated types (#438)
# Why

When testing an upgrade to this package in the Expo server application, noticed that this was a minor annoyance since TSelectedFields has a default in all other public API methods.

# How

Add default to be consistent.

# Test Plan

`yarn tsc`
2026-02-16 13:10:22 -08:00
Will Schurman 🦦 f5c728be56 fix: pass in augmentable classes to installation (#437)
# Why

An idea proposed by @ide in https://github.com/expo/entity/pull/410#pullrequestreview-3765893898.

This makes it more testable and clearer about what objects installations are "allowed" to augment.

# How

Pass in the classes explicitly.

# Test Plan

`yarn tsc`
2026-02-16 13:06:21 -08:00
renovate[bot] 285f22c461 chore(deps): update dependency typedoc to v0.28.17 (#449) 2026-02-16 13:03:44 -08:00
Will Schurman 🦦 3ec4b5f0cd feat: Add pagination max page size configuration (#436)
# Why

Currently, the Entity pagination system allows clients to request arbitrarily large page sizes using the `first` or `last` parameters in pagination queries. This can lead to:
- Performance degradation when clients request excessively large pages
- Potential memory issues on the server
- Unpredictable query response times
- Difficulty in capacity planning and resource allocation

By adding configurable max page size limits, we can:
- Protect backend services from resource exhaustion
- Ensure predictable performance characteristics
- Provide better control over API usage patterns
- Maintain backward compatibility (no limit by default)

# How

The implementation adds a `paginationMaxPageSize` configuration option to the PostgresEntityDatabaseAdapterProvider that flows through the adapter hierarchy:

1. **Configuration Interface**: Added `PostgresEntityDatabaseAdapterConfiguration` with an optional `paginationMaxPageSize` field
2. **Provider Level**: `PostgresEntityDatabaseAdapterProvider` constructor now accepts the configuration object
3. **Adapter Level**:
   - `BasePostgresEntityDatabaseAdapter` defines a `paginationMaxPageSize` getter (defaults to `undefined`)
   - `PostgresEntityDatabaseAdapter` overrides it to return the configured value
4. **Validation**: `EntityKnexDataManager.loadPageAsync` validates `first`/`last` parameters against the max page size before executing queries

The validation throws descriptive errors when limits are exceeded:
- `"first must not exceed maximum page size of {limit}"`
- `"last must not exceed maximum page size of {limit}"`

Usage example:
```typescript
const provider = new PostgresEntityDatabaseAdapterProvider({
  paginationMaxPageSize: 100
});
```

# Test Plan

Added comprehensive unit tests in `EntityKnexDataManager-test.ts`.
2026-02-16 13:02:21 -08:00
Will Schurman 🦦 f72cbcaf5a feat: Add ilike and trigram similarity search to pagination (#431)
# Why

This PR adds search capabilities to the existing cursor-based pagination system added in #422 which only supported orderBy ordering.

This adds:
1. Case-insensitive pattern matching (ILIKE) - Useful for basic text search across entity fields
2. Trigram similarity search - Provides fuzzy matching capabilities for more advanced search use cases like handling typos or finding similar text

This enables building user-facing search features while maintaining the benefits of cursor-based pagination (stable results, efficient queries).

# How

The implementation introduces a unified PaginationSpecification that supports three strategies:

  1. Standard pagination - The existing orderBy-based pagination
  2. ILIKE search - Pattern matching with automatic wildcard escaping
  3. Trigram search - PostgreSQL trigram similarity with configurable threshold

  Key implementation details:
  - Search terms are properly parameterized to prevent SQL injection
  - ILIKE special characters (%, _, \) are escaped to prevent pattern injection
  - Results maintain stable cursor-based pagination with proper ordering:
    - ILIKE: Ordered by search fields, then ID
    - Trigram: Ordered by exact match priority, similarity score, optional extra fields, then ID

# Test Plan

  The PR includes comprehensive test coverage:

  - Unit tests for both AuthorizationResultBasedKnexEntityLoader and
  EnforcingKnexEntityLoader
  - Integration tests covering:
    - Forward/backward pagination with search
    - Multi-field search
    - Case-insensitive matching
    - Trigram similarity with various thresholds
    - Cursor stability across pages
    - Edge cases (empty results, special characters)
    - Combined with WHERE clauses
    - Security validation ensuring proper escaping and parameterization

All existing tests pass, confirming backward compatibility for standard pagination when using the new API.
2026-02-16 12:58:24 -08:00
renovate[bot] fb720611f0 chore(deps): lock file maintenance (#447) 2026-02-15 23:33:12 -08:00
renovate[bot] 7b8a49c203 chore(deps): update dependency ioredis to v5.9.3 (#445) 2026-02-15 12:09:59 -08:00
renovate[bot] d97707a213 chore(deps): update dependency @types/node to v24.10.13 (#439) 2026-02-13 13:14:27 -08:00
renovate[bot] ced36081b8 chore(deps): update dependency lerna to v9.0.4 (#440) 2026-02-13 13:13:43 -08:00
renovate[bot] b511981a69 chore(deps): update postgres:18 docker digest to b6b4d0b (#435) 2026-02-12 18:03:05 -08:00
Will Schurman 🦦 58deffd426 feat: Add paginated loader to entity-database-adapter-knex (#422) 2026-02-12 11:49:06 -08:00
Will Schurman 🦦 56ec27d5f2 fix: move EntityPrivacyUtils back into core package (#429) 2026-02-11 19:20:26 -08:00
Will Schurman 🦦 be40bcffa6 fix: Type idField in EntityConfiguration as TIDField (#425)
# Why

Noticed this was mis-typed in a later PR, hoisted into this one.

# How

Update type of EntityConfiguration.idField to be `TIDField`, which is the most correct and precise type for the field. This will help later on when adding pagination ID field stuff.

The other changes to tests are fixes since these previously were underspecified and changing the field type exposed these missing types. (claude informed me of this)

# Test Plan

`yarn tsc`
2026-02-11 17:30:23 -08:00
Will Schurman 🦦 a9d09f42b6 fix: Use TSelectedFields for knex loader order by method (#424)
# Why

Noticed while creating an upcoming pagination PR that loaders should use `TSelectedFields` for `orderBy` clauses. This was an accidental omission when the selected fields concept was added.

Succinctly, one should only be able to order entities by their fields rather than by all their underlying table's fields.

# How

Create new orderBy clause type that lives at the loader level. It is fully compatible (subset or equal to) the database orderBy clause, so calling the data manager methods directly with the arguments is by design.

# Test Plan

`yarn tsc`
2026-02-11 17:26:14 -08:00
renovate[bot] a9980b2785 chore(deps): update dependency @types/node to v24.10.12 (#434) 2026-02-11 07:39:14 -08:00
renovate[bot] 7600c7e7d8 chore(deps): update redis docker digest to 7b6fb55 (#433) 2026-02-10 14:35:24 -08:00
renovate[bot] 31e226e22a chore(deps): update node.js to v24.13.1 (#432) 2026-02-10 09:50:20 -08:00
renovate[bot] 66297676e4 chore(deps): update dependency @types/node to v24.10.11 (#428) 2026-02-09 16:14:17 -08:00
Will Schurman 🦦 273d1df8ba feat!: Add sql template and loader method (#414)
# Why

This is the first new feature afforded by the changes in #407.

It adds a feature that is fairly common amongst typescript ORMs: sql via tagged templates for safe value interpolation into SQL statements.

# How

Quite a lot of ORMs have this:
- https://github.com/drizzle-team/drizzle-orm
- https://github.com/mikro-orm/mikro-orm
- https://github.com/blakeembrey/sql-template-tag
- https://github.com/andywer/squid
- https://github.com/Seb-C/kiss-orm

This adds support for it to entity knex.

See tests for examples, especially `PostgresEntityIntegration-test.ts`.

The idea is that this will replace `loadManyByRawWhereClauseAsync` over the course of a few versions.

# Test Plan

Full test coverage.
2026-02-09 16:00:24 -08:00
Will Schurman 🦦 aba0ce5b5f feat: add codemod for upcoming v0.55.0-v0.56.0 (#413)
# Why

This adds a codemod for #407-#410 changes.

# How

Replace `.loader` calls with `.knexLoader` where applicable.

# Test Plan

Run the test.
2026-02-09 15:56:23 -08:00
Will Schurman 🦦 eeccf594c1 feat: add entity-database-adapter-knex-testing-utils containing StubPostgresDatabaseAdapter (#412)
# Why

As of moving raw and conjunction loading into the `entity-database-adapter-knex` package, the stub database adapater is no longer as useful in applications that use the package (it used to support conjunction loading synthetically for unit tests).

# How

This PR adds a new package, `entity-database-adapter-knex-testing-utils` that provides what StubDatabaseAdapter used to be (it has conjunction loading).

The idea is that applications that use `entity-database-adapter-knex` will use this package for their stub database needs and unit test companion provider.

# Test Plan

Full test coverage.
2026-02-09 15:52:23 -08:00
Will Schurman 🦦 e76264c613 chore: add CLAUDE.md and disallow test.skip (#411)
# Why

This commits the claude prompts I have been using to pair program with claude while building this stack.

I also noticed that claude loved to add `test.skip` to make things pass, which now is disallowed as of this PR.

# How

Commit the file.

# Test Plan

yarn lint
2026-02-09 15:48:23 -08:00
Will Schurman 🦦 6fce3a7604 feat!: move knex loaders into entity-database-adapter-knex package (#410)
# Why

Part 3 of #407.

# How

This is the main PR of the stack. It creates a mechanism for "installing" the extension methods on required objects, and then installs them and tests them.

The goal is to make it better self-contained about what is core in the library and guaranteed to be efficient (dataloader'd and cached) and what is not (raw SQL and SQLFragment loads in a future PR).

This is becoming more critical in a time of agentic coding where having a discrete set of loaders that can be linted separately and instruct agents to run `EXPLAIN ANALYZE` in raw cases.

# Test Plan

Full test coverage.
2026-02-09 15:44:24 -08:00
Will Schurman 🦦 567bb61172 chore: move knex-specific loader logic out of EntityDataManager (#408)
# Why

Part 2 of #407.

# How

Refactor methods into a separate class for future move out of main package.

# Test Plan

Full test coverage.
2026-02-09 15:40:23 -08:00
Will Schurman 🦦 2590b95734 feat!: Move knex-specific loader methods to knexLoader (#407)
# Why

This is a long stack, but the goal is to make a place to put postgres-specific loading logic to avoid polluting the core dataloader-based library with logic specific to knex/postgres. And potentially even postgres-specific mutation logic but that's way down the road.

The best place for this stuff is in the `entity-database-adapter-knex` library. Therefore, the strategy is (in PR order):
1. Move existing knex/postgres-specific loader methods to a new loader, `knexLoader`. I'm also open to calling this a "Postgres Loader" since the `entity-database-adapter-knex` package already uses postgres-specific queries (and more are added here since this stack eventually produces raw queries) within knex even though technically knex is database agnostic.
1. Move knex-specific logic out of EntityDataManager, move it to its own EntityKnexDataManager.
1. Move these two now-separated pieces into the `entity-database-adapter-knex` package. This requires creating a "plugin" system for database adapters, where when they are included their methods are added to the prototype so that seamless loading continues to work.
1. Add a codemod for these three pieces.
1. Add the first new feature to entity loading, load by tagged template string, which creates a better/safer way to express raw queries for loading entities.

Future other things that could be added here (after thorough thought on authorization) are:
- Pagination
- Create, update, delete by tagged template string
- Batch creation
- Upsert (maybe)

# For Reviewers

The most critical things to assess are:
- "install" method in #410
- sql tagged template API usability in #414

# How

This is part 1. It refactors the loaders by moving `loadManyByFieldEqualityConjunctionAsync`, `loadFirstByFieldEqualityConjunctionAsync`, and `loadManyByRawWhereClauseAsync` to a separate loader.

The new pattern for these is:
```
await PostgresTestEntity.knexLoader(vc1).loadManyByRawWhereClauseAsync(...)
```

# Test Plan

This has full coverage in new tests.
2026-02-09 15:36:48 -08:00
renovate[bot] bc74562bf9 chore(deps): update dependency @types/node to v24.10.11 (#426) 2026-02-08 13:15:10 -08:00
renovate[bot] e37284f54a chore(deps): update dependency @types/node to v24.10.10 (#423) 2026-02-06 12:02:32 -08:00
renovate[bot] 064431924b chore(deps): update postgres:18 docker digest to 1090bc3 (#421) 2026-02-05 09:37:35 -08:00
renovate[bot] cd81c5a5b6 fix(deps): update dependency @apollo/server to v5.4.0 [security] (#420) 2026-02-04 15:41:34 -08:00
renovate[bot] 7b6ff09c68 chore(deps): update redis docker digest to c22af04 (#418) 2026-02-03 14:29:45 -08:00
renovate[bot] ad6c81def2 chore(deps): update babel monorepo to v7.29.0 (#419) 2026-02-03 14:29:23 -08:00
renovate[bot] 2791d9a05f chore(deps): update postgres:18 docker digest to 3bb77a0 (#416) 2026-02-03 09:05:05 -08:00
renovate[bot] c28e89e15c chore(deps): update redis docker digest to e59abdb (#417) 2026-02-03 09:04:53 -08:00
renovate[bot] 9a9374b173 chore(deps): update dependency pg to v8.18.0 (#415) 2026-02-02 15:19:43 -08:00
renovate[bot] 2dda22d761 fix(deps): update dependency @koa/bodyparser to v6.1.0 (#405) 2026-01-28 07:36:17 -08:00
renovate[bot] 9e88cf6aab fix(deps): update dependency @koa/router to v15.3.0 (#406) 2026-01-27 21:36:30 -08:00
Will Schurman de7548680d v0.55.0 2026-01-26 23:18:40 -07:00
Will Schurman 🦦 5db89777c6 feat: add AllowIf[Any|All]SubRulesAllowPrivacyPolicyRule rules (#401)
# Why

One other set of rules we've found useful in the Expo application for expressing complex privacy logic is a boolean any/all rule that composes an array of rules.

This PR upstreams these rules.

# How

Add rules and tests.

# Test Plan

Run tests.
2026-01-26 22:15:24 -08:00
Will Schurman 🦦 f3b575cd00 feat: add EvaluateIfEntityFieldPredicatePrivacyPolicyRule (#400)
# Why

One other rule we've found useful in the Expo application is a simple predicate rule based on an entity field.

This PR upstreams this rule.

# How

Add rule and test.

# Test Plan

Run test.
2026-01-26 22:11:23 -08:00
Will Schurman 🦦 56ed357b92 feat: Add AllowIfInParentCascadeDeletionPrivacyPolicyRule (#399)
# Why

Found this to be useful in the Expo backend application. The use case is that one may wish to permit access for the mostly-programmatic step of nullification of a field in an entity, whether or not any of the other privacy policy rules in the policy allow.

For example, let's say a user creates a build on organization X. Their user ID may be recorded in a foreign key field on the build so that the application can query all builds for that user. But then the user loses access to organization X. Then, they want to delete their user. This will do a cascade set null on the builds initiated by the user, but the user no longer has permission to update those builds via the normal organization membership permission rules, so this explicit allow-if-cascade-rule is used.

# How

Add a generic rule for use in applications.

# Test Plan

Add test.
2026-01-26 22:07:54 -08:00
renovate[bot] b758c4d344 fix(deps): update dependency @apollo/server to v5.3.0 (#403) 2026-01-26 07:32:55 -08:00
renovate[bot] d4c68112dc chore(deps): update dependency prettier to v3.8.1 (#404) 2026-01-26 07:31:21 -08:00
renovate[bot] 23d8b25805 chore(deps): update dependency pg to v8.17.2 (#402) 2026-01-23 14:54:32 -08:00
renovate[bot] 197ebd9120 chore(deps): update dependency lodash to v4.17.23 [security] (#396) 2026-01-22 11:52:51 -08:00
renovate[bot] a8223d994a chore(deps): update actions/checkout action to v6.0.2 (#398) 2026-01-22 11:51:46 -08:00
Will Schurman 🦦 7c2b812ef2 chore: Update release instructions in README (#397) 2026-01-21 15:47:41 -08:00
Will Schurman 283c02eedb v0.54.0 2026-01-21 16:34:20 -07:00
renovate[bot] c70032dc2a chore(deps): update postgres:18 docker digest to 5773fe7 (#385) 2026-01-21 15:23:42 -08:00
renovate[bot] 6c8bdfaf97 chore(deps): update dependency prettier to v3.8.0 (#392) 2026-01-19 10:01:52 -08:00
renovate[bot] acd5d715db chore(deps): update dependency pg to v8.17.1 (#391) 2026-01-19 10:01:35 -08:00
renovate[bot] 6523f07a5b chore(deps): update dependency ioredis to v5.9.2 (#393) 2026-01-19 10:01:05 -08:00
renovate[bot] 38f451fd59 chore(deps): update dependency @types/node to v24.10.9 (#394) 2026-01-19 10:00:54 -08:00
renovate[bot] 43dd87fcba chore(deps): update babel monorepo to v7.28.6 (#389) 2026-01-16 09:34:41 -08:00
renovate[bot] b125ea3473 chore(deps): update dependency @types/node to v24.10.8 (#390) 2026-01-16 09:16:19 -08:00
renovate[bot] 7244f7b591 chore(deps): update redis docker digest to 73dad42 (#386) 2026-01-15 09:01:29 -08:00
renovate[bot] 17b2105a99 chore(deps): update dependency typedoc to v0.28.16 (#387) 2026-01-15 09:00:40 -08:00
renovate[bot] 0659e8f4a2 chore(deps): update actions/setup-node action to v6.2.0 (#388) 2026-01-15 09:00:06 -08:00
Will Schurman 🦦 8fb53f2f49 chore: validate conventional commit PR title in GHA (#384)
# Why

Since this library uses conventional commits to generate changelogs, we want to validate that the PR title is in a valid conventional commit format.

Closes ENG-15958.

# How

Add lint action.

# Test Plan

This only runs once committed to main so we'll need to commit this and then try it out.

I audited the action code at a glance.
2026-01-13 15:52:12 -08:00
renovate[bot] 757393c707 chore(deps): update dependency @types/node to v24.10.7 (#383)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@types/node](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) ([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)) | [`24.10.6` → `24.10.7`](https://renovatebot.com/diffs/npm/@types%2fnode/24.10.6/24.10.7) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fnode/24.10.7?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fnode/24.10.6/24.10.7?slim=true) |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0Mi43NC41IiwidXBkYXRlZEluVmVyIjoiNDIuNzQuNSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2026-01-13 09:46:44 -08:00
renovate[bot] a75b41a515 chore(deps): update redis docker digest to eab64db (#382) 2026-01-13 09:39:50 -08:00
renovate[bot] 23f121c91e chore(deps): update node.js to v24.13.0 (#378) 2026-01-13 09:39:25 -08:00
renovate[bot] b984fa949e chore(deps): update redis docker digest to 350e485 (#380) 2026-01-13 09:32:38 -08:00
renovate[bot] cb2f4fa8ae chore(deps): update postgres:18 docker digest to fdd16e6 (#379) 2026-01-13 09:32:05 -08:00
renovate[bot] 0a70d083eb chore(deps): update dependency @types/lodash to v4.17.23 (#381) 2026-01-13 09:31:48 -08:00
James Ide 0f719432a9 Replace es6-error with native Error subclassing (1.9x faster) (#370)
Why
===
es6-error uses Object.defineProperty calls to set name, message, and stack on every error instantiation. Modern Node.js (14+) supports native Error subclassing, making this overhead unnecessary.

How
===
- Remove es6-error dependency
- Use native Error class with static blocks to set the prototype name once per class rather than per instance
- Use abstract getters for state and code in the base class, with concrete implementations in subclasses returning narrowed literal types (e.g., `get state(): EntityErrorState.PERMANENT`)
- Pass cause to Error constructor options instead of manually setting it

Abstract getters were chosen over prototype assignment because:
- They use standard TypeScript/JavaScript patterns that are self-documenting
- They don't require `declare` statements to satisfy TypeScript
- They allow narrowed return types (e.g., `EntityErrorState.PERMANENT` instead of `EntityErrorState`)

The name property is set on the prototype via static initializer because Error.prototype.name is conventionally a prototype property, and this matches how native error subclasses like TypeError work.

Benchmark shows ~1.9x faster error instantiation:
- es6-error: ~213k ops/sec
- native: ~400k ops/sec

Test Plan
===
yarn tsc passes. All tests pass
2026-01-12 17:30:52 -08:00
renovate[bot] a9a730cd83 chore(deps): update dependency @types/node to v24.10.5 (#377) 2026-01-12 17:24:29 -08:00
Will Schurman 🦦 544bb7c3f2 feat: synthetically set cascading set-null fields to null during canViewerDeleteAsync evaluation (#376) 2026-01-12 15:30:31 -08:00
Will Schurman 🦦 c38717d237 chore: require eqeqeq lint (#375) 2026-01-11 11:31:58 -08:00
James IdeandWill Schurman 🦦 ea6c128d9c fix: Inline map().filter() loops in EntityLoaderUtils (#369)
Why
===
The map().filter() pattern creates an intermediate array with null values
that are immediately discarded. For an entity with 8 fields, this allocates
an 8-element array where ~25% of values are null, only to filter them out.

Benchmarks show inlining as a for loop is 1.8x faster:
- map().filter(): 7.0M ops/sec
- for loop:       12.7M ops/sec

How
===
Replace two instances of `.map(...).filter(x => x !== null)` with for...of
loops that conditionally push to the result array. This is the same pattern
used elsewhere in the codebase (e.g., successfulResults, failedResults in
entityUtils.ts).

Test Plan
=========
All existing tests pass (362/362). Behavior is identical.

Co-authored-by: Will Schurman 🦦 <[email protected]>
2026-01-11 11:31:00 -08:00
Will Schurman 🦦 1cc57eddce fix: run tsc during tests CI job (#373) 2026-01-11 10:47:02 -08:00
James Ide 5dff512745 Optimize pick() with direct property access (#368)
Why
===
The previous implementation used Set + Object.entries + Object.fromEntries
which created intermediate data structures. Benchmarks showed this was 5.6x
slower than direct property access for typical usage (2 props from medium
entity: 1.8M vs 10.1M ops/sec).

How
===
Replace the Set-based filtering approach with a simple for...of loop that
directly accesses and copies properties. This avoids:
- Creating an intermediate Set
- Object.entries() array allocation
- Object.fromEntries() reconstruction

Use hasOwnProperty() instead of `in` operator to match the original semantics
(Object.entries only returns own properties) and avoid edge cases with
inherited properties like "toString".

Test Plan
=========
All existing tests pass. The function is semantically identical - it still
returns a new object with only the specified own properties.
2026-01-11 10:38:54 -08:00
renovate[bot] 60fed15603 chore(deps): update dependency ioredis to v5.9.1 (#374) 2026-01-11 09:50:10 -08:00
renovate[bot] fe7f7303aa chore(deps): update actions/setup-node action to v6 (#356) 2026-01-08 16:12:05 -08:00
Will Schurman 🦦 47b167e3bb chore: upgrade docker compose for tests to postgres 18 (#367) 2026-01-08 16:10:53 -08:00
Will Schurman 🦦 3997adf002 chore: upgrade lerna and run breaking change migration (#366) 2026-01-08 16:00:32 -08:00
Will Schurman 🦦 858f8715e6 chore: upgrade to uuid 13 (#365) 2026-01-08 15:45:51 -08:00
renovate[bot] 67f7516489 chore(deps): update dependency uuidv7 to v1.1.0 (#351) 2026-01-08 09:21:49 -08:00
Will Schurman 🦦 7947fe9871 chore: upgrade prettier and apply (#364) 2026-01-08 09:20:36 -08:00
renovate[bot] 5f9485627a chore(deps): update dependency ioredis to v5.9.0 (#349) 2026-01-08 09:20:16 -08:00
renovate[bot] 37fc067970 chore(deps): update yarn to v4.12.0 (#354) 2026-01-08 09:15:48 -08:00
renovate[bot] a6d2d3c55c chore(deps): update node.js to v24.12.0 (#352) 2026-01-08 09:15:15 -08:00
renovate[bot] ece647b31f chore(deps): update dependency lerna to v8.2.4 (#342) 2026-01-08 09:15:04 -08:00
renovate[bot] be72c6bf4b chore(deps): update dependency ctix to v2.7.2 (#341) 2026-01-08 09:11:26 -08:00
renovate[bot] b7f2148654 chore(deps): update actions/setup-node action to v4.4.0 (#345) 2026-01-08 09:11:16 -08:00
renovate[bot] 73f0dfa223 chore(deps): update peaceiris/actions-gh-pages action to v4 (#362) 2026-01-08 09:10:34 -08:00
renovate[bot] 35bf335a5d chore(deps): update dependency typedoc to v0.28.15 (#343) 2026-01-08 09:09:37 -08:00
renovate[bot] 971e73037d chore(deps): update codecov/codecov-action action to v5.5.2 (#346) 2026-01-08 09:09:21 -08:00
renovate[bot] 328d6ac319 chore(deps): update dependency @types/supertest to v6 (#358) 2026-01-08 09:08:08 -08:00
renovate[bot] ea21dc0f76 chore(deps): update dependency @types/jscodeshift to v17 (#357) 2026-01-08 09:07:14 -08:00
renovate[bot] 94808312eb chore(deps): update actions/checkout action to v6 (#355) 2026-01-08 09:06:00 -08:00
renovate[bot] 8998970643 chore(deps): update dependency eslint to v9.39.2 (#348) 2026-01-08 09:05:40 -08:00
renovate[bot] f9441a77a8 chore(deps): update dependency @jest/globals to v30.2.0 (#347) 2026-01-08 09:05:24 -08:00
renovate[bot] 4146afc887 chore(deps): update dependency @types/supertest to v2.0.16 (#340) 2026-01-08 08:57:18 -08:00
renovate[bot] 5b7739691f chore(deps): update dependency @isaacs/ttlcache to v2.1.4 (#333) 2026-01-08 08:56:49 -08:00
renovate[bot] 002bbd5bbf chore(deps): update dependency @tsconfig/strictest to v2.0.8 (#334) 2026-01-08 08:55:54 -08:00
renovate[bot] 9f939269cf fix(deps): pin dependency @koa/bodyparser to 6.0.0 (#339) 2026-01-08 08:55:40 -08:00
renovate[bot] ee6e57fd3b chore(deps): update dependency @types/lodash to v4.17.21 (#338) 2026-01-08 08:55:05 -08:00
Will Schurman 🦦 4cad01a940 chore: upgrade dependencies in entity-example (#332) 2026-01-08 08:51:01 -08:00
renovate[bot] 7038912ca5 fix(deps): pin dependency @apollo/server to 4.12.2 (#330) 2026-01-07 16:35:12 -08:00
renovate[bot] ce55922e3e fix(deps): pin dependency koa to 2.16.3 (#331) 2026-01-07 16:34:48 -08:00
renovate[bot] d8dccb95e4 chore(deps): update dependency koa to v2.16.2 [security] (#327) 2026-01-07 15:07:55 -08:00
renovate[bot] 5679f27209 fix(deps): pin dependencies (#328) 2026-01-07 15:06:14 -08:00
renovate[bot] bcce8ef0ae chore: Configure Renovate (#325) 2026-01-07 15:01:27 -08:00
Will Schurman 833e2e8c55 v0.53.0 2025-12-19 13:24:53 -08:00
Will Schurman 1a7dfadf64 v0.52.0 2025-12-19 13:20:50 -08:00
Will Schurman 🦦 767d9513ff chore: upgrade to node 24 LTS (#323) 2025-12-09 08:10:30 -08:00
Will Schurman 🦦 c4537262d8 feat: switch local cache to an interface (#322) 2025-12-08 17:40:30 -08:00
Will Schurman 9140938e1c v0.51.0 2025-11-21 11:36:24 -08:00
Will Schurman 🦦 019809fbe9 feat!: separate out validators into beforeCreateAndUpdate and add new beforeDelete validators (#320)
# Why

For a while in Expo application code, we've used beforeDelete triggers to approximate beforeDelete validators, but we finally found a need to separate them out. Due to the nature of validators (no side-effects) vs triggers (side-effectful), there can be a need to manually run validators but not triggers. One such need is ahead of batch deletion, to ensure that an object and it's related objects can be deleted and retain any constraints implemented in validator logic.

Closes ENG-18066.

# How

Split out definition to be more similar to triggers, update tests.

# Test Plan

Add new test for these, ensure that they are called appropriately.
2025-11-12 09:08:23 -08:00
Will Schurman 🦦 4559f8fe1b chore: add docblock for orderBy and ordering (#319)
# Why

These were missing docblocks, and are something I often have to look up.

Closes ENG-18067.

# How

Add docblocks.

# Test Plan

tsdoc
2025-11-12 09:03:23 -08:00
Will Schurman 🦦 30f4a62bfa chore: update other sibling dependencies (#318)
# Why

Part 2 of #317, just updates more dependencies. These I check the changelogs for though to be sure it isn't going to cause any issues.

# How

Read changelogs, update.

# Test Plan

CI

Changelogs:
https://github.com/redis/ioredis/blob/main/CHANGELOG.md
https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md
https://github.com/beaugunderson/ip-address/compare/v10.0.0...v10.0.1
2025-11-12 08:58:24 -08:00
Will Schurman 🦦 a1f1934fd7 chore: update dev dependencies (#317)
# Why

Ahead of some library updates, this updates the dev dependencies to their latest versions. Just a chore.

# How

Update.

# Test Plan

CI
2025-11-12 08:54:18 -08:00
Will Schurman 🦦 2f9d4873aa fix: add default for TSelectedFields in PrivacyPolicyTestUtils Case (#316) 2025-11-04 16:37:41 -08:00
Will Schurman 59e024eb19 v0.50.0 2025-10-07 16:12:16 -07:00
Will Schurman 🦦 0f53c24faf fix: make in memory and stub database adapters behave more similarly to sql (#315)
# Why

#311 updated the errors for postgres, but the conditions in the stub adapters preempted those errors in test code, so their behavior diverged. This PR makes the stub adapters behave more similarly to Postgres adapter.

# How

Return more similar results in not-found cases. See inline comments.

# Test Plan

`yarn tsc`. Also the tests in entity-example test this.
2025-10-07 16:08:43 -07:00
Will Schurman 🦦 1b9ddaa95f chore: improve test coverage (#314) 2025-10-07 13:32:23 -07:00
Will Schurman 4ab2cf1cc4 v0.49.0 2025-10-07 09:26:02 -07:00
Will Schurman 🦦 4c3fd175a9 feat: add more documentation to entity errors (#312) 2025-10-06 16:18:35 -07:00
Will Schurman 🦦 f6639f9def feat: throw specific errors for database adapter issues (#311) 2025-10-06 14:32:16 -07:00
Will Schurman 🦦 949f00d93b fix: validate that UUID field is lower-case (#310)
# Why

Technically upper-case UUIDs are valid according to https://datatracker.ietf.org/doc/html/rfc4122.

But the way entity is written expects case sensitivity due to them being treated as simple strings. For example, this would be the expected behavior:
```
const entities = await TestEntity.loader(vc).loadManyByFieldEqualingAsync('id', ['018EBFDA-DC80-782D-A891-22A0AA057D52']);
=> Map(['018EBFDA-DC80-782D-A891-22A0AA057D52', TestEntity[...]])
```

The issue is that Postgres stores them in a case-insensitive manner (which is correct according to the UUID spec). So in actuality, the above code would theoretically do something like:
```
const entities = await TestEntity.loader(vc).loadManyByFieldEqualingAsync('id', ['018EBFDA-DC80-782D-A891-22A0AA057D52']);
=> Map(['018ebfda-dc80-782d-a891-22a0aa057d52', TestEntity[...]])
```
(it actually fails due to this change of case further up in the stack at https://github.com/expo/entity/blob/077d53cd86d4c54132ea2ebcc59d1d75e2b507d0/packages/entity/src/EntityDatabaseAdapter.ts#L182, but the reason is the same: postgres transforms to lower case under-the-hood)

This results in breaking invariants the framework provides (all keys in === all keys out).

This PR solves this problem by just enforcing UUID casing in fields to match what Postgres already does to avoid this issue.

Ref: WWW-37

# How

I first tried a field normalization approach in #309, but this also had the same problem: the fields in !== fields out. Unless we were to invert the normalization on the way out, but that comes with a lot of added complexity.

So this is the solution I propose: just don't allow any upper-case characters in the UUIDs. This is technically not UUID spec compliant since we no longer accept all technically valid UUIDs.

# Test Plan

Run the test.
2025-09-30 12:52:06 -07:00
Will Schurman 🦦 02f92fc6ac chore: fix erroneous describe.only and add lint rule to prevent it (#308)
# Why

In other repositories at Expo we have this lint disallow rule to avoid accidental skips like this. This PR adds the same rule.

# How

1. Add rule.
2. Fix violation.

# Test Plan

`yarn lint`
2025-09-30 12:48:53 -07:00
Will Schurman 🦦 077d53cd86 fix: add readonly modifier to partitionArray utility method (#307) 2025-09-22 10:59:26 -07:00
Will Schurman 7b4bec8f84 v0.48.0 2025-09-19 15:59:11 -07:00
Will Schurman 4c6f340d17 v0.47.0 2025-09-19 12:03:43 -07:00
Will Schurman 🦦 4a3eda209f feat: allow manual specification of cascadingDeleteCause in delete mutator factory method (#306) 2025-09-19 11:36:24 -07:00
Will Schurman 🦦 31b52a5f75 feat: add support for Buffer fields (#305) 2025-07-29 13:26:19 -07:00
Nick Novitski 9a03fdb7ba chore: Clear dependency warnings on entity-example 2025-06-26 17:13:00 -07:00
Nick Novitski 28cbac54b4 chore: Remove unneeded files from packages 2025-06-26 17:12:21 -07:00
Will Schurman 79f61f8d7f v0.46.0 2025-06-26 13:12:05 -07:00
Nick Novitski 0038d633b3 Fix exports for ESM projects
Before this commit, for projects with `"type": "module"`,  anything
exported from a file `export default` was not resolvable from the barrel
file as a named export.

After this change, the interface of the index is unchanged, but ESM
projects can use it.

To test this, the entity-example package is given `"type": "module"`,
and changed to run with `node --experimental-transform-types`.  Its
imports from other packages do not need to change.  The other
integration tests passing are evidence that commonjs modules also do not
need to change their imports.

This commit also adds an  eslint rule to enforce this standard.
2025-06-24 10:03:22 -07:00
Nick Novitski 18a0d297d0 Unify repo-wide scripts
These are faster, and can pass arbitrary arguments to the underlying
command, which allows repo-wide "watch" workflows.
2025-06-24 09:58:54 -07:00
Nick Novitski 93d84807da chore: Use 'v8' coverage provider 2025-06-23 16:04:54 -07:00
Nick Novitski 64057b6d52 chore: Explicitly list files to collect coverage from
By default, files not loaded from any test will not effect coverage
metrics, although actually we want them to be counted as uncovered.
2025-06-23 15:45:59 -07:00
Nick Novitski 850fa2b886 chore: Use tsconfig bases (#296)
# Why

These are sensible defaults, so much so that they almost perfectly match what we already set.

# Test plan

`yarn build`
2025-06-20 12:24:42 -07:00
Nick Novitski 65d50d6cb3 chore: jest v29.7.0 -> 30.0.0 (#295)
# Why

It's faster, at least locally where we run without coverage instrumentation.

# Test Plan

CI.
2025-06-20 12:19:23 -07:00
Nick Novitski 1fd0144cc3 chore: Replace ts-jest with babel-jest (#294)
# Why

It's faster.  We can still type-check in a separate step in CI, or locally.

# Test Plan

All test scripts are still runnable, including in CI.
2025-06-20 12:14:21 -07:00
Nick Novitski 6cecd9f6b9 chore: Remove @jest/types (#293)
# Why

Jest is written in typescript and ships with its own types.

# Test Plan

Typecheck and tests running in CI.
2025-06-20 12:07:30 -07:00
Will Schurman 875cce3432 chore: update README to be more precise (#292) 2025-06-16 21:59:15 -07:00
Will Schurman fe2d246f87 v0.45.0 2025-06-06 09:06:35 -07:00
Will Schurman a3ab61bfa8 feat: add ability to define a custom cache key invalidation version supplier (#290) 2025-06-06 09:01:47 -07:00
Will Schurman 78bc264b33 feat: add createOrGetExistingAsync and createWithUniqueConstraintRecoveryAsync utility methods (#291) 2025-06-06 08:59:59 -07:00
Will Schurman b9b09933e5 v0.44.0 2025-05-29 09:45:25 -07:00
Will Schurman d3e89bdd82 fix: introduce transactional dataloader mode and fix global transaction ID generation (#289) 2025-05-28 17:09:29 -07:00
Will Schurman b6089524bb chore: add more @internal to typedoc (#287)
# Why

Noticed in this stack that a few more places could use the `@internal` typedoc annotation to prevent it from polluting the public docs.

# How

Add annotation to internal constructs.

# Test Plan

Inspect docs manually (`yarn typedoc`, open html file)
2025-05-23 11:07:21 -07:00
Will Schurman e054a0d74b feat: add transaction-scoped dataloaders (#284)
# Why

This is a retry of #98. It has become more important in Expo's server application due to more things running within transactions. When it was written previously, relatively few things were transactional.

The idea is that within a transaction, subsequent reads should be able to be cached locally (dataloader) as long as the local cache is kept consistent (not in the cache adapter though).

# How

The way this is done is to keep a dataloader map (dataloader per-entity like we do for the top-level loads) per-transaction as well. Then, when in a transaction, instead of going directly to the database, go through the transaction's dataloader before hitting the database. This way loads within the transaction are coalesced and cached locally.

Invalidation becomes more complex though. 
- We still only want to invalidate the non-transactional dataloader/cache at the end of the transaction (as late as possible for least chance of race condition) for all changed entities. This didn't change.
- As for when to invalidate the transactional dataloaders, we need to think about this like database sub-transaction visibility, but only in the sense that we don't want inconsistent reads. Put another way, any load in or out of any transaction or nested transaction needs to be consistent with what is in the database, so any time the loaded value would change if it went directly do the database we must also invalidate the cache.
   - This is further complicated by the fact that postgres doesn't have real subtransactions (see #194 for more discussion) and subtransactions weren't an entity feature for the first attempt at this. This means that it's hard to test the case where a nested transaction is isolated from the parent transaction for uncommitted reads or the inverse.
   - The way I came up with achieving this is to just to over-invalidate: clear all the dataloaders in the transaction recursively starting from the outermost transaction. And do this clearing right after the mutation and at the end of the sub-transaction and at the end of the transaction (recursively).

# Test Plan

Run new tests.
2025-05-23 11:02:22 -07:00
Will Schurman 4ddea7da77 chore: upgrade dataloader package (#286)
# Why

Noticed that this (major) dependency of entity has been updated. The [changelog](https://github.com/graphql/dataloader/blob/main/CHANGELOG.md) doesn't indicate anything breaking.

# How

Upgrade.

# Test Plan

Rely upon full test coverage for compatibility.
2025-05-23 10:55:22 -07:00
Will Schurman 8c52ace8d1 chore: upgrade dev dependencies and eslint (#285)
# Why

This upgrades some dev dependencies and eslint to v9.

# How

Upgrade packages, [update eslint config to be flat config.](https://eslint.org/docs/latest/use/configure/migration-guide)

# Test Plan

CI (lint).
2025-05-23 10:48:27 -07:00
Will Schurman 618383027d v0.43.0 2025-04-10 14:22:20 -07:00
Will Schurman 9758fd5df8 fix: fix generated tsc definition sentinel type visibility (#283) 2025-04-10 14:12:55 -07:00
Will Schurman 8414d96d94 v0.42.0 2025-04-10 12:39:21 -07:00
Will Schurman ed8528d6a6 chore: upgrade dependencies (#282) 2025-04-10 11:24:50 -07:00
Will Schurman 0ea917ad02 chore: separate out build tsconfig (#281) 2025-04-09 09:16:26 -07:00
Will Schurman 485894af16 feat: add @expo/entity-testing-utils package (#280) 2025-04-09 09:11:36 -07:00
Will Schurman 1da5cc01e1 feat: enforce explicit id field cache property (#276) 2025-04-02 10:53:25 -07:00
Will Schurman b7e524c418 feat: change entity id field generic to field name and derive type where necessary (#278) 2025-04-02 10:48:47 -07:00
Will Schurman d9c1852706 feat: invalidate n+/-1 cacheKeyVersion for entities for push safety (#275) 2025-04-02 10:31:15 -07:00
Will Schurman ca1b69e2cb chore: upgrade typescript to 5.8 and a few other packages (#274) 2025-03-30 11:32:50 -07:00
Will Schurman f0aa0dafeb feat: add composite field loading and caching (#272) 2025-03-30 11:16:38 -07:00
Will Schurman 06b3cb7f80 feat: convert batched/cached loader interface to holder pattern (#271)
# Why

Step 2 of #201.

This PR abstracts out (genericizes) the load key and load value interfaces for batched/cached loading in preparation for composite field loading.

# How

Accomplishing this requires a number of things:
1. A set of interfaces for the keys and load values to be used to load entity field objects.
2. A concrete implementation of that interface for the current use case (single fieldName, fieldValue(s)).
3. Data structure changes in the adapters and data manager to handle the new key type not being comparable via `===`. (see base PR for new map data structure for using these objects as keys).
4. Updates to the test matchers to know how to compare the serializable holder objects.

# Test Plan

Update a number of tests and add a few new tests. This has full coverage.
2025-03-28 11:36:10 -07:00
Will Schurman ad41af1c82 feat: add some utility classes and methods in preparation for composite keys (#269)
# Why

This adds the utility classes needed for composite field loading as described in #201.

# How

This adds a few concepts:
- `areSetsEqual` - simple utility for checking set equality
- `SerializableKeyMap` - a map where keys must be of a custom interface for checking equality.

# Test Plan

Add new tests, run them. Full coverage.
2025-03-28 11:32:12 -07:00
Will Schurman f3a2bf62de chore: update generic constraint for TFields everywhere (#268) 2025-03-22 09:57:08 -07:00
Will Schurman 1d95eba1f9 fix: fix metrics for dataloader waterfall logging (#267) 2025-03-22 09:41:39 -07:00
Will Schurman aa56b705db fix: move source of truth of loader docblocks to enforcing loader since it is not the default loader (#264) 2025-03-22 08:58:24 -07:00
Will Schurman ee4be736a9 v0.41.0 2025-03-07 11:43:14 -08:00
Will Schurman 4a2b61f2bd feat: Create codemod for v0.40.0 -> v0.41.0 (#260) 2025-03-07 11:39:22 -08:00
Will Schurman a976e76509 feat!: Default all loaders and mutators to enforcing (#256)
BREAKING CHANGE: Default all loaders and mutators to enforcing
2025-03-07 11:34:13 -08:00
Will Schurman 72f34f0bee chore: Fix changelogs for 0.40.0, update readme 2025-02-26 22:59:51 -08:00
Will Schurman 74c396255c v0.40.0 2025-02-26 22:50:49 -08:00
Will Schurman 5cb88ca6e7 chore: remove prepublish script 2025-02-26 22:43:23 -08:00
Will Schurman 4d070248d8 chore: fix typedoc 2025-02-26 22:29:01 -08:00
Will Schurman acd52f18ce chore: fix build setup (#263) 2025-02-26 22:23:22 -08:00
Will Schurman d4eb0a6214 chore: attempt to fix lint again 2025-02-26 21:44:08 -08:00
Will Schurman 3cd004ad55 chore: attempt to fix lint in CI 2025-02-26 21:41:34 -08:00
Will Schurman 381651d8bb chore: fix lint 2025-02-26 21:29:34 -08:00
Will Schurman 089cfbb040 chore: fix yarn v4 dev dependencies 2025-02-26 21:24:34 -08:00
Will Schurman 02f7428257 chore: upgrade to yarn 4.6.0 2025-02-26 09:55:39 -08:00
Will Schurman 26ad65d657 chore: upgrade lerna 2025-02-26 09:11:40 -08:00
Will Schurman fa78ce84e2 Revert "v0.40.0"
This reverts commit c9ab838a75.
2025-02-25 09:59:55 -08:00
Will Schurman c9ab838a75 v0.40.0 2025-02-25 09:55:05 -08:00
Will Schurman 3c2a1920f0 chore: fix typedoc again (#262) 2025-02-25 09:38:34 -08:00
Will Schurman ab34b4ae8a chore: fix typedoc (#261) 2025-02-25 07:57:08 -08:00
Will Schurman d63ddd6599 feat: Create codemod for v0.39.0 -> v0.40.0 (#259) 2025-02-24 12:09:24 -08:00
Will Schurman c6f74a14a9 feat!: Add enforcing entity association loader (#257) 2025-02-23 19:35:25 -08:00
Will Schurman be9c40e8d7 chore: add CI check for needing to run ctix (#258) 2025-02-23 19:32:25 -08:00
Will Schurman 081c3576a8 chore: switch barreling to ctix (#255) 2025-02-23 19:29:25 -08:00
Will Schurman 92bac05687 chore: internal refactor of loader to be structured like new mutation methods (#254) 2025-02-23 19:26:22 -08:00
Will Schurman 4d56065518 feat!: Make load/create/update/delete entity authorization result interfaces consistent (#253) 2025-02-23 19:23:25 -08:00
Will Schurman 275c63d277 chore: upgrade typescript and related packages (#252) 2025-02-23 19:20:47 -08:00
Will Schurman 2504aecddb chore: update pg package to latest version (#251) 2024-12-04 10:01:29 -08:00
Will Schurman 9426a08a32 v0.39.0 2024-11-21 10:46:32 -08:00
Will Schurman ae3a6c5c77 chore: upgrade typescript and lint (#250) 2024-11-21 10:45:15 -08:00
Will Schurman 27a358d259 feat: add function to get authorization results for canViewerUpdateAsync/canViewerDeleteAsync (#249) 2024-11-20 14:32:00 -08:00
Will Schurman c2ae21b5cc chore: upgrade typescript to 5.5.4 (#247) 2024-08-21 13:45:21 -07:00
Will Schurman 5ab1f7953c chore: upgrade typescript and related packages (#246) 2024-07-01 09:52:04 -07:00
Will Schurman d7cc0c23b9 v0.38.0 2024-06-27 12:45:48 -07:00
Will Schurman 162b19257a feat: add EntityEdgeDeletionAuthorizationInferenceBehavior for canViewerDeleteAsync (#243) 2024-06-27 12:33:41 -07:00
Will Schurman 15117fc85e chore: add linter for async method Async suffix (#242) 2024-06-14 14:55:10 -07:00
Will Schurman 59fc77810a v0.37.0 2024-06-12 09:56:29 -07:00
Will Schurman 8c5f81bcca fix: update barrels (#241) 2024-06-12 09:54:39 -07:00
Will Schurman 28e3afb62b chore: update CHANGELOG.md (#240) 2024-06-11 21:35:08 -07:00
Will Schurman f17a33c428 v0.36.0 2024-06-11 21:29:05 -07:00
Will Schurman 93905be81c chore: move entity loader utils into their own object (#239) 2024-06-11 21:04:20 -07:00
Will Schurman 2edc7af7e2 BREAKING CHANGE: make using non-enforcing entity loader explicit (#238) 2024-06-11 20:50:49 -07:00
Josh McFarlin 6569486ecd feat: Add global mutation trigger field to EntityCompanionProvider (#215)
* Add global mutation trigger field to EntityCompanionProvider

* Add globalMutationTrigger to additional test cases

* merge mutation triggers outside of EntityMutator

* Exclude triggers with empty arrays
2024-06-11 12:24:16 -07:00
Will Schurman 778d9e304e chore: upgrade dev dependencies and knex (#237) 2024-06-04 16:01:56 -07:00
Will Schurman 05726d45fa fix: disallow keys of JS Object prototype for safety (#236) 2024-06-04 15:41:13 -07:00
Will Schurman 7e2cea1697 fix: constrain entity fields type to string-keyed object (#235) 2024-06-04 15:35:13 -07:00
Will Schurman 7c3c9854a4 fix: always reload entity after update since cascading changes may have changed it since commit (#233) 2024-06-04 11:19:22 -07:00
Josh McFarlin 1e8ea64017 feat: Use uuid v7 in stub database adapter (#234) 2024-06-03 14:14:59 -07:00
Will Schurman af495a9a85 feat: add previousValue to privacy policy context for updates (#232) 2024-05-31 13:14:32 -07:00
Will Schurman 60fc9a4c4b feat: make canViewerDeleteAsync recursive (#224) 2024-05-31 12:41:36 -07:00
Will Schurman 14bd959e53 chore: Update README.md 2024-05-30 10:17:03 -07:00
Will Schurman 91d46ba49f chore: add test coverage for remaining code (#231) 2024-05-30 10:11:35 -07:00
Will Schurman 16e7c21b1c chore: add more tests for stub database adapter and test utils (#230) 2024-05-30 10:11:02 -07:00
Will Schurman 3ce0ac91f8 chore: add more tests for entity loader (#229) 2024-05-30 10:10:29 -07:00
Will Schurman 428cc6fc5c chore: add more tests for privacy policy rules and nested transaction query contexts (#228) 2024-05-30 10:09:57 -07:00
Will Schurman 8490ad5a19 chore: add more test coverage to postgres transaction isolation levels (#227) 2024-05-30 10:09:16 -07:00
Will Schurman 8a448b5c42 chore: Update README.md 2024-05-28 12:19:23 -07:00
Will Schurman 3fd8a92cc8 chore: Update README.md 2024-05-28 12:18:37 -07:00
Will Schurman b5998063a6 chore: set codecov default branch (#226) 2024-05-28 12:12:15 -07:00
Will Schurman 8fc0849618 chore: ignore entity-example package from codecov (#225) 2024-05-28 12:01:19 -07:00
Will Schurman 87532525d0 feat: Add StrictEnumField with better validation (#222) 2024-05-02 11:30:32 -07:00
Will Schurman 33771295c5 v0.35.0 2024-04-11 09:55:15 -07:00
Will Schurman 1bdfbd6b56 fix: Revert: require explicit query context specification (#219)
This reverts commit 8b0b31fdde.
2024-04-11 09:51:40 -07:00
Will Schurman 0fa6f554a2 v0.34.0 2024-04-11 09:13:48 -07:00
Will Schurman 8b0b31fdde feat: require explicit query context specification (#219) 2024-04-11 09:09:18 -07:00
James Ide cc6f3dc1e6 feat: Allow UUIDv6/7/8 in UUIDField validation (#221)
Why
---
Currently the UUIDField type supports only UUIDv1-5 and to use a newer UUID version we need to use StringField.

How
---
Manually implemented UUID validation, adding support for versions 6, 7, and 8. Also added support for the "max" UUID, which is now part of the spec.

Test Plan
---
Added a UUIDv7 value (from https://uuid7.com/) to test cases.
2024-04-08 18:17:24 -07:00
Will Schurman 9c43a56a39 BREAKING CHANGE: hoist the responsibility of sharing local memory cache adapter into application (#218) 2023-12-06 12:38:30 -08:00
Will Schurman ef7b452c9f docs: fix EntityMutationTrigger docblocks (#216) 2023-12-04 16:18:29 -08:00
Will Schurman 659e08386f v0.33.0 2023-10-06 16:00:57 -07:00
Will Schurman aad94aca47 feat: add loadManyByIDsNullableAsync loader method (#214) 2023-10-04 15:28:28 -07:00
Will Schurman d2a4405fa1 v0.32.0 2023-06-09 20:13:23 -05:00
Will Schurman 503a2fb6a8 fix: remove unused field from redis configuration (#213) 2023-06-09 17:08:57 -07:00
Will Schurman 3a00a43036 chore: fix lint (#212) 2023-06-09 11:38:07 -07:00
Will Schurman 05b14843a6 chore: upgrade dependencies (#211) 2023-06-09 11:33:58 -07:00
Will Schurman 6f8020fbf8 chore: refactor common cache logic into GenericEntityCacheAdapter (#209) 2023-06-09 11:32:34 -07:00
Will Schurman 4b18010d42 feat: entity companion definition static thunk (#210) 2023-06-09 11:20:24 -07:00
Will Schurman 2069a0d9fd feat: add ability to specify knex transaction config (#207) 2023-04-20 18:10:07 -07:00
James Ide 83fe9cdea5 v0.31.1 2023-03-06 22:08:09 -08:00
James Ide 1934216b70 [rfc] loadFirstByFieldEqualityConjunction, a convenience method (#206) 2023-01-24 16:41:06 -08:00
Will Schurman 92f5529eba v0.31.0 2023-01-18 21:51:04 -08:00
Will Schurman 67732d6847 chore: upgrade ioredis to 5.2.5 (#205) 2023-01-18 21:49:52 -08:00
Will Schurman bb0cd51eb5 chore: upgrade knex to 2.4.1 for CVE-2016-20018 (#204) 2023-01-18 14:02:47 -08:00
Will Schurman abd86b91d3 v0.30.0 2022-12-23 15:42:23 -07:00
Will Schurman b5f734b1e1 [www] Upgrade knex to 2.3.0 (#203) 2022-12-23 12:13:46 -08:00
Will Schurman b11bd24351 v0.29.0 2022-10-06 11:48:07 -07:00
Will Schurman 684f91b1f2 feat: add example of redis mget batching using @expo/batcher (#199) 2022-10-06 11:42:22 -07:00
Will Schurman e381f5b2ac feat: change redis cache adapter to use redis-like interface (#200) 2022-10-06 11:34:28 -07:00
James Ide 69692c3b64 docs: fix typo descripting afterAll trigger (#198)
The docblock for the afterAll trigger used to say that the triggers were run before create/update/delete operations. s/before/after/
2022-10-05 14:53:12 -07:00
Will Schurman dbf1e03039 v0.28.0 2022-09-13 11:23:26 -07:00
Will Schurman 0fb74137f2 chore: upgrade integration test postgres to v14 (#195) 2022-09-13 11:18:56 -07:00
Will Schurman a77b914ad6 feat: nested transactions (#194) 2022-09-13 10:47:57 -07:00
Will Schurman acbad826a3 feat: add BigIntField (#193) 2022-08-31 18:48:48 -07:00
Will Schurman 8bdd9f7ac2 docs: improve documentation to FieldEqualityCondition (#192) 2022-08-24 13:44:48 -07:00
Will Schurman bc6a0d4310 chore: update readme for easier release (#191) 2022-08-09 13:13:13 -07:00
Will Schurman a067a44230 v0.27.0 (#190) 2022-08-09 13:07:40 -07:00
Will Schurman bffa7ca870 ci: fix typedoc (#189) 2022-08-09 12:35:38 -07:00
Will Schurman 847c20fabe ci: fix node version in docs action (#188) 2022-08-09 12:09:39 -07:00
Will Schurman 8a98f35b91 chore: upgrade lerna (#187) 2022-08-09 11:41:15 -07:00
Will Schurman 7829d90f4b ci: fix github actions docs workflow branch (#186) 2022-08-09 11:06:36 -07:00
Juwan Wheatley 2817d78392 feat: add orderByRaw to loadManyByRawWhereClauseAsync (#185)
* feat: add orderByRaw to querySelectionModifiers

* implement PR feedback

* fix tsc

* restrict orderByRaw to loadManyByRawWhereClauseAsync

* create new type for orderbyraw param
2022-08-09 13:44:07 -04:00
Quinlan Jung af81981772 docs: update readme deployment (#184) 2022-07-29 23:05:21 +02:00
Quinlan Jung 0c6aa30cdd v0.26.1 (#183)
* v0.26.1

* manually add changelog
2022-07-29 22:47:58 +02:00
Quinlan JungandWill Schurman 8530fb0983 [fix/feat] recache values in higher layers for composed cache adapter (#182)
* [fix/feat] recache values in lower layers for composed cache adapter

* Update packages/entity/src/ComposedEntityCacheAdapter.ts

Co-authored-by: Will Schurman <[email protected]>

* pr feedback

* Update packages/entity/src/ComposedEntityCacheAdapter.ts

Co-authored-by: Will Schurman <[email protected]>

* pr feedback

Co-authored-by: Will Schurman <[email protected]>
2022-07-28 03:46:00 +02:00
Will Schurman ce0ac6f8cb docs: add docblock for EntityAssociationLoader and fix other docs (#181) 2022-07-25 11:45:16 -07:00
Will Schurman c8f81a76c3 v0.26.0 (#180) 2022-07-14 14:46:42 -07:00
Will Schurman a0c35ddd7b feat: add EntityEdgeDeletionBehavior.SET_NULL_INVALIDATE_CACHE_ONLY (#178) 2022-07-14 14:15:41 -07:00
Will Schurman 25d14bf8c1 fix: fix workflow branch specification (#179) 2022-07-14 12:10:56 -07:00
James Ide 0dbf5e98bb [tests] Run tests on Node 18 instead of Node 12 (#174)
Node 16 is LTS now and 12 is unsupported. 18 is also now released and we will one day run on it so this makes sure one of our major dependencies works on Node 18.

All tests pass, no changes needed.
2022-06-28 17:24:42 -07:00
James Ide 4347d9d577 [npm][ts] Upgrade all minor npm deps incl. TS 4.7 (#176)
Why
---
There were some automated security notices about outdated npm package versions, many of which often can be addressed with minor dependency upgrades.

How
---
Deleted and regenerated yarn.lock, which affects this repo but not consumers of these packages. However, TypeScript 4.7 has a change where it considers stringifying a Symbol to be a potential error. This commit explicitly stringifies keys (entity field names).

I opted for `String(fieldName)` over constraining the field name with `N extends string & keyof TFields` because adding `string &` is "viral" in the sense that lots of files including users of Entity would need to add `string &`. Also, using Symbol field names probably would work fine, they just need to be stringified before printing them.

There were also some Koa router type changes that treat params as `string | undefined` so we need to use `!` in the Entity example router.

Test Plan
---
tsc + unit tests
2022-06-21 00:18:15 -07:00
Will Schurman 9a91b19466 chore: clean up metrics API a bit and add docblocks (#175) 2022-06-08 17:32:07 -07:00
Will Schurman cb7aaad979 docs: update docblock for EntityQueryContext classes (#173) 2022-05-11 11:22:49 -07:00
James Ide 9fcfbf746a feat: update ioredis to v5, make it a devDependency of the cache packages (#172)
Why: ioredis v5 ships with its own TypeScript declarations. The breaking changes from v4 to v5 affect the code that instantiates ioredis, not the code that uses it. As such, I believe that entity is still technically compatible with ioredis v4, but for the sake of keeping our test matrix smaller, this commit uses just ioredis v5 in tests and makes v5 a peer dependency of `entity-cache-adapter-redis`.

Additionally ioredis was a dependency of `entity-cache-adapter-redis`. I changed it to a devDependency and changed the import to `import type` since the package is used only for type checking.

ioredis v5.0.0 changelog: https://github.com/luin/ioredis/releases/tag/v5.0.0. Notable it drops support for Node 10 and for custom Promise libraries, neither of which we use.

Test Plan: building all packages and testing them with Lerna succeeds
2022-04-11 09:44:57 -07:00
James Ide 9c44e8e8d4 Upgrade TS to 4.6.x, use knex 1.0.5 during testing (#171)
Why: Expo's API server will use TS 4.6 and knex 1.0.5. This change makes it so that we run entity's CI tests against these new dependencies.

How: Upgraded the version of typescript in the root package.json. Modified yarn.lock to use knex 1.0.5 instead of updating the db adapter's package.json since knex 1.0.2 still works fine.

Test Plan: Lerna runs tsc in every package (no errors). Unit tests in CI.
2022-04-11 09:16:19 -07:00
Will Schurman d731b0ddb7 v0.25.3 2022-03-12 14:32:00 -07:00
Will Schurman 293868b1a7 fix: lock lru-cache to v6.0.0 (#170) 2022-03-12 13:29:16 -08:00
Will Schurman 02d2f24bb6 v0.25.2 (#169) 2022-03-11 17:09:48 -08:00
Will Schurman e76ca8c2f7 fix: lru-cache yarn workspace version resolution (#168) 2022-03-11 17:07:02 -08:00
Will Schurman 0736e20987 v0.25.1 (#167) 2022-03-10 15:26:07 -08:00
Will Schurman 5c82edcd9f fix: add composed cache to index (#166) 2022-03-10 15:24:09 -08:00
Will Schurman 96e1023d40 v0.25.0 (#165) 2022-03-10 15:05:15 -08:00
Will Schurman 3a81a4c059 feat: privacy policy evaluation context (#162) 2022-03-10 14:44:12 -08:00
Will Schurman 6b6e70e75f feat: composed cache adapter, composed secondary cache, local memory secondary cache (#164) 2022-03-10 14:42:01 -08:00
Quinlan Jung bc378b6c79 chore: better variable and method naming (#160) 2022-02-16 18:37:51 -08:00
Quinlan Jung fefa4100fc v0.24.0 (#159)
* v0.24.0

* changelog
2022-02-16 18:37:39 -08:00
Quinlan Jung 85d35752fa feat: add local memory cache package (#158)
* feat: add local memory cache package

* some pr feedback

* more tests
2022-02-15 18:44:20 -08:00
Quinlan Jung 09d48a617c chore: refactor cachers to use an abstract class and interface (#157)
* chore: refactor cachers to use an abstract class and interface

* fix lint

* changelog
2022-02-14 18:42:27 -08:00
Will Schurman 0adcf7238f [entity] Add docblocks for EntityFields and EntityFieldDefinition (#156) 2022-02-11 14:20:45 -08:00
Quinlan Jung 41bd0722c9 v0.23.0 (#155)
* v0.23.0

* update changelog
2022-02-09 13:59:40 -08:00
Quinlan Jung 03a1fce067 move responsibility of transformer to generic redis cacher (#153)
* will's commit

* integ tests

* fix codecov
2022-02-07 15:04:02 -08:00
James Ide a2c6dd7488 v0.22.0 (#151) 2022-02-03 18:52:25 -08:00
James Ide e1b0ee5281 feat: Upgrade knex to 1.x in @expo/entity-database-adapter-knex (#150)
Upgrades knex to 1.x internally. This changes the behavior of `returning` but not in a way that affects how the knex adapter uses it: `returning('*')` behaves the same as before.

This does, however, change the TypeScript declarations for knex, which changed from:
```
 Knex<TRecord extends {} = any, TResult = unknown[]>
```
to:
```
 Knex<TRecord extends {} = any, TResult = Record<string, any>[]>
```
2022-02-03 18:35:55 -08:00
Will Schurman 5d39fa928f v0.21.0 (#148) 2022-01-03 11:04:02 -08:00
Will Schurman f1d9847210 feat: add pre-commit callbacks on EntityQueryContext (#147) 2022-01-03 09:37:19 -08:00
Will Schurman 99657196f8 v0.20.0 (#146) 2021-12-29 10:50:58 -08:00
Will Schurman 3727191767 feat: add cascading deletion info (#145) 2021-12-29 10:39:22 -08:00
Will Schurman efa131ef29 fix: separate post-commit invalidation callbacks from trigger and user callbacks (#144) 2021-12-23 12:41:29 -08:00
Will Schurman c4df889d50 v0.19.0 (#143) 2021-10-28 14:35:19 -07:00
Will Schurman 7cf54d83c5 feat: add authorization metrics (#142) 2021-10-24 17:26:40 -07:00
kgc00 1405a9126e feat: add IP support (#138)
* Add @expo/entity-ip-address-field

- adds an entity field for IP addresses, validates both v4 and v6 type IPs
- ip address field functionality moved to its own package
- specified `@expo/entity` as a dev & peer dependency
2021-10-15 10:40:20 -07:00
Will Schurman 677d15174c v0.18.0 (#140) 2021-10-14 14:51:59 -07:00
Will Schurman 1220477aa3 feat: expose describeFieldTestCase for use in other packages (#139) 2021-10-14 14:41:26 -07:00
Will Schurman 5c7efe1678 feat: use better QueryContext types for transactional triggers and validators (#136) 2021-09-17 17:24:48 -07:00
Will Schurman 4263cb9b8b feat: enable noImplicitOverride tsc setting (#135) 2021-09-07 15:18:22 -07:00
Will Schurman 7612392dbf feat: upgrade TypeScript to 4.4 (#134) 2021-09-07 15:17:39 -07:00
Will Schurman 1201cc065c v0.17.0 (#133) 2021-08-16 11:52:43 -06:00
Hayden Daly 2f2d963cf9 feat: Add IntField and FloatField, deprecate NumberField (#131)
Why
To better represent Postgres' type system, deprecated the type NumberField and replaced with IntField and FloatField.

How
Added the new types to EntityFields.ts. To release, will need to bump minor version so breaking changes aren't propagated.

Test Plan
Update tests using the NumberField to use IntField.
2021-07-08 08:50:10 -07:00
James Ide 944f4d2e07 v0.16.0 (#132) 2021-07-06 21:33:03 -07:00
Will Schurman 2f37dc6a37 feat: allow null field values in loadManyByFieldEqualityConjunctionAsync (#130) 2021-07-01 10:58:34 -07:00
Will Schurman 5401be0ac8 docs: add better docblocks for EntityCompanionDefinition and EntityConfiguration (#129) 2021-06-11 13:08:11 -07:00
Will Schurman ee28fa0a87 v0.15.0 (#127) 2021-05-26 17:20:52 -06:00
Will Schurman 226bffe0a9 fix: upgrade some other packages to fix dependabot alerts (#126) 2021-05-26 15:00:08 -06:00
Will Schurman 7c43edf3de fix: upgrade ioredis, knex, and pg (#125) 2021-05-26 14:59:20 -06:00
Will Schurman 4cba01eb25 feat: secondary cache loader (#123) 2021-05-26 09:52:38 -06:00
Will Schurman d78f452bb3 fix: use column name instead of field name for redis cache key (#124) 2021-05-12 15:59:42 -07:00
Will Schurman eaab0b1f4c v0.14.1 (#122) 2021-03-24 15:31:19 -07:00
Will Schurman 8e8f0dc863 fix: typescript typedef of EntityMutationInfo (#121) 2021-03-24 15:28:10 -07:00
Will Schurman 1621a1c432 v0.14.0 (#120) 2021-03-24 15:05:39 -07:00
Will Schurman ef40fa5e9c feat: add mutation type to validator and trigger execute functions (#119) 2021-03-23 22:07:53 -07:00
Will Schurman b11dbc087b fix: mutation validator and trigger cyclic import structure (#118) 2021-03-23 13:08:00 -07:00
Will Schurman 3ce961d2f2 docs: upgrade typedoc (#117) 2021-02-19 15:01:01 -08:00
Will Schurman 2052838403 docs: fix docblock comment issue (#116) 2021-02-19 14:28:04 -08:00
Will Schurman 9e828f4e0b v0.13.0 (#115) 2021-02-12 09:40:01 -08:00
Will Schurman 022e0e9b88 fix: allow undefined value for all fields (#114) 2021-02-12 09:34:24 -08:00
Will Schurman 6c4d4b03b5 feat: add basic field type runtime validators (#113) 2021-02-08 09:36:29 -08:00
Will Schurman 5a8479f3d9 v0.12.0 (#112) 2021-01-22 09:28:24 -08:00
Will Schurman 045c575713 chore: upgrade more packages (#111) 2021-01-20 12:53:31 -08:00
Will Schurman a032141fbf chore: upgrade typescript to 4.1 (#110) 2021-01-20 12:37:15 -08:00
Will Schurman 72a77f8b68 fix: remove cache and database adaptor flavor enums (#109) 2021-01-20 11:49:45 -08:00
Will Schurman b7309e18a2 fix: move runInTransaction to ViewerContext (#108) 2021-01-20 10:07:13 -08:00
Will Schurman 01b3bfef7c feat: better typing on filterMap for use with predicates (#106) 2021-01-08 10:02:02 -08:00
Will Schurman 83547412a2 v0.11.0 (#105) 2020-12-23 16:49:06 -08:00
Will Schurman fe2917e6b5 fix: add type generic default in test utils (#104) 2020-12-23 16:18:57 -08:00
Will Schurman 29c1a434ab fix: maps utils invariants for collections containing undefined values (#103) 2020-12-23 14:01:20 -08:00
Will Schurman 15546aad97 feat: better cache adapter error handling (#102) 2020-12-23 13:47:22 -08:00
Will Schurman 5208aeedb0 feat: better database adapter error handling (#101) 2020-12-22 18:01:12 -08:00
Will Schurman b8e07f9ddc fix: remove any cast from EntityLoader (#100) 2020-12-21 16:28:28 -08:00
Will Schurman e3b2b235f8 v0.10.0 (#97) 2020-11-16 20:27:33 -08:00
Will Schurman d9603298e0 fix: fix null handling in load paths (try 2) (#96) 2020-11-16 20:03:32 -08:00
Will Schurman b22c734275 v0.9.1 (#94) 2020-11-06 09:11:55 -08:00
Will Schurman 012c05d62d fix: edge deletion behavior cyclic import structure again (#93) 2020-11-06 08:52:37 -08:00
Will Schurman ead9561cc7 v0.9.0 (#92) 2020-11-05 14:40:17 -08:00
Will Schurman d1a6f13396 feat: add ability to invalidate an entity manually (#91) 2020-11-05 12:38:14 -08:00
Will Schurman cca8fc1987 fix: edge deletion behavior cyclic import structure (#89) 2020-11-05 10:08:50 -08:00
Will Schurman 86aa5b291d v0.8.1 (#87) 2020-10-09 12:07:19 -07:00
Will Schurman 4026fc1a61 docs: update to add 2FA token note to release process 2020-10-09 10:59:58 -07:00
Will Schurman a263e74b8a fix!: add entity name to loader counter metric events (#86) 2020-10-09 10:48:00 -07:00
Will Schurman f737fa5b28 v0.8.0 (#85) 2020-10-09 10:11:22 -07:00
Will Schurman 884320ac53 docs: update readme one last time for release process 2020-10-09 10:06:34 -07:00
Will Schurman 40906202cf docs: update readme again for release process 2020-10-09 10:04:30 -07:00
Will Schurman 6cdd720c98 docs: update readme for release process (#84) 2020-10-09 09:58:16 -07:00
Will Schurman f4cdc01c6c feat: add entityClassName to metrics adapter events (#83) 2020-10-08 14:47:20 -07:00
Will Schurman d0cd8d88f2 v0.7.1 (#82) 2020-09-30 20:31:20 -07:00
Will Schurman f98f7d14b6 fix: correct type of runInTransactionAsync (#81) 2020-09-30 20:19:40 -07:00
Will Schurman f4b30cb704 v0.7.0 (#80) 2020-09-30 19:55:22 -07:00
Will Schurman 22aa907b11 docs: fix typedoc (#79) 2020-08-25 14:05:28 -07:00
Will Schurman 9891e74694 fix: upgrade packages (#78) 2020-08-25 13:38:00 -07:00
Will Schurman dbc3c81b35 fix: move cache invalidation to after transaction commit (#77) 2020-08-25 11:16:16 -07:00
Will Schurman 62bfd6c703 fix!: constuctor thrown exceptions for loadMany (#74) 2020-07-29 10:35:28 -07:00
Will Schurman 7e829f30fd fix: fix index export for EntityMutationTrigger and EntityMutationValidator (#73) 2020-07-28 15:09:45 -07:00
Will Schurman cd2df8da4b fix: fix incorrect loader types for field selecitons (#72) 2020-07-28 15:01:33 -07:00
Will Schurman b580a218c0 v0.6.0 (#69) 2020-07-23 14:48:27 -07:00
Will Schurman 547a1efcec fix: separate out EntityMutationValidator (#68) 2020-07-23 14:22:02 -07:00
Quinlan Jung fc4377d883 feat: entity mutation validators (#67)
* entity mutation validators

* pr feedback

* tests
2020-07-23 11:03:20 -07:00
Will Schurman 5744c51e6b fix: ensure transitive deletion triggers are run for CASCADE_DELETE_INVALIDATE_CACHE (#66) 2020-07-22 19:26:32 -07:00
Will Schurman fd6060cc84 feat: mutation triggers (#65) 2020-07-22 16:57:46 -07:00
Will Schurman a757867360 fix!: remove lodash dependency for vuln, require node 12 (#64) 2020-07-22 10:48:12 -07:00
Will Schurman 1224de79d5 feat!: add support for reference deletion and enclose mutations in transactions (#63) 2020-07-22 09:47:37 -07:00
Will Schurman 7a77afc83e fix: update setField to correct typing (#62) 2020-07-10 17:00:13 -07:00
Will Schurman 9c92d10b80 v0.5.2 (#60) 2020-07-01 22:04:27 -07:00
Will Schurman 7c180e84d9 fix: fix stub cache key generation for global cache (#59) 2020-07-01 22:00:55 -07:00
Will Schurman 1aa44e168e v0.5.1 (#58) 2020-07-01 16:32:35 -07:00
Will Schurman e91cfba993 fix: correct some typing issues and exports (#57) 2020-07-01 16:27:33 -07:00
Will Schurman 252d945917 Cut v0.5.0 (#56) 2020-07-01 11:43:46 -07:00
Will Schurman 4e40b2e521 feat: support entity fields subset of db fields (#49) 2020-07-01 11:23:55 -07:00
Will Schurman ccbdbc472a ci: fix checkout action to checkout branch at current state instead of merging into base branch (#55) 2020-07-01 10:45:01 -07:00
Will Schurman 89cfb3d5a0 feat: add two fan out methods to association loader (#52) 2020-06-30 09:26:05 -07:00
403 changed files with 66783 additions and 16496 deletions

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is Entity, a privacy-aware data layer for defining, caching, and authorizing access to application data models.
## Essential Development Commands
### Dependencies
- `yarn add` - Add a dependency. Run this only if necessary and ask before running.
### Building and Linting
- `yarn tsc` - Typecheck the code
- `yarn lint` - Run oxlint on source code
- `yarn format` - Format code with oxfmt
- `yarn format-check` - Check formatting without writing
- `yarn ctix` - Build barrel index.ts files
### Testing
- `yarn test` - Run unit tests
- `yarn integration` - Run integration tests against dockerized environment
### Commit messages
- All PRs use conventional commits for their titles: https://www.conventionalcommits.org/
## Code conventions
- Backwards compatibility is a non-goal of refactorings of this library. Consumers rely upon typescript to know what needs to be migrated, so all breaking changes should be detectable via running typescript post-entity-version-upgrade in a consumer application that makes use of this package.
- Do not use jest's `test.skip`
- Prefer not to use dynamic imports for now
- Whenever technically correct, use typescript types for something instead of `any`
- Prefer American English spellings of words over British English. For example, prefer "Adapter" over "Adaptor".
+54
View File
@@ -0,0 +1,54 @@
{
"mode": "bundle",
"fileExt": "keep",
"overwrite": true,
"backup": false,
"generationStyle": "default-alias-named-star",
"options": [
{
"project": "packages/entity/tsconfig.json",
"output": "packages/entity/src",
"exclude": [ "**/__testfixtures__/**", "**/__tests__/**" ]
},
{
"project": "packages/entity-cache-adapter-local-memory/tsconfig.json",
"output": "packages/entity-cache-adapter-local-memory/src",
"exclude": [ "**/__testfixtures__/**", "**/__tests__/**" ]
},
{
"project": "packages/entity-cache-adapter-redis/tsconfig.json",
"output": "packages/entity-cache-adapter-redis/src",
"exclude": [ "**/__testfixtures__/**", "**/__tests__/**" ]
},
{
"project": "packages/entity-database-adapter-knex/tsconfig.json",
"output": "packages/entity-database-adapter-knex/src",
"exclude": [ "**/__testfixtures__/**", "**/__tests__/**" ]
},
{
"project": "packages/entity-ip-address-field/tsconfig.json",
"output": "packages/entity-ip-address-field/src",
"exclude": [ "**/__testfixtures__/**", "**/__tests__/**" ]
},
{
"project": "packages/entity-secondary-cache-local-memory/tsconfig.json",
"output": "packages/entity-secondary-cache-local-memory/src",
"exclude": [ "**/__testfixtures__/**", "**/__tests__/**" ]
},
{
"project": "packages/entity-secondary-cache-redis/tsconfig.json",
"output": "packages/entity-secondary-cache-redis/src",
"exclude": [ "**/__testfixtures__/**", "**/__tests__/**" ]
},
{
"project": "packages/entity-testing-utils/tsconfig.json",
"output": "packages/entity-testing-utils/src",
"exclude": [ "**/__testfixtures__/**", "**/__tests__/**" ]
},
{
"project": "packages/entity-database-adapter-knex-testing-utils/tsconfig.json",
"output": "packages/entity-database-adapter-knex-testing-utils/src",
"exclude": [ "**/__testfixtures__/**", "**/__tests__/**" ]
}
]
}
-53
View File
@@ -1,53 +0,0 @@
{
"root": true,
"extends": [
"universe/node",
"universe/shared/typescript-analysis"
],
"plugins": [
"eslint-plugin-tsdoc"
],
"rules": {
"no-restricted-imports": ["error",{ "paths": [".", "..", "../.."] }],
"tsdoc/syntax": "warn"
},
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.d.ts"
],
"parserOptions": {
"project": "./tsconfig.json"
},
"rules": {
"@typescript-eslint/explicit-function-return-type": [
"warn",
{
"allowExpressions": true
}
],
"@typescript-eslint/naming-convention": [
"warn",
{
"selector": "typeLike",
"format": [
"PascalCase"
]
},
{
"selector": "enumMember",
"format": [
"UPPER_CASE"
]
}
],
"no-dupe-class-members": "off",
"@typescript-eslint/no-dupe-class-members": [
"error"
]
}
}
]
}
+1
View File
@@ -0,0 +1 @@
packages/entity-codemod/**/__testfixtures__/**
+1
View File
@@ -0,0 +1 @@
yarn.lock -diff linguist-generated
-27
View File
@@ -1,27 +0,0 @@
name: docs
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 10.x
- name: Cache Node.js modules
uses: actions/cache@v1
with:
path: ${{ github.workspace }}/node_modules
key: ${{ runner.OS }}-node_modules-${{ hashFiles('yarn.lock') }}
- run: yarn --frozen-lockfile
- run: yarn typedoc
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./doc
+20
View File
@@ -0,0 +1,20 @@
name: Lint PR
on:
pull_request_target:
types:
- opened
- reopened
- edited
- synchronize
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+45 -27
View File
@@ -2,35 +2,53 @@ name: tests
on:
push:
branches: [ master ]
branches: [main]
pull_request:
env:
# renovate: datasource=github-releases depName=jdx/mise
MISE_VERSION: v2026.9.7
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 10.x
- name: Cache Node.js modules
uses: actions/cache@v1
with:
path: ${{ github.workspace }}/node_modules
key: ${{ runner.OS }}-node_modules-${{ hashFiles('yarn.lock') }}
- run: yarn --frozen-lockfile
- run: yarn lint -- -- --max-warnings=0
- run: yarn test -- -- --coverage
- run: yarn integration -- -- --coverage
- uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./packages/*/coverage/clover.xml
flags: unittest
fail_ci_if_error: true
- uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./packages/*/coverage-integration/clover.xml
flags: integration
fail_ci_if_error: true
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
version: ${{ env.MISE_VERSION }}
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
cache: yarn
- run: mise run ci
- uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6.0.2
with:
token: ${{ secrets.CODECOV_TOKEN }}
directory: ./coverage
flags: unittest
fail_ci_if_error: true
- uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6.0.2
with:
token: ${{ secrets.CODECOV_TOKEN }}
directory: ./coverage-integration
flags: integration
fail_ci_if_error: true
docs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
version: ${{ env.MISE_VERSION }}
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
cache: yarn
- run: mise run typedoc
- name: Deploy
if: ${{ github.ref == 'refs/heads/main' }}
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./doc
+13 -1
View File
@@ -23,12 +23,24 @@ node_modules/
.yarn-integrity
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.pnp.*
# [yarn](https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored)
**/.yarn/*
!**/.yarn/cache
!**/.yarn/patches
!**/.yarn/plugins
!**/.yarn/releases
!**/.yarn/sdks
!**/.yarn/versions
**/.pnp.*
.nx/cache
.nx/workspace-data
# Entity-specific ignores
build/
+17
View File
@@ -0,0 +1,17 @@
{
"printWidth": 100,
"useTabs": false,
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"sortImports": {
"groups": [
["builtin", "external"],
["internal", "subpath"],
["parent", "sibling", "index"],
"unknown"
],
"ignoreCase": false
}
}
-6
View File
@@ -1,6 +0,0 @@
{
"printWidth": 100,
"tabWidth": 2,
"singleQuote": true,
"jsxBracketSameLine": true
}
+11 -2
View File
@@ -1,3 +1,12 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}
"typescript.tsdk": "node_modules/typescript/lib",
"oxc.fmt.configPath": ".oxfmtrc.json",
"editor.defaultFormatter": "oxc.oxc-vscode",
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
}
}
+7
View File
@@ -0,0 +1,7 @@
nodeLinker: node-modules
npmMinimalAgeGate: 2d
npmPreapprovedPackages:
- '@expo/*'
- 'oxlint-config-universe'
+635 -22
View File
@@ -1,26 +1,639 @@
# Changelog
# Change Log
## v0.4.0 (2020-06-03)
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
* All Packages
* Documentation Improvements
* CI & Coverage
* TypeScript 3.9 ([#18](https://github.com/expo/entity/pull/18))
* `@expo/entity`
* Enforcing mutator convenience functions ([#5](https://github.com/expo/entity/pull/5))
* `loadByIDNullable` loader method ([#28](https://github.com/expo/entity/pull/28))
* Add ability to check update and delete policies outside of mutation ([#30](https://github.com/expo/entity/pull/30))
* `@expo/entity-cache-adapter-redis`
* `@expo/entity-database-adapter-knex`
* Throw when database adapter receives unexpected mutation results ([#31](https://github.com/expo/entity/pull/31))
* `@expo/entity-example`
* Initial Version
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
## v0.3.0 (2020-04-28)
### Features
* `@expo/entity`
* Initial Version
* `@expo/entity-cache-adapter-redis`
* Initial Version
* `@expo/entity-database-adapter-knex`
* Initial Version
- add ID-based and field-based knex database loaders for use with locking modifiers ([#706](https://github.com/expo/entity/issues/706)) ([e87e2a3](https://github.com/expo/entity/commit/e87e2a3a28f4279f5911bfd0189964b94a2ced7f))
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
### Features
- add forShare and skipLocked selection modifiers to knex loader ([#705](https://github.com/expo/entity/issues/705)) ([243a038](https://github.com/expo/entity/commit/243a0386fb9755f3926ade8dcc1b92adf38cd923))
- add forUpdate to knex loader ([#704](https://github.com/expo/entity/issues/704)) ([727f166](https://github.com/expo/entity/commit/727f16662d8baa3757bb8e94f51d837f17fd333d))
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
**Note:** Version bump only for package root
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
**Note:** Version bump only for package root
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
### Bug Fixes
- **deps:** update dependency koa to v3.2.1 ([#634](https://github.com/expo/entity/issues/634)) ([1c17dc0](https://github.com/expo/entity/commit/1c17dc0f37ec68fb73aae1f92b3ce2c79062b93e))
### Features
- accept array of parent entity classes in cascade deletion rule ([#650](https://github.com/expo/entity/issues/650)) ([df75910](https://github.com/expo/entity/commit/df75910cbdc89df60ed2f23dbedf8862ed5c9077))
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
### Bug Fixes
- only throw EntityDatabaseAdapterPaginationCursorInvalidError from cursor decoding logic ([#537](https://github.com/expo/entity/issues/537)) ([e3cee07](https://github.com/expo/entity/commit/e3cee07091af8d1e6c5bb2d82f13e837c7658ea9))
- remove unused arg from EntityInvalidationUtils ([#536](https://github.com/expo/entity/issues/536)) ([0a1d1b5](https://github.com/expo/entity/commit/0a1d1b5972c15c932f5c31015f2519163f882f83))
# [0.61.0](https://github.com/expo/entity/compare/v0.60.0...v0.61.0) (2026-03-12)
### Bug Fixes
- **deps:** update dependency graphql to v16.13.1 ([#510](https://github.com/expo/entity/issues/510)) ([1a8535f](https://github.com/expo/entity/commit/1a8535f983f38aaa462a8d587e2f62f5fab1043c))
### Features
- update the SQLExpression and SQLFragmentHelpers APIs ([#516](https://github.com/expo/entity/issues/516)) ([112d4dc](https://github.com/expo/entity/commit/112d4dc9b379da5317ff6acaf1d2e9bd597128a6))
# [0.60.0](https://github.com/expo/entity/compare/v0.59.0...v0.60.0) (2026-03-06)
### Features
- Add SQLExpression class that supports fluent comparison method API ([#506](https://github.com/expo/entity/issues/506)) ([535b681](https://github.com/expo/entity/commit/535b6811e08578aba1aadb095f3e401cfe7eaa26))
# [0.59.0](https://github.com/expo/entity/compare/v0.58.0...v0.59.0) (2026-03-03)
### Bug Fixes
- **deps:** update dependency @koa/router to v15.3.1 ([#497](https://github.com/expo/entity/issues/497)) ([72b3c80](https://github.com/expo/entity/commit/72b3c80d12a1afa3c5593bcf6d7ad233c3524341))
### Features
- add support to SQLFragment for `= ANY(?)` ([#498](https://github.com/expo/entity/issues/498)) ([b931daa](https://github.com/expo/entity/commit/b931daa9a082cdb33f4559d6d32e1655f0459b79))
# [0.58.0](https://github.com/expo/entity/compare/v0.57.0...v0.58.0) (2026-03-02)
### Bug Fixes
- escape all matching portions of parts of cacher cache keys ([#486](https://github.com/expo/entity/issues/486)) ([21f2f6f](https://github.com/expo/entity/commit/21f2f6f461ee771e8b1c1b22dbaf9b275094c0db))
- fix some security review suggestions ([#485](https://github.com/expo/entity/issues/485)) ([e263bd2](https://github.com/expo/entity/commit/e263bd218cda579365766d0918c4961304bd1d9f))
# [0.57.0](https://github.com/expo/entity/compare/v0.56.0...v0.57.0) (2026-02-28)
**Note:** Version bump only for package root
# [0.56.0](https://github.com/expo/entity/compare/v0.55.0...v0.56.0) (2026-02-28)
### Bug Fixes
- Add default for TSelectedFields for application-instantiated types ([#438](https://github.com/expo/entity/issues/438)) ([3d4aa0e](https://github.com/expo/entity/commit/3d4aa0e9efa519a36f3db69bd8f117a993c100fa))
- Apply same viewer context subclass type mechanism to knexLoader and related methods ([#472](https://github.com/expo/entity/issues/472)) ([018a40b](https://github.com/expo/entity/commit/018a40bc01d579ffbf88f308e641921e6fc39097))
- correct discriminated union type for forward/backward pagination ([#474](https://github.com/expo/entity/issues/474)) ([90ae6e2](https://github.com/expo/entity/commit/90ae6e299bda26ac9a80d83e83334d4152c022dd))
- correct pagination behavior for standard pagination with descending ordering clauses ([#477](https://github.com/expo/entity/issues/477)) ([e32c4c2](https://github.com/expo/entity/commit/e32c4c2bcc05f225a556f95c90f1ee0f4e24de74))
- correct precedence parentheses for SQLFragment joins ([#476](https://github.com/expo/entity/issues/476)) ([2c4c649](https://github.com/expo/entity/commit/2c4c649a4bca5bc9ffdc41eb3135b2dfe4db5e57)), closes [/github.com/expo/entity/blob/main/packages/entity-database-adapter-knex/src/internal/EntityKnexDataManager.ts#L303](https://github.com//github.com/expo/entity/blob/main/packages/entity-database-adapter-knex/src/internal/EntityKnexDataManager.ts/issues/L303)
- **deps:** update dependency @apollo/server to v5.4.0 [security] ([#420](https://github.com/expo/entity/issues/420)) ([cd81c5a](https://github.com/expo/entity/commit/cd81c5a5b6be43ef60248eeea4031a883dd1eae8))
- **deps:** update dependency @koa/bodyparser to v6.1.0 ([#405](https://github.com/expo/entity/issues/405)) ([2dda22d](https://github.com/expo/entity/commit/2dda22d761f87851e3fcccc8443597daf8ac42e8))
- **deps:** update dependency @koa/router to v15.3.0 ([#406](https://github.com/expo/entity/issues/406)) ([9e88cf6](https://github.com/expo/entity/commit/9e88cf6aab76902c65994d28de8dab73f1267021))
- **deps:** update dependency graphql to v16.13.0 ([#479](https://github.com/expo/entity/issues/479)) ([976e2c3](https://github.com/expo/entity/commit/976e2c33867da71a2920d050c4086e2af8f6d54d))
- **deps:** update dependency koa to v3.1.2 [security] ([#484](https://github.com/expo/entity/issues/484)) ([b258dad](https://github.com/expo/entity/commit/b258dad2ed69c473f04d57d26910eab2032d142c))
- eliminate some unnecessary any casts ([#448](https://github.com/expo/entity/issues/448)) ([cc78029](https://github.com/expo/entity/commit/cc78029117c14510b0934b5e0d19876a45570f8d))
- explicitly document behavior when cursor entity no longer exists ([#453](https://github.com/expo/entity/issues/453)) ([4dc156c](https://github.com/expo/entity/commit/4dc156c7a8dc50bc9e2802be133b9e258b289f54)), closes [#422](https://github.com/expo/entity/issues/422) [#431](https://github.com/expo/entity/issues/431)
- move EntityPrivacyUtils back into core package ([#429](https://github.com/expo/entity/issues/429)) ([56ec27d](https://github.com/expo/entity/commit/56ec27d5f201d83930a2cc05dfc380a054bff1e1))
- pass in augmentable classes to installation ([#437](https://github.com/expo/entity/issues/437)) ([f5c728b](https://github.com/expo/entity/commit/f5c728be565e01c0a8ace9ef51f6de8f49184dab)), closes [/github.com/expo/entity/pull/410#pullrequestreview-3765893898](https://github.com//github.com/expo/entity/pull/410/issues/pullrequestreview-3765893898)
- refactor install method to instead be free functions ([#441](https://github.com/expo/entity/issues/441)) ([51d632d](https://github.com/expo/entity/commit/51d632da1619dc039d5c41ce6533f63254ec4337)), closes [#410](https://github.com/expo/entity/issues/410)
- Reorganize constructionUtils to make non-public ([#454](https://github.com/expo/entity/issues/454)) ([d9377a2](https://github.com/expo/entity/commit/d9377a290b7e0c6803f9e83537eddb7204bc9320)), closes [#410](https://github.com/expo/entity/issues/410)
- revert change to codemod now that Entity.knexLoader is supported again via subclass ([#443](https://github.com/expo/entity/issues/443)) ([ed6b2e0](https://github.com/expo/entity/commit/ed6b2e05539c77ee2abd2c6da74e2a451967d06e)), closes [#441](https://github.com/expo/entity/issues/441)
- Type idField in EntityConfiguration as TIDField ([#425](https://github.com/expo/entity/issues/425)) ([be40bcf](https://github.com/expo/entity/commit/be40bcffa6e7db9b7ad75e1a80bbb48ff417c6c5))
- update integration test to use BlahEntity.knexLoader syntax ([#444](https://github.com/expo/entity/issues/444)) ([457f58a](https://github.com/expo/entity/commit/457f58ad2c4e1cf09f3132f3b9af5008ba0290c7)), closes [#441](https://github.com/expo/entity/issues/441)
- Use TSelectedFields for knex loader order by method ([#424](https://github.com/expo/entity/issues/424)) ([a9d09f4](https://github.com/expo/entity/commit/a9d09f42b67378d06133bcfb5b99c9afb17aac0d))
### Features
- Add authorization action to EntityPrivacyPolicyRuleEvaluationContext ([#464](https://github.com/expo/entity/issues/464)) ([e71ae5f](https://github.com/expo/entity/commit/e71ae5fcbae95096e0d6a42a8c960896ab83ccfb))
- add codemod for upcoming v0.55.0-v0.56.0 ([#413](https://github.com/expo/entity/issues/413)) ([aba0ce5](https://github.com/expo/entity/commit/aba0ce5b5f38cb7b01956cde14a4714e2a03abb4)), closes [#407](https://github.com/expo/entity/issues/407) [-#410](https://github.com/-/issues/410)
- add entity-database-adapter-knex-testing-utils containing StubPostgresDatabaseAdapter ([#412](https://github.com/expo/entity/issues/412)) ([eeccf59](https://github.com/expo/entity/commit/eeccf594c11bf090aa83bbf71fb4ead8668aec7d))
- add entityField SQL helper ([#481](https://github.com/expo/entity/issues/481)) ([459a5ff](https://github.com/expo/entity/commit/459a5ff85014a8c7396c0856dffacb1491ccb963))
- Add ilike and trigram similarity search to pagination ([#431](https://github.com/expo/entity/issues/431)) ([f72cbca](https://github.com/expo/entity/commit/f72cbcaf5a722e6bb8299644ec32b07f1d3362a9)), closes [#422](https://github.com/expo/entity/issues/422)
- add method to get pagination cursor for single entity ([#475](https://github.com/expo/entity/issues/475)) ([898f71c](https://github.com/expo/entity/commit/898f71ce082c2593585575b60bd6ef2546717124))
- Add paginated loader to entity-database-adapter-knex ([#422](https://github.com/expo/entity/issues/422)) ([58deffd](https://github.com/expo/entity/commit/58deffd426b0214073ede608cd47c04db2596d60))
- Add pagination max page size configuration ([#436](https://github.com/expo/entity/issues/436)) ([3ec4b5f](https://github.com/expo/entity/commit/3ec4b5f0cd53bf18d6bcfa27dd29b70d3f4d5f19))
- add PostgresEntity/ReadonlyPostgresEntity classes that expose knexLoader methods ([#442](https://github.com/expo/entity/issues/442)) ([40b409f](https://github.com/expo/entity/commit/40b409f6f889febf8b90b8837caecac26ff64174)), closes [#441](https://github.com/expo/entity/issues/441)
- Add raw sqlfragment orderBy capability for pagination ([#457](https://github.com/expo/entity/issues/457)) ([0615ffa](https://github.com/expo/entity/commit/0615ffacffbe9e903a9bf03b171c102f0554b992))
- add support for nulls ordering in order by clauses ([#468](https://github.com/expo/entity/issues/468)) ([6c0838d](https://github.com/expo/entity/commit/6c0838dc577a3b6190e3551ee834925d7bd53bc6))
- support nullable search fields with postgres transforms ([#470](https://github.com/expo/entity/issues/470)) ([53b0604](https://github.com/expo/entity/commit/53b0604152b8ca5ad9fc616d18478f93473c7eb0))
# [0.55.0](https://github.com/expo/entity/compare/v0.54.0...v0.55.0) (2026-01-27)
### Bug Fixes
- **deps:** update dependency @apollo/server to v5.3.0 ([#403](https://github.com/expo/entity/issues/403)) ([b758c4d](https://github.com/expo/entity/commit/b758c4d344294797430de0afb88e47f558b6fa8d))
### Features
- add AllowIf[Any|All]SubRulesAllowPrivacyPolicyRule rules ([#401](https://github.com/expo/entity/issues/401)) ([5db8977](https://github.com/expo/entity/commit/5db89777c605eda9a8393227ceeb641e460dc463))
- Add AllowIfInParentCascadeDeletionPrivacyPolicyRule ([#399](https://github.com/expo/entity/issues/399)) ([56ed357](https://github.com/expo/entity/commit/56ed357b923a8374a3c120e3eef5d487d5fb2f9c))
- add EvaluateIfEntityFieldPredicatePrivacyPolicyRule ([#400](https://github.com/expo/entity/issues/400)) ([f3b575c](https://github.com/expo/entity/commit/f3b575cd00d47c747cf45e96c6db7857592b0f93))
# [0.54.0](https://github.com/expo/entity/compare/v0.53.0...v0.54.0) (2026-01-21)
### Bug Fixes
- **deps:** pin dependencies ([#328](https://github.com/expo/entity/issues/328)) ([5679f27](https://github.com/expo/entity/commit/5679f27209f1515ca5e626d858c4aa054b414625))
- **deps:** pin dependency @apollo/server to 4.12.2 ([#330](https://github.com/expo/entity/issues/330)) ([7038912](https://github.com/expo/entity/commit/7038912ca5e5b203ebbca4b46bda5612e7660eac))
- **deps:** pin dependency @koa/bodyparser to 6.0.0 ([#339](https://github.com/expo/entity/issues/339)) ([9f93926](https://github.com/expo/entity/commit/9f939269cf843059ec0ac729d470db074730348e))
- **deps:** pin dependency koa to 2.16.3 ([#331](https://github.com/expo/entity/issues/331)) ([ce55922](https://github.com/expo/entity/commit/ce55922e3ec6692a9ea4847427dc475902174830))
- Inline map().filter() loops in EntityLoaderUtils ([#369](https://github.com/expo/entity/issues/369)) ([ea6c128](https://github.com/expo/entity/commit/ea6c128d9c8fc21484645796f79e1aa8839dd395))
- run tsc during tests CI job ([#373](https://github.com/expo/entity/issues/373)) ([1cc57ed](https://github.com/expo/entity/commit/1cc57eddce72663cff17a5102afa6c3e8248aee4))
### Features
- synthetically set cascading set-null fields to null during canViewerDeleteAsync evaluation ([#376](https://github.com/expo/entity/issues/376)) ([544bb7c](https://github.com/expo/entity/commit/544bb7c3f2f41419cb3b779f7e3d968e09455fa0))
# [0.53.0](https://github.com/expo/entity/compare/v0.52.0...v0.53.0) (2025-12-19)
**Note:** Version bump only for package root
# [0.52.0](https://github.com/expo/entity/compare/v0.51.0...v0.52.0) (2025-12-19)
### Features
- switch local cache to an interface ([#322](https://github.com/expo/entity/issues/322)) ([c453726](https://github.com/expo/entity/commit/c4537262d810ea01e519f9aec6ae5b17a54ce936))
# [0.51.0](https://github.com/expo/entity/compare/v0.50.0...v0.51.0) (2025-11-21)
### Bug Fixes
- add default for TSelectedFields in PrivacyPolicyTestUtils Case ([#316](https://github.com/expo/entity/issues/316)) ([2f9d487](https://github.com/expo/entity/commit/2f9d4873aaefe891920c25a41088f7c467da6113))
# [0.50.0](https://github.com/expo/entity/compare/v0.49.0...v0.50.0) (2025-10-07)
### Bug Fixes
- make in memory and stub database adapters behave more similarly to sql ([#315](https://github.com/expo/entity/issues/315)) ([0f53c24](https://github.com/expo/entity/commit/0f53c24faff8a994906e630d7f17d67d89ff5b0f)), closes [#311](https://github.com/expo/entity/issues/311)
# [0.49.0](https://github.com/expo/entity/compare/v0.48.0...v0.49.0) (2025-10-07)
### Bug Fixes
- add readonly modifier to partitionArray utility method ([#307](https://github.com/expo/entity/issues/307)) ([077d53c](https://github.com/expo/entity/commit/077d53cd86d4c54132ea2ebcc59d1d75e2b507d0))
- validate that UUID field is lower-case ([#310](https://github.com/expo/entity/issues/310)) ([949f00d](https://github.com/expo/entity/commit/949f00d93b52e405188b9b23e23ae59ce1c546de)), closes [/github.com/expo/entity/blob/077d53cd86d4c54132ea2ebcc59d1d75e2b507d0/packages/entity/src/EntityDatabaseAdapter.ts#L182](https://github.com//github.com/expo/entity/blob/077d53cd86d4c54132ea2ebcc59d1d75e2b507d0/packages/entity/src/EntityDatabaseAdapter.ts/issues/L182) [#309](https://github.com/expo/entity/issues/309)
### Features
- add more documentation to entity errors ([#312](https://github.com/expo/entity/issues/312)) ([4c3fd17](https://github.com/expo/entity/commit/4c3fd175a91cf3107b1ac413e39e52e051a9dc4b))
- throw specific errors for database adapter issues ([#311](https://github.com/expo/entity/issues/311)) ([f6639f9](https://github.com/expo/entity/commit/f6639f9def4ac20b628dd48fdca8d7cf61b6347b))
# [0.48.0](https://github.com/expo/entity/compare/v0.47.0...v0.48.0) (2025-09-19)
**Note:** Version bump only for package root
# [0.47.0](https://github.com/expo/entity/compare/v0.46.0...v0.47.0) (2025-09-19)
### Features
- add support for Buffer fields ([#305](https://github.com/expo/entity/issues/305)) ([31b52a5](https://github.com/expo/entity/commit/31b52a5f75842f4810f66c12ad03a10d9192f889))
- allow manual specification of cascadingDeleteCause in delete mutator factory method ([#306](https://github.com/expo/entity/issues/306)) ([4a3eda2](https://github.com/expo/entity/commit/4a3eda209fa747ee093ca3053b3196bf9a52193d))
# [0.46.0](https://github.com/expo/entity/compare/v0.45.0...v0.46.0) (2025-06-26)
**Note:** Version bump only for package root
# [0.45.0](https://github.com/expo/entity/compare/v0.44.0...v0.45.0) (2025-06-06)
### Features
- add ability to define a custom cache key invalidation version supplier ([#290](https://github.com/expo/entity/issues/290)) ([a3ab61b](https://github.com/expo/entity/commit/a3ab61bfa8d2eab3bbd9c703c5e483184bb67f13))
- add createOrGetExistingAsync and createWithUniqueConstraintRecoveryAsync utility methods ([#291](https://github.com/expo/entity/issues/291)) ([78bc264](https://github.com/expo/entity/commit/78bc264b3328692b4b6722388a57d298f02e65c5))
# [0.44.0](https://github.com/expo/entity/compare/v0.43.0...v0.44.0) (2025-05-29)
### Bug Fixes
- introduce transactional dataloader mode and fix global transaction ID generation ([#289](https://github.com/expo/entity/issues/289)) ([d3e89bd](https://github.com/expo/entity/commit/d3e89bdd82e6df5c5b35522bc3574fd9e1cfddff))
### Features
- add transaction-scoped dataloaders ([#284](https://github.com/expo/entity/issues/284)) ([e054a0d](https://github.com/expo/entity/commit/e054a0d74b3a39158ca7d73d05f59d3633113400)), closes [#98](https://github.com/expo/entity/issues/98) [#194](https://github.com/expo/entity/issues/194)
# [0.43.0](https://github.com/expo/entity/compare/v0.42.0...v0.43.0) (2025-04-10)
### Bug Fixes
- fix generated tsc definition sentinel type visibility ([#283](https://github.com/expo/entity/issues/283)) ([9758fd5](https://github.com/expo/entity/commit/9758fd5df8695a8121258cd988c0c79ce8a3e4b6))
# [0.42.0](https://github.com/expo/entity/compare/v0.41.0...v0.42.0) (2025-04-10)
### Bug Fixes
- fix metrics for dataloader waterfall logging ([#267](https://github.com/expo/entity/issues/267)) ([1d95eba](https://github.com/expo/entity/commit/1d95eba1f9efb67164c3affc917c9b2514e1529f))
- move source of truth of loader docblocks to enforcing loader since it is not the default loader ([#264](https://github.com/expo/entity/issues/264)) ([aa56b70](https://github.com/expo/entity/commit/aa56b705db115cf760125208eb2d3585a3794e58))
### Features
- add @expo/entity-testing-utils package ([#280](https://github.com/expo/entity/issues/280)) ([485894a](https://github.com/expo/entity/commit/485894af16e233d533a9480285bbda56812cbe0b))
- add composite field loading and caching ([#272](https://github.com/expo/entity/issues/272)) ([f0aa0da](https://github.com/expo/entity/commit/f0aa0dafebdb56418cbd22dda437233d850db3e7))
- add some utility classes and methods in preparation for composite keys ([#269](https://github.com/expo/entity/issues/269)) ([ad41af1](https://github.com/expo/entity/commit/ad41af1c826fb267f5e463771107c85802578a19)), closes [#201](https://github.com/expo/entity/issues/201)
- change entity id field generic to field name and derive type where necessary ([#278](https://github.com/expo/entity/issues/278)) ([b7e524c](https://github.com/expo/entity/commit/b7e524c41892797608d0b884410b8d520a80a9ef))
- convert batched/cached loader interface to holder pattern ([#271](https://github.com/expo/entity/issues/271)) ([06b3cb7](https://github.com/expo/entity/commit/06b3cb7f8076ef9e5b83cdf154da159801bd14cc)), closes [#201](https://github.com/expo/entity/issues/201)
- enforce explicit id field cache property ([#276](https://github.com/expo/entity/issues/276)) ([1da5cc0](https://github.com/expo/entity/commit/1da5cc01e1affc3b3338a0ab2050504b5853ac6f))
- invalidate n+/-1 cacheKeyVersion for entities for push safety ([#275](https://github.com/expo/entity/issues/275)) ([d9c1852](https://github.com/expo/entity/commit/d9c1852706f98f0761ff24ddd244e4ed2beca580))
# [0.41.0](https://github.com/expo/entity/compare/v0.40.0...v0.41.0) (2025-03-07)
- feat!: Default all loaders and mutators to enforcing (#256) ([a976e76](https://github.com/expo/entity/commit/a976e765098bca27049585e43a3bed5c71b9ac6b)), closes [#256](https://github.com/expo/entity/issues/256)
### Features
- Create codemod for v0.40.0 -> v0.41.0 ([#260](https://github.com/expo/entity/issues/260)) ([4a2b61f](https://github.com/expo/entity/commit/4a2b61f2bd8edfc19109ef4c8ac780eaf0f7795c))
### BREAKING CHANGES
- Default all loaders and mutators to enforcing
# [0.40.0](https://github.com/expo/entity/compare/v0.39.0...v0.40.0) (2025-02-27)
### Features
- Create codemod for v0.39.0 -> v0.40.0 ([#259](https://github.com/expo/entity/issues/259)) ([d63ddd6](https://github.com/expo/entity/commit/d63ddd65997a441a914a71b158a111339970aeab))
### Reverts
- Revert "v0.40.0" ([fa78ce8](https://github.com/expo/entity/commit/fa78ce84e2f2eb8199e36fd51779fc68905001c0))
# [0.39.0](https://github.com/expo/entity/compare/v0.38.0...v0.39.0) (2024-11-21)
### Features
- add function to get authorization results for canViewerUpdateAsync/canViewerDeleteAsync ([#249](https://github.com/expo/entity/issues/249)) ([27a358d](https://github.com/expo/entity/commit/27a358d259503aa8d7d7413bfc1e52e0974dc626))
# [0.38.0](https://github.com/expo/entity/compare/v0.37.0...v0.38.0) (2024-06-27)
### Features
- add EntityEdgeDeletionAuthorizationInferenceBehavior for canViewerDeleteAsync ([#243](https://github.com/expo/entity/issues/243)) ([162b192](https://github.com/expo/entity/commit/162b19257ac720c94fb84d64808fb75748adcdc7))
# [0.37.0](https://github.com/expo/entity/compare/v0.36.0...v0.37.0) (2024-06-12)
### Bug Fixes
- update barrels ([#241](https://github.com/expo/entity/issues/241)) ([8c5f81b](https://github.com/expo/entity/commit/8c5f81bcca107e9b7bbea5f0ab41b0105057806e))
# [0.36.0](https://github.com/expo/entity/compare/v0.35.0...v0.36.0) (2024-06-12)
### Breaking Changes
- make using non-enforcing entity loader explicit ([#238](https://github.com/expo/entity/issues/238)) ([2edc7af](https://github.com/expo/entity/commit/2edc7af7e20c362688ff2ee674fa2245205d6de4))
### Bug Fixes
- always reload entity after update since cascading changes may have changed it since commit ([#233](https://github.com/expo/entity/issues/233)) ([7c3c985](https://github.com/expo/entity/commit/7c3c9854a4dd91d4b73ebdb18bdeadea8b63f4c7))
- constrain entity fields type to string-keyed object ([#235](https://github.com/expo/entity/issues/235)) ([7e2cea1](https://github.com/expo/entity/commit/7e2cea16973a0ae1917f867cd25d6ef7c8eaecef))
- disallow keys of JS Object prototype for safety ([#236](https://github.com/expo/entity/issues/236)) ([05726d4](https://github.com/expo/entity/commit/05726d45fa9b91a6b25ddbc9c7e3e097b02097ca))
### Features
- Add global mutation trigger field to EntityCompanionProvider ([#215](https://github.com/expo/entity/issues/215)) ([6569486](https://github.com/expo/entity/commit/6569486ecddf2fd33c626f634c761e06636b1730))
- add previousValue to privacy policy context for updates ([#232](https://github.com/expo/entity/issues/232)) ([af495a9](https://github.com/expo/entity/commit/af495a9a856a5042f00cfd150348e415139cb85e))
- Add StrictEnumField with better validation ([#222](https://github.com/expo/entity/issues/222)) ([8753252](https://github.com/expo/entity/commit/87532525d090b3a67f1cedd95ac990f7dcb3ab5d))
- make canViewerDeleteAsync recursive ([#224](https://github.com/expo/entity/issues/224)) ([60fc9a4](https://github.com/expo/entity/commit/60fc9a4c4b587b866f2f5a1b3bd22145bc77e6e9))
- Use uuid v7 in stub database adapter ([#234](https://github.com/expo/entity/issues/234)) ([1e8ea64](https://github.com/expo/entity/commit/1e8ea64017430a2ce5ea197678bd78713ad77ecd))
- move entity loader utils into their own object ([#239](https://github.com/expo/entity/issues/239))([93905be](https://github.com/expo/entity/commit/93905be81cf74c5626ca19358794261c17223f95))
# [0.35.0](https://github.com/expo/entity/compare/v0.34.0...v0.35.0) (2024-04-11)
### Bug Fixes
- Revert: require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([1bdfbd6](https://github.com/expo/entity/commit/1bdfbd6b562d1200e4029df0533b5adb2d917831))
# [0.34.0](https://github.com/expo/entity/compare/v0.33.0...v0.34.0) (2024-04-11)
### Features
- Allow UUIDv6/7/8 in UUIDField validation ([#221](https://github.com/expo/entity/issues/221)) ([cc6f3dc](https://github.com/expo/entity/commit/cc6f3dc1e677382eec220723c37b5b883d980372))
- require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([8b0b31f](https://github.com/expo/entity/commit/8b0b31fdde5bd565aa527719003ef283a45f55cc))
# [0.33.0](https://github.com/expo/entity/compare/v0.32.0...v0.33.0) (2023-10-06)
### Features
- add loadManyByIDsNullableAsync loader method ([#214](https://github.com/expo/entity/issues/214)) ([aad94ac](https://github.com/expo/entity/commit/aad94aca479ac07cc7f76b5e1a122ff85a2d1542))
# [0.32.0](https://github.com/expo/entity/compare/v0.31.1...v0.32.0) (2023-06-10)
### Bug Fixes
- remove unused field from redis configuration ([#213](https://github.com/expo/entity/issues/213)) ([503a2fb](https://github.com/expo/entity/commit/503a2fb6a8d57f6a09f6da99daf5ab92fc3e7945))
### Features
- add ability to specify knex transaction config ([#207](https://github.com/expo/entity/issues/207)) ([2069a0d](https://github.com/expo/entity/commit/2069a0d9fd1d8f7d090a71a90a758b2429ea5dd3))
- entity companion definition static thunk ([#210](https://github.com/expo/entity/issues/210)) ([4b18010](https://github.com/expo/entity/commit/4b18010d42be50ef329f428b08330e21bf676586))
# [0.31.1](https://github.com/expo/entity/compare/v0.31.0...v0.31.1) (2023-03-06)
### Features
- add loadFirstByFieldEqualityConjunction, a convenience method ([#206](https://github.com/expo/entity/pull/206)) ([1934216](https://github.com/expo/entity/commit/1934216b70572cf2b9693e0f14939f49b90427dd))
# [0.31.0](https://github.com/expo/entity/compare/v0.30.0...v0.31.0) (2023-01-19)
**Note:** Version bump only for package root
# [0.30.0](https://github.com/expo/entity/compare/v0.29.0...v0.30.0) (2022-12-23)
**Note:** Version bump only for package root
# [0.29.0](https://github.com/expo/entity/compare/v0.28.0...v0.29.0) (2022-10-06)
### Features
- add example of redis mget batching using @expo/batcher ([#199](https://github.com/expo/entity/issues/199)) ([684f91b](https://github.com/expo/entity/commit/684f91b1f270851dbe331df2e2620664eea17266))
- change redis cache adapter to use redis-like interface ([#200](https://github.com/expo/entity/issues/200)) ([e381f5b](https://github.com/expo/entity/commit/e381f5b2acfc17dcade2cf6e8506fa1c1c255e73))
# [0.28.0](https://github.com/expo/entity/compare/v0.27.0...v0.28.0) (2022-09-13)
### Features
- add BigIntField ([#193](https://github.com/expo/entity/issues/193)) ([acbad82](https://github.com/expo/entity/commit/acbad826a37bb5a054b4dcf9be91d11aa5f7f29a))
- nested transactions ([#194](https://github.com/expo/entity/issues/194)) ([a77b914](https://github.com/expo/entity/commit/a77b914ad6dc018d1e309c9caf4971088a553ecf))
# [0.27.0](https://github.com/expo/entity/compare/v0.26.1...v0.27.0) (2022-08-09)
### Features
- add orderByRaw to loadManyByRawWhereClauseAsync ([#185](https://github.com/expo/entity/issues/185)) ([2817d78](https://github.com/expo/entity/commit/2817d78392dfecd0093111c24d581bdf01dba06d))
## [0.26.1](https://github.com/expo/entity/compare/v0.26.0...v0.26.1) (2022-07-28)
### Features
- recache to higher cache levels when loading from a composed cache ([#182](https://github.com/expo/entity/pull/182))
# [0.26.0](https://github.com/expo/entity/compare/v0.25.3...v0.26.0) (2022-07-14)
### Bug Fixes
- fix workflow branch specification ([#179](https://github.com/expo/entity/issues/179)) ([25d14bf](https://github.com/expo/entity/commit/25d14bf8c1688524ad488aa2b63221393a4f0b05))
### Features
- add EntityEdgeDeletionBehavior.SET_NULL_INVALIDATE_CACHE_ONLY ([#178](https://github.com/expo/entity/issues/178)) ([a0c35dd](https://github.com/expo/entity/commit/a0c35ddd7bca05d0c3f41dcf48e16d058918231c))
- update ioredis to v5, make it a devDependency of the cache packages ([#172](https://github.com/expo/entity/issues/172)) ([9fcfbf7](https://github.com/expo/entity/commit/9fcfbf746a61554cefedf7fb0f1a3a9056b58ffe))
## [0.25.3](https://github.com/expo/entity/compare/v0.25.2...v0.25.3) (2022-03-12)
### Bug Fixes
- lock lru-cache to v6.0.0 ([#170](https://github.com/expo/entity/issues/170)) ([293868b](https://github.com/expo/entity/commit/293868b1a73df9f749216af68b1ae94ac22ac2b9))
## [0.25.2](https://github.com/expo/entity/compare/v0.25.1...v0.25.2) (2022-03-12)
### Bug Fixes
- lru-cache yarn workspace version resolution ([#168](https://github.com/expo/entity/issues/168)) ([e76ca8c](https://github.com/expo/entity/commit/e76ca8c2f72da84aa372a64ef1898ade220ed0a8))
## [0.25.1](https://github.com/expo/entity/compare/v0.25.0...v0.25.1) (2022-03-10)
### Bug Fixes
- add composed cache to index ([#166](https://github.com/expo/entity/issues/166)) ([5c82edc](https://github.com/expo/entity/commit/5c82edcd9f26854878d29950a05852e117bbd723))
# [0.25.0](https://github.com/expo/entity/compare/v0.24.0...v0.25.0) (2022-03-10)
### Features
- composed cache adapter, composed secondary cache, local memory secondary cache ([#164](https://github.com/expo/entity/issues/164)) ([6b6e70e](https://github.com/expo/entity/commit/6b6e70e75f7e42a4dce13edeb406a4d88d9264f0))
- privacy policy evaluation context ([#162](https://github.com/expo/entity/issues/162)) ([3a81a4c](https://github.com/expo/entity/commit/3a81a4c059e2e995eb800e35e76b3d432c22b23f))
# [0.24.0](https://github.com/expo/entity/compare/v0.23.0...v0.24.0) (2022-02-16)
### Features
- add local memory cache package ([#158](https://github.com/expo/entity/issues/158)) ([85d3575](https://github.com/expo/entity/commit/85d35752fa0919a62efb776a762a6829e83ca7af))
### Chores
- Refactor cachers to use an abstract class and interface ([#157](https://github.com/expo/entity/pull/157))
# [0.23.0](https://github.com/expo/entity/compare/v0.22.0...v0.23.0) (2022-02-09)
### Breaking changes
- Move data transforming responsibilities to the CacheAdapter instead of the ReadThroughEntityCache. Affects any classes extending the EntityCacheAdapter and relying on the transformation behavior of the ReadThroughEntityCache. ([#153](https://github.com/expo/entity/pull/153))
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
### Features
- Upgrade knex to 1.x in `@expo/entity-database-adapter-knex` ([#150](https://github.com/expo/entity/issues/150)) ([e1b0ee5](https://github.com/expo/entity/commit/e1b0ee528175646239d88aa71ffce73983eeb52f))
# [0.21.0](https://github.com/expo/entity/compare/v0.20.0...v0.21.0) (2022-01-03)
### Features
- add pre-commit callbacks on EntityQueryContext ([#147](https://github.com/expo/entity/issues/147)) ([f1d9847](https://github.com/expo/entity/commit/f1d9847210f5775de964509e44ce493e44482b21))
# [0.20.0](https://github.com/expo/entity/compare/v0.19.0...v0.20.0) (2021-12-29)
### Bug Fixes
- separate post-commit invalidation callbacks from trigger and user callbacks ([#144](https://github.com/expo/entity/issues/144)) ([efa131e](https://github.com/expo/entity/commit/efa131ef29532accf0e51b6d867ffe15a27b4c90))
### Features
- add cascading deletion info ([#145](https://github.com/expo/entity/issues/145)) ([3727191](https://github.com/expo/entity/commit/372719176742d3ead6b7cbca29cb66b09b3fe09c))
# [0.19.0](https://github.com/expo/entity/compare/v0.18.0...v0.19.0) (2021-10-28)
### Features
- add authorization metrics ([#142](https://github.com/expo/entity/issues/142)) ([7cf54d8](https://github.com/expo/entity/commit/7cf54d83c52e104159a3e2e675cdda2d29afe204))
- add IP support ([#138](https://github.com/expo/entity/issues/138)) ([1405a91](https://github.com/expo/entity/commit/1405a9126ee3a56c3e2a38bf3b697283744f5b5d))
# [0.18.0](https://github.com/expo/entity/compare/v0.17.0...v0.18.0) (2021-10-14)
### Features
- enable noImplicitOverride tsc setting ([#135](https://github.com/expo/entity/issues/135)) ([4263cb9](https://github.com/expo/entity/commit/4263cb9b8b69fe4fb68c74ec1c7aba04508a1555))
- expose describeFieldTestCase for use in other packages ([#139](https://github.com/expo/entity/issues/139)) ([1220477](https://github.com/expo/entity/commit/1220477aa331349b482ad015b44f76fa2bda7b90))
- upgrade TypeScript to 4.4 ([#134](https://github.com/expo/entity/issues/134)) ([7612392](https://github.com/expo/entity/commit/7612392dbfd9778d25c465c1626c372d75a6d05a))
- use better QueryContext types for transactional triggers and validators ([#136](https://github.com/expo/entity/issues/136)) ([5c7efe1](https://github.com/expo/entity/commit/5c7efe1678424ff907687e9327175bdc2538c3e0))
# [0.17.0](https://github.com/expo/entity/compare/v0.16.0...v0.17.0) (2021-08-16)
### Features
- Add IntField and FloatField, deprecate NumberField ([#131](https://github.com/expo/entity/issues/131)) ([2f2d963](https://github.com/expo/entity/commit/2f2d963cf92c6575d78f8eeca344a4aadcc9b8d9))
# [0.16.0](https://github.com/expo/entity/compare/v0.15.0...v0.16.0) (2021-07-07)
### Features
- allow null field values in loadManyByFieldEqualityConjunctionAsync ([#130](https://github.com/expo/entity/issues/130)) ([2f37dc6](https://github.com/expo/entity/commit/2f37dc6a37d368b12b3c8375e04f0777fd448797))
# [0.15.0](https://github.com/expo/entity/compare/v0.14.1...v0.15.0) (2021-05-26)
### Bug Fixes
- upgrade ioredis, knex, and pg ([#125](https://github.com/expo/entity/issues/125)) ([7c43edf](https://github.com/expo/entity/commit/7c43edf3de37e8af13e2a99bd44f03f54803d9b9))
- upgrade some other packages to fix dependabot alerts ([#126](https://github.com/expo/entity/issues/126)) ([226bffe](https://github.com/expo/entity/commit/226bffe0a95d7b8a3e9e9d46aebdb0edf1eeec1a))
- use column name instead of field name for redis cache key ([#124](https://github.com/expo/entity/issues/124)) ([d78f452](https://github.com/expo/entity/commit/d78f452bb3ac5527069813a03f5b2264375bd8ab))
### Features
- secondary cache loader ([#123](https://github.com/expo/entity/issues/123)) ([4cba01e](https://github.com/expo/entity/commit/4cba01eb259c87d60b3026ce776e46f781363690))
## [0.14.1](https://github.com/expo/entity/compare/v0.14.0...v0.14.1) (2021-03-24)
### Bug Fixes
- typescript typedef of EntityMutationInfo ([#121](https://github.com/expo/entity/issues/121)) ([8e8f0dc](https://github.com/expo/entity/commit/8e8f0dc8636e343e29a6012d155e4111477a8989))
# [0.14.0](https://github.com/expo/entity/compare/v0.13.0...v0.14.0) (2021-03-24)
### Bug Fixes
- mutation validator and trigger cyclic import structure ([#118](https://github.com/expo/entity/issues/118)) ([b11dbc0](https://github.com/expo/entity/commit/b11dbc087ba5ad13c748032698dd73279b7960b0))
### Features
- add mutation type to validator and trigger execute functions ([#119](https://github.com/expo/entity/issues/119)) ([ef40fa5](https://github.com/expo/entity/commit/ef40fa5e9cdf53e25e78a9423c698e6d1c9737af))
# [0.13.0](https://github.com/expo/entity/compare/v0.12.0...v0.13.0) (2021-02-12)
### Bug Fixes
- allow undefined value for all fields ([#114](https://github.com/expo/entity/issues/114)) ([022e0e9](https://github.com/expo/entity/commit/022e0e9b88a5e84e0c1bc6c8810c850beb05f0cf))
### Features
- add basic field type runtime validators ([#113](https://github.com/expo/entity/issues/113)) ([6c4d4b0](https://github.com/expo/entity/commit/6c4d4b03b5404dd776a12c5eb6f0af77a8c964f8))
# [0.12.0](https://github.com/expo/entity/compare/v0.11.0...v0.12.0) (2021-01-22)
### Bug Fixes
- move runInTransaction to ViewerContext ([#108](https://github.com/expo/entity/issues/108)) ([b7309e1](https://github.com/expo/entity/commit/b7309e18a2dd780b64cd306d566de1f12bf8df3a))
- remove cache and database adaptor flavor enums ([#109](https://github.com/expo/entity/issues/109)) ([72a77f8](https://github.com/expo/entity/commit/72a77f8b6893a4b76d3b1cce16659bdf3ce473ee))
### Features
- better typing on filterMap for use with predicates ([#106](https://github.com/expo/entity/issues/106)) ([01b3bfe](https://github.com/expo/entity/commit/01b3bfef7c6da55e7d89d8cccb52a0050187aee8))
# [0.11.0](https://github.com/expo/entity/compare/v0.10.0...v0.11.0) (2020-12-24)
### Bug Fixes
- add type generic default in test utils ([#104](https://github.com/expo/entity/issues/104)) ([fe2917e](https://github.com/expo/entity/commit/fe2917e6b5af808f166aa354d476e168c41e6d21))
- maps utils invariants for collections containing undefined values ([#103](https://github.com/expo/entity/issues/103)) ([29c1a43](https://github.com/expo/entity/commit/29c1a434ab6cd8d30e9f0e7c742065c8d46d8d8a))
- remove any cast from EntityLoader ([#100](https://github.com/expo/entity/issues/100)) ([b8e07f9](https://github.com/expo/entity/commit/b8e07f9ddc4077768980c000d61f5ddcc824e2e3))
### Features
- better cache adapter error handling ([#102](https://github.com/expo/entity/issues/102)) ([15546aa](https://github.com/expo/entity/commit/15546aad97734ab9ac67f1c012485f096a0e94f1))
- better database adapter error handling ([#101](https://github.com/expo/entity/issues/101)) ([5208aee](https://github.com/expo/entity/commit/5208aeedb0192f0460ba3d575770a04175f5c3aa))
# [0.10.0](https://github.com/expo/entity/compare/v0.9.1...v0.10.0) (2020-11-17)
### Bug Fixes
- fix null handling in load paths (try 2) ([#96](https://github.com/expo/entity/issues/96)) ([d960329](https://github.com/expo/entity/commit/d9603298e0cc5c3c2b0cca959876e9195555fb57))
## [0.9.1](https://github.com/expo/entity/compare/v0.9.0...v0.9.1) (2020-11-06)
### Bug Fixes
- edge deletion behavior cyclic import structure again ([#93](https://github.com/expo/entity/issues/93)) ([012c05d](https://github.com/expo/entity/commit/012c05d62d2ecd559eb331c6ca358052bfe66dc8))
# [0.9.0](https://github.com/expo/entity/compare/v0.8.1...v0.9.0) (2020-11-05)
### Bug Fixes
- edge deletion behavior cyclic import structure ([#89](https://github.com/expo/entity/issues/89)) ([cca8fc1](https://github.com/expo/entity/commit/cca8fc19870057ebc7033081b59fab4a142fbf1a))
### Features
- add ability to invalidate an entity manually ([#91](https://github.com/expo/entity/issues/91)) ([d1a6f13](https://github.com/expo/entity/commit/d1a6f1339648942f12b3fb605eb329aefe5cdd63))
## [0.8.1](https://github.com/expo/entity/compare/v0.8.0...v0.8.1) (2020-10-09)
**Note:** Version bump only for package root
# [0.8.0](https://github.com/expo/entity/compare/v0.7.1...v0.8.0) (2020-10-09)
### Features
- add entityClassName to metrics adapter events ([#83](https://github.com/expo/entity/issues/83)) ([f4cdc01](https://github.com/expo/entity/commit/f4cdc01c6cb8868c24ce3da2274f0901687e05bd))
## [0.7.1](https://github.com/expo/entity/compare/v0.7.0...v0.7.1) (2020-10-01)
### Bug Fixes
- correct type of runInTransactionAsync ([#81](https://github.com/expo/entity/issues/81)) ([f98f7d1](https://github.com/expo/entity/commit/f98f7d14b6149a496a7a3e97dd63d89b2842cc26))
# [0.7.0](https://github.com/expo/entity/compare/v0.6.0...v0.7.0) (2020-10-01)
### Bug Fixes
- fix incorrect loader types for field selecitons ([#72](https://github.com/expo/entity/issues/72)) ([cd2df8d](https://github.com/expo/entity/commit/cd2df8da4b81bcd4769e950d3124c18886df2816))
- fix index export for EntityMutationTrigger and EntityMutationValidator ([#73](https://github.com/expo/entity/issues/73)) ([7e829f3](https://github.com/expo/entity/commit/7e829f30fde0bd343cb8a584ccda5530cf61ec33))
- move cache invalidation to after transaction commit ([#77](https://github.com/expo/entity/issues/77)) ([dbc3c81](https://github.com/expo/entity/commit/dbc3c81b359a668b5134189ce6de49b5893bb5e7))
- upgrade packages ([#78](https://github.com/expo/entity/issues/78)) ([9891e74](https://github.com/expo/entity/commit/9891e7469467a28589f529c8d87b10fc2232d3ff))
# [0.6.0](https://github.com/expo/entity/compare/v0.5.2...v0.6.0) (2020-07-23)
### Bug Fixes
- ensure transitive deletion triggers are run for CASCADE_DELETE_INVALIDATE_CACHE ([#66](https://github.com/expo/entity/issues/66)) ([5744c51](https://github.com/expo/entity/commit/5744c51e6b3178bd9f229f3f465b683bb4474c8a))
- separate out EntityMutationValidator ([#68](https://github.com/expo/entity/issues/68)) ([547a1ef](https://github.com/expo/entity/commit/547a1efcecd17cc085702d1a3e9888ce5b644b13))
- update setField to correct typing ([#62](https://github.com/expo/entity/issues/62)) ([7a77afc](https://github.com/expo/entity/commit/7a77afc83ea732c9b062a5d6865eff9d3131d015))
### Features
- entity mutation validators ([#67](https://github.com/expo/entity/issues/67)) ([fc4377d](https://github.com/expo/entity/commit/fc4377d8839da07417b88afc138f73556383d896))
- mutation triggers ([#65](https://github.com/expo/entity/issues/65)) ([fd6060c](https://github.com/expo/entity/commit/fd6060cc844f60635b3ce4c400c4877f2df8fa44))
## [0.5.2](https://github.com/expo/entity/compare/v0.5.1...v0.5.2) (2020-07-02)
### Bug Fixes
- fix stub cache key generation for global cache ([#59](https://github.com/expo/entity/issues/59)) ([7c180e8](https://github.com/expo/entity/commit/7c180e84d9a8cdfd9380fac0eab2d02a96ec18f3))
## [0.5.1](https://github.com/expo/entity/compare/v0.5.0...v0.5.1) (2020-07-01)
### Bug Fixes
- correct some typing issues and exports ([#57](https://github.com/expo/entity/issues/57)) ([e91cfba](https://github.com/expo/entity/commit/e91cfba99321c2c4da2078aafd8627cbf0ceca50))
# 0.5.0 (2020-07-01)
### Bug Fixes
- support number ID field generation in StubDatabaseAdapter ([#43](https://github.com/expo/entity/issues/43)) ([60cc0fb](https://github.com/expo/entity/commit/60cc0fbda460c3ada1f5244a2780f90382fe3ebd))
- upgrade Apollo server for security advisory ([#38](https://github.com/expo/entity/issues/38)) ([d25c1e1](https://github.com/expo/entity/commit/d25c1e142d52385725593f4f155d1795697ab490))
### Features
- add two fan out methods to association loader ([#52](https://github.com/expo/entity/issues/52)) ([89cfb3d](https://github.com/expo/entity/commit/89cfb3d5a01d4da90f64acc52ae1b839ba348a35))
- support entity fields subset of db fields ([#49](https://github.com/expo/entity/issues/49)) ([4e40b2e](https://github.com/expo/entity/commit/4e40b2e521407e521d236978ec3b3b56db3990be))
# 0.4.0 (2020-06-03)
- All Packages
- Documentation Improvements
- CI & Coverage
- TypeScript 3.9 ([#18](https://github.com/expo/entity/pull/18))
- `@expo/entity`
- Enforcing mutator convenience functions ([#5](https://github.com/expo/entity/pull/5))
- `loadByIDNullable` loader method ([#28](https://github.com/expo/entity/pull/28))
- Add ability to check update and delete policies outside of mutation ([#30](https://github.com/expo/entity/pull/30))
- `@expo/entity-cache-adapter-redis`
- `@expo/entity-database-adapter-knex`
- Throw when database adapter receives unexpected mutation results ([#31](https://github.com/expo/entity/pull/31))
- `@expo/entity-example`
- Initial Version
# 0.3.0 (2020-04-28)
- `@expo/entity`
- Initial Version
- `@expo/entity-cache-adapter-redis`
- Initial Version
- `@expo/entity-database-adapter-knex`
- Initial Version
+28 -12
View File
@@ -4,24 +4,23 @@ Entity is a privacy-aware data layer for defining, caching, and authorizing acce
[![tests](https://github.com/expo/entity/workflows/tests/badge.svg)](https://github.com/expo/entity/actions?query=workflow%3Atests)
[![docs](https://github.com/expo/entity/workflows/docs/badge.svg)](https://expo.github.io/entity/)
[![codecov](https://codecov.io/gh/expo/entity/branch/master/graph/badge.svg)](https://codecov.io/gh/expo/entity)
[![codecov](https://codecov.io/gh/expo/entity/graph/badge.svg?token=4cFAAdINbk)](https://codecov.io/gh/expo/entity)
[![npm](https://img.shields.io/npm/v/@expo/entity)](https://www.npmjs.com/package/@expo/entity)
[![NPM](https://img.shields.io/npm/l/@expo/entity)](https://www.npmjs.com/package/@expo/entity)
## Core Features
- Declarative actor authorization using Privacy Policies
- Configurable data storage using Database Adapters
- Configurable, optional full-object caching using Cache Adapters
- [Dataloader](https://github.com/graphql/dataloader) in-memory caching
- Well-typed model declaration
- Typescript ORM.
- Declarative actor pre-authorization of all CRUD operations using Privacy Policies.
- Configurable data storage using Database Adapters.
- Configurable, optional, full-object read-through caching using Cache Adapters.
- [Dataloader](https://github.com/graphql/dataloader) in-memory batching and read-through caching.
## Getting Started
- [Example application](packages/entity-example)
- [Example application](https://github.com/expo/entity/tree/main/packages/entity-example)
- [Documentation](https://expo.github.io/entity/)
## Background
Authorization is the process of determining whether a user has access to a piece of data or a feature.
@@ -55,10 +54,7 @@ The Entity framework solves this by adding an additional property to the system:
```typescript
class PhotoPrivacyPolicy {
const readRules = [
new AllowIfOwnerRule(),
new AllowIfOrganizationPermissionRule(),
];
readRules = [new AllowIfOwnerRule(), new AllowIfOrganizationPermissionRule()];
}
// in the view, for example
@@ -71,6 +67,7 @@ async function get_photo_page(viewer: ViewerContext): string {
## Use Case
Entity is not limited in where it can or should be used, but was designed for use in a [Koa](https://koajs.com/)-like environment with a request and response. At Expo, we use Entity in the following manner:
1. A request comes into Koa router
1. Middleware initializes the Entity framework for the request
1. A `ViewerContext` is created identifying the individual making the request.
@@ -78,6 +75,25 @@ Entity is not limited in where it can or should be used, but was designed for us
Note: The entity framework instance should not be shared across multiple requests since it contains a unique memoized [Dataloader](https://github.com/graphql/dataloader#class-dataloader). A long-lived instance is prone to data synchronization issues, especially when the application is scaled horizontally and multiple shared caches would exist for the same data.
## Contributing
We control development tooling versions using [mise-en-place](https://mise.jdx.dev). If you have it installed, then:
- Running `mise run ci` will run all tests that our CI environment will run.
- Running `mise t` will show a list of mise tasks you can run with `mise run <name>`.
- Running `mise en .` will enter a shell environment which includes node and yarn.
If you don't have `mise` installed, [you can install it](https://mise.jdx.dev/installing-mise.html#https-mise-run) as easily as running `curl https://mise.run/zsh | sh`.
## Releasing
To release a new version:
1. `git checkout main`
1. `npm login` (to refresh token)
1. `yarn lerna publish [patch|minor|major] --conventional-commits --force-publish`
1. In GitHub release interface, create a new release from the tag, copy changelog changes to release description.
## License
The Entity source code is made available under the [MIT license](LICENSE).
+3
View File
@@ -0,0 +1,3 @@
export default {
presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
};
@@ -1,6 +1,7 @@
module.exports = {
preset: 'ts-jest',
testMatch: ['**/__integration-tests__/**/*-test.ts'],
coveragePathIgnorePatterns: ['testfixtures'],
import unitConfig from './jest.config.js';
export default {
...unitConfig,
coverageDirectory: 'coverage-integration',
testMatch: ['**/__integration-tests__/**/*-test.ts'],
};
+11
View File
@@ -0,0 +1,11 @@
import { stripTypeScriptTypes } from 'node:module';
export function process(sourceText, sourcePath) {
return {
code: stripTypeScriptTypes(sourceText, {
mode: 'transform',
sourceMap: true,
sourceUrl: sourcePath,
}),
};
}
+16
View File
@@ -0,0 +1,16 @@
export default {
// This is an absolute path determined at runtime so that running `yarn test`
// both from each package's directory and from root works correctly.
transform: { '\\.[jt]sx?$': new URL('./jest-transform.js', import.meta.url).toString() },
extensionsToTreatAsEsm: ['.ts'],
collectCoverageFrom: [
'packages/*/src/**',
'!packages/*/src/index.ts',
'!packages/entity-example/**',
'!packages/entity-codemod/**',
'!packages/entity-testing-utils/**',
'!**/__*test*__/**',
],
coverageProvider: 'v8',
testMatch: ['**/__tests__/**/*-test.ts'],
};
+3 -5
View File
@@ -1,8 +1,6 @@
{
"packages": [
"packages/*"
],
"packages": ["packages/*"],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.4.0"
"version": "0.69.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+52
View File
@@ -0,0 +1,52 @@
[tools]
node = "24.21.0"
[tasks.yarn]
run = "yarn install --immutable"
[tasks.build]
depends = "yarn"
run = "yarn build"
[tasks.typedoc]
depends = "build"
run = "yarn typedoc"
[tasks."checks:tsc"]
depends = "yarn"
run = "yarn tsc"
[tasks."checks:lint"]
depends = "yarn"
run = "yarn lint"
[tasks."checks:format"]
depends = "yarn"
run = "yarn format-check"
[tasks."checks:barrel"]
description = "Check if barrel files are up-to-date"
depends = "yarn"
run = ["yarn ctix", '[ -z "$(git status --porcelain)" ]']
[tasks."checks:unit"]
depends = "yarn"
run = "yarn test --coverage"
[tasks."checks:integration"]
depends = "yarn"
run = "yarn integration --coverage"
[tasks.ci]
run = { task = "checks:*" }
[settings]
experimental = true # needed for postinstall hook
[hooks]
postinstall = [
# Enable yarn shim. Version controlled by package.json#packageManager
# NOTE: mise can install yarn directly, but renovate does not use mise, so we
# control yarn versions using the packageManager field.
"corepack enable yarn",
]
+16
View File
@@ -0,0 +1,16 @@
{
"targetDefaults": {
"tsc": {
"dependsOn": ["^clean", "clean", "^tsc"]
},
"build": {
"dependsOn": ["^clean", "clean", "^build"]
},
"test": {
"dependsOn": ["^tsc"]
},
"integration": {
"dependsOn": ["^tsc"]
}
}
}
+95
View File
@@ -0,0 +1,95 @@
import { defineConfig } from 'oxlint';
import node from 'oxlint-config-universe/node';
import typescriptAnalysis from 'oxlint-config-universe/typescript-analysis';
export default defineConfig({
extends: [node, typescriptAnalysis],
plugins: ['jest'],
options: {
typeAware: true,
},
rules: {
// Overrides from oxlint-config-universe defaults
eqeqeq: ['warn', 'always'],
'no-restricted-properties': [
'warn',
{
object: 'it',
property: 'only',
message: 'it.only should not be committed to main.',
},
{
object: 'test',
property: 'only',
message: 'test.only should not be committed to main.',
},
{
object: 'describe',
property: 'only',
message: 'describe.only should not be committed to main.',
},
{
object: 'it',
property: 'skip',
message: 'it.skip should not be committed to main.',
},
{
object: 'test',
property: 'skip',
message: 'test.skip should not be committed to main.',
},
{
object: 'describe',
property: 'skip',
message: 'describe.skip should not be committed to main.',
},
],
'no-console': 'warn',
'no-void': ['warn', { allowAsStatement: true }],
'no-restricted-imports': ['error', { paths: ['.', '..', '../..'] }],
// TypeScript rules
'typescript/ban-ts-comment': 'warn',
'typescript/unbound-method': 'warn',
'typescript/restrict-template-expressions': 'off',
'typescript/no-base-to-string': 'off',
'typescript/no-redundant-type-constituents': 'off',
'typescript/require-array-sort-compare': 'off',
'typescript/explicit-function-return-type': ['warn', { allowExpressions: true }],
// Jest rules
'jest/valid-title': 'off',
'jest/expect-expect': 'off',
'jest/no-conditional-expect': 'off',
'jest/no-export': 'off',
'jest/require-to-throw-message': 'off',
},
overrides: [
{
files: ['*.config.js'],
env: { node: true },
globals: { URL: 'readonly' },
},
{
files: ['**/__tests__/**/*.ts', '**/__integration-tests__/**/*.ts'],
rules: {
'typescript/no-confusing-void-expression': 'off',
'typescript/unbound-method': 'off',
},
},
{
files: ['packages/**/*.ts'],
excludeFiles: ['**/*tests__/**', 'packages/entity-codemod/src/transforms/**'],
rules: {
'no-restricted-exports': ['error', { restrictDefaultExports: { direct: true } }],
},
},
],
ignorePatterns: [
'packages/entity-codemod/**/__testfixtures__/**',
'packages/*/build',
'coverage',
'coverage-integration',
'doc',
],
});
+34 -38
View File
@@ -1,46 +1,42 @@
{
"name": "root",
"private": true,
"workspaces": ["packages/*"],
"workspaces": [
"packages/*"
],
"type": "module",
"scripts": {
"tsc": "lerna run tsc",
"clean": "lerna run clean",
"lint": "lerna run lint",
"test": "lerna run test",
"integration": "./resources/run-with-docker lerna run integration-no-setup",
"prepublish": "lerna link && lerna run clean",
"prepare": "lerna run tsc",
"barrelsby": "lerna run barrelsby"
"tsc": "tsc --build",
"build": "tsc --build",
"clean": "yarn build --clean",
"lint": "oxlint --type-aware . --max-warnings=0 && yarn format-check",
"lint-fix": "oxlint --type-aware --fix . && yarn format",
"format": "oxfmt --write --ignore-path .fmtignore .",
"format-check": "oxfmt --check --ignore-path .fmtignore .",
"test": "yarn build --noCheck && NODE_OPTIONS='--experimental-vm-modules --disable-warning=ExperimentalWarning' jest",
"test:all": "yarn test",
"integration": "./resources/run-with-docker yarn test --config jest-integration.config.js --runInBand",
"integration:all": "yarn integration",
"ctix": "ctix build && resources/prepend-barrels.sh",
"package-files": "yarn workspaces foreach --all --no-private --parallel --topological pack --dry-run",
"typedoc": "yarn build && typedoc"
},
"devDependencies": {
"@expo/entity": "file:packages/entity",
"@expo/results": "^0.3.0",
"@types/invariant": "^2.2.32",
"@types/ioredis": "^4.14.9",
"@types/jest": "^25.2.1",
"@types/lodash": "^4.14.150",
"@types/node": "^13.13.4",
"@types/uuid": "^7.0.3",
"@typescript-eslint/eslint-plugin": "^2.30.0",
"@typescript-eslint/parser": "^2.30.0",
"barrelsby": "^2.2.0",
"eslint": "^6.8.0",
"eslint-config-universe": "^3.0.0",
"eslint-plugin-tsdoc": "^0.2.4",
"jest": "^24.9.0",
"lerna": "^3.20.2",
"pg": "^8.0.3",
"prettier": "^2.0.5",
"ts-jest": "~24.2.0",
"ts-mockito": "^2.5.0",
"typedoc": "^0.17.6",
"typedoc-plugin-lerna-packages": "^0.3.0",
"typescript": "^3.9.2",
"uuid": "^7.0.3"
"@tsconfig/bases": "1.0.28",
"@types/node": "24.13.5",
"ctix": "2.8.2",
"jest": "30.5.1",
"lerna": "9.0.7",
"oxfmt": "0.67.0",
"oxlint": "1.82.0",
"oxlint-config-universe": "0.2.0",
"oxlint-tsgolint": "0.25.0",
"prettier": "3.9.6",
"typedoc": "0.28.20",
"typescript": "6.0.3"
},
"dependencies": {
"@expo/entity": "file:packages/entity",
"@expo/entity-cache-adapter-redis": "file:packages/entity-cache-adapter-redis",
"@expo/entity-database-adapter-knex": "file:packages/entity-database-adapter-knex"
}
"resolutions": {
"collect-v8-coverage": "^1.0.3"
},
"packageManager": "[email protected]+sha512.5c20ba010c99815433e5c8453112165e673f1c7948d8d2b267f4b5e52097538658388ebc9f9580656d9b75c5cc996f990f611f99304a2197d4c56d21eea370e7"
}
@@ -0,0 +1,232 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.61.0](https://github.com/expo/entity/compare/v0.60.0...v0.61.0) (2026-03-12)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.60.0](https://github.com/expo/entity/compare/v0.59.0...v0.60.0) (2026-03-06)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.59.0](https://github.com/expo/entity/compare/v0.58.0...v0.59.0) (2026-03-03)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.58.0](https://github.com/expo/entity/compare/v0.57.0...v0.58.0) (2026-03-02)
### Bug Fixes
- escape all matching portions of parts of cacher cache keys ([#486](https://github.com/expo/entity/issues/486)) ([21f2f6f](https://github.com/expo/entity/commit/21f2f6f461ee771e8b1c1b22dbaf9b275094c0db))
# [0.57.0](https://github.com/expo/entity/compare/v0.56.0...v0.57.0) (2026-02-28)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.56.0](https://github.com/expo/entity/compare/v0.55.0...v0.56.0) (2026-02-28)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.55.0](https://github.com/expo/entity/compare/v0.54.0...v0.55.0) (2026-01-27)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.54.0](https://github.com/expo/entity/compare/v0.53.0...v0.54.0) (2026-01-21)
### Bug Fixes
- **deps:** pin dependencies ([#328](https://github.com/expo/entity/issues/328)) ([5679f27](https://github.com/expo/entity/commit/5679f27209f1515ca5e626d858c4aa054b414625))
# [0.53.0](https://github.com/expo/entity/compare/v0.52.0...v0.53.0) (2025-12-19)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.52.0](https://github.com/expo/entity/compare/v0.51.0...v0.52.0) (2025-12-19)
### Features
- switch local cache to an interface ([#322](https://github.com/expo/entity/issues/322)) ([c453726](https://github.com/expo/entity/commit/c4537262d810ea01e519f9aec6ae5b17a54ce936))
# [0.51.0](https://github.com/expo/entity/compare/v0.50.0...v0.51.0) (2025-11-21)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.50.0](https://github.com/expo/entity/compare/v0.49.0...v0.50.0) (2025-10-07)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.49.0](https://github.com/expo/entity/compare/v0.48.0...v0.49.0) (2025-10-07)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.48.0](https://github.com/expo/entity/compare/v0.47.0...v0.48.0) (2025-09-19)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.47.0](https://github.com/expo/entity/compare/v0.46.0...v0.47.0) (2025-09-19)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.46.0](https://github.com/expo/entity/compare/v0.45.0...v0.46.0) (2025-06-26)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.45.0](https://github.com/expo/entity/compare/v0.44.0...v0.45.0) (2025-06-06)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.44.0](https://github.com/expo/entity/compare/v0.43.0...v0.44.0) (2025-05-29)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.43.0](https://github.com/expo/entity/compare/v0.42.0...v0.43.0) (2025-04-10)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.42.0](https://github.com/expo/entity/compare/v0.41.0...v0.42.0) (2025-04-10)
### Features
- add @expo/entity-testing-utils package ([#280](https://github.com/expo/entity/issues/280)) ([485894a](https://github.com/expo/entity/commit/485894af16e233d533a9480285bbda56812cbe0b))
- add composite field loading and caching ([#272](https://github.com/expo/entity/issues/272)) ([f0aa0da](https://github.com/expo/entity/commit/f0aa0dafebdb56418cbd22dda437233d850db3e7))
- change entity id field generic to field name and derive type where necessary ([#278](https://github.com/expo/entity/issues/278)) ([b7e524c](https://github.com/expo/entity/commit/b7e524c41892797608d0b884410b8d520a80a9ef))
- convert batched/cached loader interface to holder pattern ([#271](https://github.com/expo/entity/issues/271)) ([06b3cb7](https://github.com/expo/entity/commit/06b3cb7f8076ef9e5b83cdf154da159801bd14cc)), closes [#201](https://github.com/expo/entity/issues/201)
- invalidate n+/-1 cacheKeyVersion for entities for push safety ([#275](https://github.com/expo/entity/issues/275)) ([d9c1852](https://github.com/expo/entity/commit/d9c1852706f98f0761ff24ddd244e4ed2beca580))
# [0.41.0](https://github.com/expo/entity/compare/v0.40.0...v0.41.0) (2025-03-07)
- feat!: Default all loaders and mutators to enforcing (#256) ([a976e76](https://github.com/expo/entity/commit/a976e765098bca27049585e43a3bed5c71b9ac6b)), closes [#256](https://github.com/expo/entity/issues/256)
### BREAKING CHANGES
- Default all loaders and mutators to enforcing
# [0.40.0](https://github.com/expo/entity/compare/v0.39.0...v0.40.0) (2025-02-27)
### Reverts
- Revert "v0.40.0" ([fa78ce8](https://github.com/expo/entity/commit/fa78ce84e2f2eb8199e36fd51779fc68905001c0))
# [0.39.0](https://github.com/expo/entity/compare/v0.38.0...v0.39.0) (2024-11-21)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.38.0](https://github.com/expo/entity/compare/v0.37.0...v0.38.0) (2024-06-27)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.37.0](https://github.com/expo/entity/compare/v0.36.0...v0.37.0) (2024-06-12)
### Bug Fixes
- update barrels ([#241](https://github.com/expo/entity/issues/241)) ([8c5f81b](https://github.com/expo/entity/commit/8c5f81bcca107e9b7bbea5f0ab41b0105057806e))
# [0.36.0](https://github.com/expo/entity/compare/v0.35.0...v0.36.0) (2024-06-12)
### Bug Fixes
- constrain entity fields type to string-keyed object ([#235](https://github.com/expo/entity/issues/235)) ([7e2cea1](https://github.com/expo/entity/commit/7e2cea16973a0ae1917f867cd25d6ef7c8eaecef))
# [0.35.0](https://github.com/expo/entity/compare/v0.34.0...v0.35.0) (2024-04-11)
### Bug Fixes
- Revert: require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([1bdfbd6](https://github.com/expo/entity/commit/1bdfbd6b562d1200e4029df0533b5adb2d917831))
# [0.34.0](https://github.com/expo/entity/compare/v0.33.0...v0.34.0) (2024-04-11)
### Features
- require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([8b0b31f](https://github.com/expo/entity/commit/8b0b31fdde5bd565aa527719003ef283a45f55cc))
# [0.33.0](https://github.com/expo/entity/compare/v0.32.0...v0.33.0) (2023-10-06)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.32.0](https://github.com/expo/entity/compare/v0.31.1...v0.32.0) (2023-06-10)
### Features
- entity companion definition static thunk ([#210](https://github.com/expo/entity/issues/210)) ([4b18010](https://github.com/expo/entity/commit/4b18010d42be50ef329f428b08330e21bf676586))
# [0.31.0](https://github.com/expo/entity/compare/v0.30.0...v0.31.0) (2023-01-19)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.30.0](https://github.com/expo/entity/compare/v0.29.0...v0.30.0) (2022-12-23)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.29.0](https://github.com/expo/entity/compare/v0.28.0...v0.29.0) (2022-10-06)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.28.0](https://github.com/expo/entity/compare/v0.27.0...v0.28.0) (2022-09-13)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.27.0](https://github.com/expo/entity/compare/v0.26.1...v0.27.0) (2022-08-09)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
## [0.26.1](https://github.com/expo/entity/compare/v0.26.0...v0.26.1) (2022-07-28)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.26.0](https://github.com/expo/entity/compare/v0.25.3...v0.26.0) (2022-07-14)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
## [0.25.3](https://github.com/expo/entity/compare/v0.25.2...v0.25.3) (2022-03-12)
### Bug Fixes
- lock lru-cache to v6.0.0 ([#170](https://github.com/expo/entity/issues/170)) ([293868b](https://github.com/expo/entity/commit/293868b1a73df9f749216af68b1ae94ac22ac2b9))
## [0.25.2](https://github.com/expo/entity/compare/v0.25.1...v0.25.2) (2022-03-12)
### Bug Fixes
- lru-cache yarn workspace version resolution ([#168](https://github.com/expo/entity/issues/168)) ([e76ca8c](https://github.com/expo/entity/commit/e76ca8c2f72da84aa372a64ef1898ade220ed0a8))
## [0.25.1](https://github.com/expo/entity/compare/v0.25.0...v0.25.1) (2022-03-10)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.25.0](https://github.com/expo/entity/compare/v0.24.0...v0.25.0) (2022-03-10)
### Features
- composed cache adapter, composed secondary cache, local memory secondary cache ([#164](https://github.com/expo/entity/issues/164)) ([6b6e70e](https://github.com/expo/entity/commit/6b6e70e75f7e42a4dce13edeb406a4d88d9264f0))
# [0.24.0](https://github.com/expo/entity/compare/v0.23.0...v0.24.0) (2022-02-16)
### Features
- add local memory cache package ([#158](https://github.com/expo/entity/issues/158)) ([85d3575](https://github.com/expo/entity/commit/85d35752fa0919a62efb776a762a6829e83ca7af))
@@ -0,0 +1,49 @@
# `@expo/entity-cache-adapter-local-memory`
Cross-request [LRU](https://github.com/isaacs/node-lru-cache) cache adapter for `@expo/entity`. Use
this cache with caution - it is nonstandard. The cache is shared between requests in the node process.
Note: This uses version 6.0.0 of `node-lru-cache` since it the most tuned version for our use case (low TTL + LRU). Upgrading
to 7.x will cause high memory usage for the entity cache adapter use case since it allocates fixed-size data structures up front to tune for the non-TTL use case: https://github.com/isaacs/node-lru-cache/issues/208.
[Documentation](https://expo.github.io/entity/modules/_expo_cache_adapter_local_memory.html)
## Why NOT use this cache
Because this is an in-memory cache, cross-machine invalidation is not possible. Do not use this cache
if you have the following use cases:
- The objects stored are mutable
- Reading a stale object from the cache is not acceptable in your application
- Cross-machine invalidation is not possible
## Typical use cases
If your application sees many requests fetching the same objects, you can save a trip to your cache
cluster and backing datastore by using this in-memory cache. Here are some good use cases:
- The objects stored are mostly immutable, and reading a stale object for a short TTL is acceptable
- You have a low TTL setting in your cache
## Usage
During `EntityCompanionProvider` instantiation:
```typescript
export const createDefaultEntityCompanionProvider = (
metricsAdapter: IEntityMetricsAdapter = new NoOpEntityMetricsAdapter()
): EntityCompanionProvider => {
return new EntityCompanionProvider(
metricsAdapter,
{
...
},
{
['local-memory']: {
cacheAdapterProvider: new LocalMemoryCacheAdapterProvider.getProvider(),
},
}
);
};
```
@@ -0,0 +1,40 @@
{
"name": "@expo/entity-cache-adapter-local-memory",
"version": "0.69.0",
"description": "Cross-request local memory cache adapter for @expo/entity",
"keywords": [
"entity"
],
"license": "MIT",
"author": "Expo",
"files": [
"build",
"!*.tsbuildinfo",
"!__*",
"src"
],
"type": "module",
"main": "build/src/index.js",
"types": "build/src/index.d.ts",
"scripts": {
"build": "tsc --build",
"prepack": "rm -rf build && yarn build",
"clean": "yarn build --clean",
"lint": "yarn run --top-level oxlint --type-aware src",
"lint-fix": "yarn lint --fix",
"test": "yarn test:all --rootDir $(pwd)",
"integration": "yarn integration:all --rootDir $(pwd)"
},
"dependencies": {
"@expo/entity": "workspace:^"
},
"devDependencies": {
"@expo/entity-testing-utils": "workspace:^",
"@isaacs/ttlcache": "2.1.5",
"@jest/globals": "30.5.1",
"typescript": "6.0.3"
},
"engines": {
"node": ">=18"
}
}
@@ -0,0 +1,139 @@
import type {
CacheLoadResult,
EntityConfiguration,
IEntityGenericCacher,
IEntityLoadKey,
IEntityLoadValue,
} from '@expo/entity';
import { CacheStatus } from '@expo/entity';
// Sentinel value we store in local memory to negatively cache a database miss.
// The sentinel value is distinct from any (positively) cached value.
export const DOES_NOT_EXIST_LOCAL_MEMORY_CACHE = Symbol('doesNotExist');
/**
* Type of value stored in the local memory cache. This is either the cached fields, or the
* DOES_NOT_EXIST_LOCAL_MEMORY_CACHE sentinel value.
*/
export type LocalMemoryCacheValue<TFields extends Record<string, any>> =
| Readonly<TFields>
| typeof DOES_NOT_EXIST_LOCAL_MEMORY_CACHE;
/**
* Interface for a local memory cache used by GenericLocalMemoryCacher. Most often, this is something like
* a TTLCache from the `@isaacs/ttlcache` package or an lru-cache.
*/
export interface ILocalMemoryCache<TFields extends Record<string, any>> {
/**
* Gets a value from the cache for specified key.
* @param key - key to get
* @returns the cached value, or undefined if not present
*/
get(key: string): LocalMemoryCacheValue<TFields> | undefined;
/**
* Sets a value in the cache for specified key.
* @param key - key to set
* @param value - value to set
*/
set(key: string, value: LocalMemoryCacheValue<TFields>): void;
/**
* Deletes a value from the cache for specified key.
* @param key - key to delete
*/
delete(key: string): void;
}
export class GenericLocalMemoryCacher<
TFields extends Record<string, any>,
TIDField extends keyof TFields,
> implements IEntityGenericCacher<TFields, TIDField> {
constructor(
private readonly entityConfiguration: EntityConfiguration<TFields, TIDField>,
private readonly localMemoryCache: ILocalMemoryCache<TFields>,
) {}
static createNoOpCache<TFields extends Record<string, any>>(): ILocalMemoryCache<TFields> {
return {
get(_key: string): LocalMemoryCacheValue<TFields> | undefined {
return undefined;
},
set(_key: string, _value: LocalMemoryCacheValue<TFields>): void {},
delete(_key: string): void {},
};
}
public async loadManyAsync(
keys: readonly string[],
): Promise<ReadonlyMap<string, CacheLoadResult<TFields>>> {
const cacheResults = new Map<string, CacheLoadResult<TFields>>();
for (const key of keys) {
const cacheResult = this.localMemoryCache.get(key);
if (cacheResult === DOES_NOT_EXIST_LOCAL_MEMORY_CACHE) {
cacheResults.set(key, {
status: CacheStatus.NEGATIVE,
});
} else if (cacheResult) {
cacheResults.set(key, {
status: CacheStatus.HIT,
item: cacheResult as unknown as TFields,
});
} else {
cacheResults.set(key, {
status: CacheStatus.MISS,
});
}
}
return cacheResults;
}
public async cacheManyAsync(objectMap: ReadonlyMap<string, Readonly<TFields>>): Promise<void> {
for (const [key, item] of objectMap) {
this.localMemoryCache.set(key, item);
}
}
public async cacheDBMissesAsync(keys: readonly string[]): Promise<void> {
for (const key of keys) {
this.localMemoryCache.set(key, DOES_NOT_EXIST_LOCAL_MEMORY_CACHE);
}
}
public async invalidateManyAsync(keys: readonly string[]): Promise<void> {
for (const key of keys) {
this.localMemoryCache.delete(key);
}
}
public makeCacheKeyForStorage<
TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
TSerializedLoadValue,
TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
>(key: TLoadKey, value: TLoadValue): string {
const cacheKeyType = key.getLoadMethodType();
const keyAndValueParts = key.createCacheKeyPartsForLoadValue(this.entityConfiguration, value);
const parts = [
this.entityConfiguration.tableName,
cacheKeyType,
`${this.entityConfiguration.cacheKeyVersion}`,
...keyAndValueParts,
];
const delimiter = ':';
const escapedParts = parts.map((part) =>
part.replaceAll('\\', '\\\\').replaceAll(delimiter, `\\${delimiter}`),
);
return escapedParts.join(delimiter);
}
public makeCacheKeysForInvalidation<
TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
TSerializedLoadValue,
TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
>(key: TLoadKey, value: TLoadValue): readonly string[] {
// for local memory caches, we don't need to invalidate old versions of the cache keys
// since they are not persisted across deploys
return [this.makeCacheKeyForStorage(key, value)];
}
}
@@ -0,0 +1,54 @@
import type {
EntityConfiguration,
IEntityCacheAdapter,
IEntityCacheAdapterProvider,
} from '@expo/entity';
import { computeIfAbsent, GenericEntityCacheAdapter } from '@expo/entity';
import type { ILocalMemoryCache } from './GenericLocalMemoryCacher.ts';
import { GenericLocalMemoryCacher } from './GenericLocalMemoryCacher.ts';
export type LocalMemoryCacheCreator = <
TFields extends Record<string, any>,
>() => ILocalMemoryCache<TFields>;
/**
* Vends local memory cache adapters. An instance of this class may be shared across requests to
* share the local memory cache.
*/
export class LocalMemoryCacheAdapterProvider implements IEntityCacheAdapterProvider {
/**
* @returns a no-op local memory cache adapter provider, or one that doesn't cache at all.
*/
static createNoOpProvider(): IEntityCacheAdapterProvider {
return new LocalMemoryCacheAdapterProvider(<TFields extends Record<string, any>>() =>
GenericLocalMemoryCacher.createNoOpCache<TFields>(),
);
}
/**
* @returns a local memory cache adapter provider configured with the supplied local memory cache creator function.
*/
static createProviderWithCacheCreator(
cacheCreator: LocalMemoryCacheCreator,
): IEntityCacheAdapterProvider {
return new LocalMemoryCacheAdapterProvider(cacheCreator);
}
private readonly localMemoryCacheAdapterMap = new Map<
string,
GenericEntityCacheAdapter<any, any>
>();
private constructor(private readonly localMemoryCacheCreator: LocalMemoryCacheCreator) {}
public getCacheAdapter<TFields extends Record<string, any>, TIDField extends keyof TFields>(
entityConfiguration: EntityConfiguration<TFields, TIDField>,
): IEntityCacheAdapter<TFields, TIDField> {
return computeIfAbsent(this.localMemoryCacheAdapterMap, entityConfiguration.tableName, () => {
const localMemoryCache = this.localMemoryCacheCreator<TFields>();
return new GenericEntityCacheAdapter(
new GenericLocalMemoryCacher(entityConfiguration, localMemoryCache),
);
});
}
}
@@ -0,0 +1,99 @@
import type { EntityCompanionDefinition, ViewerContext } from '@expo/entity';
import {
AlwaysAllowPrivacyPolicyRule,
DateField,
Entity,
EntityConfiguration,
EntityPrivacyPolicy,
StringField,
UUIDField,
} from '@expo/entity';
export type LocalMemoryTestEntityFields = {
id: string;
name: string;
dateField: Date | null;
};
export class LocalMemoryTestEntity extends Entity<
LocalMemoryTestEntityFields,
'id',
ViewerContext
> {
static defineCompanionDefinition(): EntityCompanionDefinition<
LocalMemoryTestEntityFields,
'id',
ViewerContext,
LocalMemoryTestEntity,
LocalMemoryTestEntityPrivacyPolicy
> {
return {
entityClass: LocalMemoryTestEntity,
entityConfiguration: localMemoryTestEntityConfiguration,
privacyPolicyClass: LocalMemoryTestEntityPrivacyPolicy,
};
}
}
export class LocalMemoryTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
LocalMemoryTestEntityFields,
'id',
ViewerContext,
LocalMemoryTestEntity
> {
protected override readonly createRules = [
new AlwaysAllowPrivacyPolicyRule<
LocalMemoryTestEntityFields,
'id',
ViewerContext,
LocalMemoryTestEntity
>(),
];
protected override readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<
LocalMemoryTestEntityFields,
'id',
ViewerContext,
LocalMemoryTestEntity
>(),
];
protected override readonly updateRules = [
new AlwaysAllowPrivacyPolicyRule<
LocalMemoryTestEntityFields,
'id',
ViewerContext,
LocalMemoryTestEntity
>(),
];
protected override readonly deleteRules = [
new AlwaysAllowPrivacyPolicyRule<
LocalMemoryTestEntityFields,
'id',
ViewerContext,
LocalMemoryTestEntity
>(),
];
}
export const localMemoryTestEntityConfiguration = new EntityConfiguration<
LocalMemoryTestEntityFields,
'id'
>({
idField: 'id',
tableName: 'local_memory_test_entities',
schema: {
id: new UUIDField({
columnName: 'id',
cache: true,
}),
name: new StringField({
columnName: 'name',
cache: true,
}),
dateField: new DateField({
columnName: 'date_field',
}),
},
databaseAdapterFlavor: 'postgres',
cacheAdapterFlavor: 'local-memory',
});
@@ -0,0 +1,63 @@
import type { IEntityMetricsAdapter } from '@expo/entity';
import { EntityCompanionProvider, NoOpEntityMetricsAdapter } from '@expo/entity';
import { StubDatabaseAdapterProvider, StubQueryContextProvider } from '@expo/entity-testing-utils';
import { TTLCache } from '@isaacs/ttlcache';
import type { ILocalMemoryCache, LocalMemoryCacheValue } from '../GenericLocalMemoryCacher.ts';
import { LocalMemoryCacheAdapterProvider } from '../LocalMemoryCacheAdapterProvider.ts';
const queryContextProvider = new StubQueryContextProvider();
function createTTLCache<TFields extends Record<string, any>>(
options: { maxSize?: number; ttlSeconds?: number } = {},
): ILocalMemoryCache<TFields> {
const DEFAULT_LRU_CACHE_MAX_AGE_SECONDS = 10;
const DEFAULT_LRU_CACHE_SIZE = 10000;
const maxAgeSeconds = options.ttlSeconds ?? DEFAULT_LRU_CACHE_MAX_AGE_SECONDS;
return new TTLCache<string, LocalMemoryCacheValue<TFields>>({
max: options.maxSize ?? DEFAULT_LRU_CACHE_SIZE,
ttl: maxAgeSeconds * 1000, // convert to ms
updateAgeOnGet: true,
});
}
export const createLocalMemoryTestEntityCompanionProvider = (
localMemoryOptions: { maxSize?: number; ttlSeconds?: number } = {},
metricsAdapter: IEntityMetricsAdapter = new NoOpEntityMetricsAdapter(),
): EntityCompanionProvider => {
const localMemoryCacheAdapterProvider =
localMemoryOptions.maxSize === 0 && localMemoryOptions.ttlSeconds === 0
? LocalMemoryCacheAdapterProvider.createNoOpProvider()
: LocalMemoryCacheAdapterProvider.createProviderWithCacheCreator(() =>
createTTLCache(localMemoryOptions),
);
return new EntityCompanionProvider(
metricsAdapter,
new Map([
[
'postgres',
{
adapterProvider: new StubDatabaseAdapterProvider(),
queryContextProvider,
},
],
]),
new Map([
[
'local-memory',
{
cacheAdapterProvider: localMemoryCacheAdapterProvider,
},
],
]),
);
};
export const createNoOpLocalMemoryIntegrationTestEntityCompanionProvider = (
metricsAdapter: IEntityMetricsAdapter = new NoOpEntityMetricsAdapter(),
): EntityCompanionProvider => {
return createLocalMemoryTestEntityCompanionProvider(
{ maxSize: 0, ttlSeconds: 0 },
metricsAdapter,
);
};
@@ -0,0 +1,178 @@
import type { GenericEntityCacheAdapter, IEntityGenericCacher } from '@expo/entity';
import {
CacheStatus,
SingleFieldHolder,
SingleFieldValueHolder,
ViewerContext,
} from '@expo/entity';
import { describe, expect, it } from '@jest/globals';
import { v4 as uuidv4 } from 'uuid';
import { GenericLocalMemoryCacher } from '../GenericLocalMemoryCacher.ts';
import type { LocalMemoryCacheAdapterProvider } from '../LocalMemoryCacheAdapterProvider.ts';
import type { LocalMemoryTestEntityFields } from '../__testfixtures__/LocalMemoryTestEntity.ts';
import { LocalMemoryTestEntity } from '../__testfixtures__/LocalMemoryTestEntity.ts';
import {
createLocalMemoryTestEntityCompanionProvider,
createNoOpLocalMemoryIntegrationTestEntityCompanionProvider,
} from '../__testfixtures__/createLocalMemoryTestEntityCompanionProvider.ts';
describe(GenericLocalMemoryCacher, () => {
it('has correct caching behavior', async () => {
const entityCompanionProvider = createLocalMemoryTestEntityCompanionProvider();
const viewerContext = new ViewerContext(entityCompanionProvider);
const genericCacher = (
viewerContext.entityCompanionProvider.getCompanionForEntity(LocalMemoryTestEntity)[
'tableDataCoordinator'
]['cacheAdapter'] as GenericEntityCacheAdapter<LocalMemoryTestEntityFields, 'id'>
)['genericCacher'];
const date = new Date();
const entity1Created = await LocalMemoryTestEntity.creator(viewerContext)
.setField('name', 'blah')
.setField('dateField', date)
.createAsync();
// loading an entity should put it in cache
const entity1 = await LocalMemoryTestEntity.loader(viewerContext).loadByIDAsync(
entity1Created.getID(),
);
const localMemoryCacheAdapterProvider = entityCompanionProvider['cacheAdapterFlavors'].get(
'local-memory',
)!.cacheAdapterProvider as LocalMemoryCacheAdapterProvider;
const entitySpecificGenericCacher = localMemoryCacheAdapterProvider[
'localMemoryCacheAdapterMap'
].get(
viewerContext.entityCompanionProvider.getCompanionForEntity(LocalMemoryTestEntity)
.entityCompanionDefinition.entityConfiguration.tableName,
)!['genericCacher'] as IEntityGenericCacher<LocalMemoryTestEntityFields, 'id'>;
const cachedResult = await entitySpecificGenericCacher.loadManyAsync([
genericCacher.makeCacheKeyForStorage(
new SingleFieldHolder('id'),
new SingleFieldValueHolder(entity1.getID()),
),
]);
const cachedValue = cachedResult.get(
genericCacher.makeCacheKeyForStorage(
new SingleFieldHolder('id'),
new SingleFieldValueHolder(entity1.getID()),
),
)!;
expect(cachedValue).toMatchObject({
status: CacheStatus.HIT,
item: {
id: entity1.getID(),
name: 'blah',
dateField: date,
},
});
// simulate non existent db fetch, should write negative result ('') to cache
const nonExistentId = uuidv4();
const entityNonExistentResult =
await LocalMemoryTestEntity.loaderWithAuthorizationResults(viewerContext).loadByIDAsync(
nonExistentId,
);
expect(entityNonExistentResult.ok).toBe(false);
const nonExistentCachedResult = await entitySpecificGenericCacher.loadManyAsync([
genericCacher.makeCacheKeyForStorage(
new SingleFieldHolder('id'),
new SingleFieldValueHolder(nonExistentId),
),
]);
expect(
nonExistentCachedResult.get(
genericCacher.makeCacheKeyForStorage(
new SingleFieldHolder('id'),
new SingleFieldValueHolder(nonExistentId),
),
),
).toMatchObject({
status: CacheStatus.NEGATIVE,
});
// load again through entities framework to ensure it reads negative result
const entityNonExistentResult2 =
await LocalMemoryTestEntity.loaderWithAuthorizationResults(viewerContext).loadByIDAsync(
nonExistentId,
);
expect(entityNonExistentResult2.ok).toBe(false);
// invalidate from cache to ensure it invalidates correctly
await LocalMemoryTestEntity.invalidationUtils(viewerContext).invalidateFieldsAsync(
entity1.getAllFields(),
);
const keys = genericCacher.makeCacheKeysForInvalidation(
new SingleFieldHolder('id'),
new SingleFieldValueHolder(entity1.getID()),
);
const cachedResultMiss = await entitySpecificGenericCacher.loadManyAsync(keys);
expect(Array.from(cachedResultMiss.values()).every((v) => v.status === CacheStatus.MISS)).toBe(
true,
);
});
it('respects the parameters of a noop cache', async () => {
const entityCompanionProvider = createNoOpLocalMemoryIntegrationTestEntityCompanionProvider();
const viewerContext = new ViewerContext(entityCompanionProvider);
const genericCacher = (
viewerContext.entityCompanionProvider.getCompanionForEntity(LocalMemoryTestEntity)[
'tableDataCoordinator'
]['cacheAdapter'] as GenericEntityCacheAdapter<LocalMemoryTestEntityFields, 'id'>
)['genericCacher'];
const cacheKeyMaker = genericCacher['makeCacheKeyForStorage'].bind(genericCacher);
const date = new Date();
const entity1Created = await LocalMemoryTestEntity.creator(viewerContext)
.setField('name', 'blah')
.setField('dateField', date)
.createAsync();
// loading an entity will try to put it in cache but it's a noop cache, so it should be a miss
const entity1 = await LocalMemoryTestEntity.loader(viewerContext).loadByIDAsync(
entity1Created.getID(),
);
const localMemoryCacheAdapterProvider = entityCompanionProvider['cacheAdapterFlavors'].get(
'local-memory',
)!.cacheAdapterProvider as LocalMemoryCacheAdapterProvider;
const entitySpecificGenericCacher = localMemoryCacheAdapterProvider[
'localMemoryCacheAdapterMap'
].get(
viewerContext.entityCompanionProvider.getCompanionForEntity(LocalMemoryTestEntity)
.entityCompanionDefinition.entityConfiguration.tableName,
)!['genericCacher'] as IEntityGenericCacher<LocalMemoryTestEntityFields, 'id'>;
const cachedResult = await entitySpecificGenericCacher.loadManyAsync([
cacheKeyMaker(new SingleFieldHolder('id'), new SingleFieldValueHolder(entity1.getID())),
]);
const cachedValue = cachedResult.get(
cacheKeyMaker(new SingleFieldHolder('id'), new SingleFieldValueHolder(entity1.getID())),
)!;
expect(cachedValue).toMatchObject({
status: CacheStatus.MISS,
});
// a non existent db fetch should try to write negative result ('') but it's a noop cache, so it should be a miss
const nonExistentId = uuidv4();
const entityNonExistentResult =
await LocalMemoryTestEntity.loaderWithAuthorizationResults(viewerContext).loadByIDAsync(
nonExistentId,
);
expect(entityNonExistentResult.ok).toBe(false);
const nonExistentCachedResult = await entitySpecificGenericCacher.loadManyAsync([
cacheKeyMaker(new SingleFieldHolder('id'), new SingleFieldValueHolder(nonExistentId)),
]);
expect(
nonExistentCachedResult.get(
cacheKeyMaker(new SingleFieldHolder('id'), new SingleFieldValueHolder(nonExistentId)),
),
).toMatchObject({
status: CacheStatus.MISS,
});
});
});
@@ -0,0 +1,170 @@
import {
CacheStatus,
EntityConfiguration,
GenericEntityCacheAdapter,
SingleFieldHolder,
SingleFieldValueHolder,
SingleFieldValueHolderMap,
UUIDField,
} from '@expo/entity';
import { TTLCache } from '@isaacs/ttlcache';
import { describe, expect, it } from '@jest/globals';
import type { ILocalMemoryCache, LocalMemoryCacheValue } from '../GenericLocalMemoryCacher.ts';
import {
DOES_NOT_EXIST_LOCAL_MEMORY_CACHE,
GenericLocalMemoryCacher,
} from '../GenericLocalMemoryCacher.ts';
type BlahFields = {
id: string;
};
const entityConfiguration = new EntityConfiguration<BlahFields, 'id'>({
idField: 'id',
tableName: 'blah',
schema: {
id: new UUIDField({ columnName: 'id', cache: true }),
},
databaseAdapterFlavor: 'postgres',
cacheAdapterFlavor: 'local-memory',
});
function createTTLCache<TFields extends Record<string, any>>(): ILocalMemoryCache<TFields> {
return new TTLCache<string, LocalMemoryCacheValue<TFields>>({
max: 10000,
ttl: 10 * 1000, // convert to ms
updateAgeOnGet: true,
});
}
describe('Use within GenericEntityCacheAdapter', () => {
describe('loadManyAsync', () => {
it('returns appropriate cache results', async () => {
const cacheAdapter = new GenericEntityCacheAdapter(
new GenericLocalMemoryCacher(entityConfiguration, createTTLCache()),
);
const cacheHits = new Map<SingleFieldValueHolder<BlahFields, 'id'>, Readonly<BlahFields>>([
[new SingleFieldValueHolder('test-id-1'), { id: 'test-id-1' }],
]);
await cacheAdapter.cacheManyAsync(
new SingleFieldHolder<BlahFields, 'id', 'id'>('id'),
cacheHits,
);
await cacheAdapter.cacheDBMissesAsync(new SingleFieldHolder('id'), [
new SingleFieldValueHolder('test-id-2'),
]);
const results = await cacheAdapter.loadManyAsync(new SingleFieldHolder('id'), [
new SingleFieldValueHolder('test-id-1'),
new SingleFieldValueHolder('test-id-2'),
new SingleFieldValueHolder('test-id-3'),
]);
expect(results.get(new SingleFieldValueHolder('test-id-1'))).toMatchObject({
status: CacheStatus.HIT,
item: { id: 'test-id-1' },
});
expect(results.get(new SingleFieldValueHolder('test-id-2'))).toMatchObject({
status: CacheStatus.NEGATIVE,
});
expect(results.get(new SingleFieldValueHolder('test-id-3'))).toMatchObject({
status: CacheStatus.MISS,
});
expect(results.size).toBe(3);
});
it('returns empty map when passed empty array of values', async () => {
const cacheAdapter = new GenericEntityCacheAdapter(
new GenericLocalMemoryCacher(entityConfiguration, createTTLCache()),
);
const results = await cacheAdapter.loadManyAsync(
new SingleFieldHolder<BlahFields, 'id', 'id'>('id'),
[] as SingleFieldValueHolder<BlahFields, 'id'>[],
);
expect(results).toEqual(new SingleFieldValueHolderMap(new Map()));
});
});
describe('cacheManyAsync', () => {
it('correctly caches all objects', async () => {
const localMemoryCache = createTTLCache<BlahFields>();
const localMemoryCacher = new GenericLocalMemoryCacher(entityConfiguration, localMemoryCache);
const cacheAdapter = new GenericEntityCacheAdapter(localMemoryCacher);
await cacheAdapter.cacheManyAsync(
new SingleFieldHolder('id'),
new Map([[new SingleFieldValueHolder('test-id-1'), { id: 'test-id-1' }]]),
);
const cacheKey = localMemoryCacher['makeCacheKeyForStorage'](
new SingleFieldHolder('id'),
new SingleFieldValueHolder('test-id-1'),
);
expect(localMemoryCache.get(cacheKey)).toMatchObject({
id: 'test-id-1',
});
});
});
describe('cacheDBMissesAsync', () => {
it('correctly caches misses', async () => {
const localMemoryCache = createTTLCache<BlahFields>();
const localMemoryCacher = new GenericLocalMemoryCacher(entityConfiguration, localMemoryCache);
const cacheAdapter = new GenericEntityCacheAdapter(localMemoryCacher);
await cacheAdapter.cacheDBMissesAsync(new SingleFieldHolder('id'), [
new SingleFieldValueHolder('test-id-1'),
]);
const cacheKey = localMemoryCacher['makeCacheKeyForStorage'](
new SingleFieldHolder('id'),
new SingleFieldValueHolder('test-id-1'),
);
expect(localMemoryCache.get(cacheKey)).toEqual(DOES_NOT_EXIST_LOCAL_MEMORY_CACHE);
});
});
describe('invalidateManyAsync', () => {
it('invalidates correctly', async () => {
const localMemoryCache = createTTLCache<BlahFields>();
const cacheAdapter = new GenericEntityCacheAdapter(
new GenericLocalMemoryCacher(entityConfiguration, localMemoryCache),
);
await cacheAdapter.cacheManyAsync(
new SingleFieldHolder('id'),
new Map([[new SingleFieldValueHolder('test-id-1'), { id: 'test-id-1' }]]),
);
await cacheAdapter.cacheDBMissesAsync(new SingleFieldHolder('id'), [
new SingleFieldValueHolder('test-id-2'),
]);
await cacheAdapter.invalidateManyAsync(new SingleFieldHolder('id'), [
new SingleFieldValueHolder('test-id-1'),
new SingleFieldValueHolder('test-id-2'),
]);
const results = await cacheAdapter.loadManyAsync(new SingleFieldHolder('id'), [
new SingleFieldValueHolder('test-id-1'),
new SingleFieldValueHolder('test-id-2'),
]);
expect(results.get(new SingleFieldValueHolder('test-id-1'))).toMatchObject({
status: CacheStatus.MISS,
});
expect(results.get(new SingleFieldValueHolder('test-id-2'))).toMatchObject({
status: CacheStatus.MISS,
});
});
it('returns when passed empty array of values', async () => {
const cacheAdapter = new GenericEntityCacheAdapter(
new GenericLocalMemoryCacher(entityConfiguration, createTTLCache<BlahFields>()),
);
await cacheAdapter.invalidateManyAsync(
new SingleFieldHolder<BlahFields, 'id', 'id'>('id'),
[] as SingleFieldValueHolder<BlahFields, 'id'>[],
);
});
});
});
@@ -0,0 +1,8 @@
/* eslint-disable tsdoc/syntax */
/**
* @packageDocumentation
* @module @expo/entity-cache-adapter-local-memory
*/
export * from './GenericLocalMemoryCacher.ts';
export * from './LocalMemoryCacheAdapterProvider.ts';
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src/**/*"],
"references": [{ "path": "../entity" }, { "path": "../entity-testing-utils" }]
}
@@ -0,0 +1,359 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.61.0](https://github.com/expo/entity/compare/v0.60.0...v0.61.0) (2026-03-12)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.60.0](https://github.com/expo/entity/compare/v0.59.0...v0.60.0) (2026-03-06)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.59.0](https://github.com/expo/entity/compare/v0.58.0...v0.59.0) (2026-03-03)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.58.0](https://github.com/expo/entity/compare/v0.57.0...v0.58.0) (2026-03-02)
### Bug Fixes
- escape all matching portions of parts of cacher cache keys ([#486](https://github.com/expo/entity/issues/486)) ([21f2f6f](https://github.com/expo/entity/commit/21f2f6f461ee771e8b1c1b22dbaf9b275094c0db))
# [0.57.0](https://github.com/expo/entity/compare/v0.56.0...v0.57.0) (2026-02-28)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.56.0](https://github.com/expo/entity/compare/v0.55.0...v0.56.0) (2026-02-28)
### Bug Fixes
- eliminate some unnecessary any casts ([#448](https://github.com/expo/entity/issues/448)) ([cc78029](https://github.com/expo/entity/commit/cc78029117c14510b0934b5e0d19876a45570f8d))
# [0.55.0](https://github.com/expo/entity/compare/v0.54.0...v0.55.0) (2026-01-27)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.54.0](https://github.com/expo/entity/compare/v0.53.0...v0.54.0) (2026-01-21)
### Bug Fixes
- **deps:** pin dependencies ([#328](https://github.com/expo/entity/issues/328)) ([5679f27](https://github.com/expo/entity/commit/5679f27209f1515ca5e626d858c4aa054b414625))
# [0.53.0](https://github.com/expo/entity/compare/v0.52.0...v0.53.0) (2025-12-19)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.52.0](https://github.com/expo/entity/compare/v0.51.0...v0.52.0) (2025-12-19)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.51.0](https://github.com/expo/entity/compare/v0.50.0...v0.51.0) (2025-11-21)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.50.0](https://github.com/expo/entity/compare/v0.49.0...v0.50.0) (2025-10-07)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.49.0](https://github.com/expo/entity/compare/v0.48.0...v0.49.0) (2025-10-07)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.48.0](https://github.com/expo/entity/compare/v0.47.0...v0.48.0) (2025-09-19)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.47.0](https://github.com/expo/entity/compare/v0.46.0...v0.47.0) (2025-09-19)
### Features
- add support for Buffer fields ([#305](https://github.com/expo/entity/issues/305)) ([31b52a5](https://github.com/expo/entity/commit/31b52a5f75842f4810f66c12ad03a10d9192f889))
# [0.46.0](https://github.com/expo/entity/compare/v0.45.0...v0.46.0) (2025-06-26)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.45.0](https://github.com/expo/entity/compare/v0.44.0...v0.45.0) (2025-06-06)
### Features
- add ability to define a custom cache key invalidation version supplier ([#290](https://github.com/expo/entity/issues/290)) ([a3ab61b](https://github.com/expo/entity/commit/a3ab61bfa8d2eab3bbd9c703c5e483184bb67f13))
# [0.44.0](https://github.com/expo/entity/compare/v0.43.0...v0.44.0) (2025-05-29)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.43.0](https://github.com/expo/entity/compare/v0.42.0...v0.43.0) (2025-04-10)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.42.0](https://github.com/expo/entity/compare/v0.41.0...v0.42.0) (2025-04-10)
### Features
- add @expo/entity-testing-utils package ([#280](https://github.com/expo/entity/issues/280)) ([485894a](https://github.com/expo/entity/commit/485894af16e233d533a9480285bbda56812cbe0b))
- add composite field loading and caching ([#272](https://github.com/expo/entity/issues/272)) ([f0aa0da](https://github.com/expo/entity/commit/f0aa0dafebdb56418cbd22dda437233d850db3e7))
- change entity id field generic to field name and derive type where necessary ([#278](https://github.com/expo/entity/issues/278)) ([b7e524c](https://github.com/expo/entity/commit/b7e524c41892797608d0b884410b8d520a80a9ef))
- convert batched/cached loader interface to holder pattern ([#271](https://github.com/expo/entity/issues/271)) ([06b3cb7](https://github.com/expo/entity/commit/06b3cb7f8076ef9e5b83cdf154da159801bd14cc)), closes [#201](https://github.com/expo/entity/issues/201)
- invalidate n+/-1 cacheKeyVersion for entities for push safety ([#275](https://github.com/expo/entity/issues/275)) ([d9c1852](https://github.com/expo/entity/commit/d9c1852706f98f0761ff24ddd244e4ed2beca580))
# [0.41.0](https://github.com/expo/entity/compare/v0.40.0...v0.41.0) (2025-03-07)
- feat!: Default all loaders and mutators to enforcing (#256) ([a976e76](https://github.com/expo/entity/commit/a976e765098bca27049585e43a3bed5c71b9ac6b)), closes [#256](https://github.com/expo/entity/issues/256)
### BREAKING CHANGES
- Default all loaders and mutators to enforcing
# [0.40.0](https://github.com/expo/entity/compare/v0.39.0...v0.40.0) (2025-02-27)
### Reverts
- Revert "v0.40.0" ([fa78ce8](https://github.com/expo/entity/commit/fa78ce84e2f2eb8199e36fd51779fc68905001c0))
# [0.39.0](https://github.com/expo/entity/compare/v0.38.0...v0.39.0) (2024-11-21)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.38.0](https://github.com/expo/entity/compare/v0.37.0...v0.38.0) (2024-06-27)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.37.0](https://github.com/expo/entity/compare/v0.36.0...v0.37.0) (2024-06-12)
### Bug Fixes
- update barrels ([#241](https://github.com/expo/entity/issues/241)) ([8c5f81b](https://github.com/expo/entity/commit/8c5f81bcca107e9b7bbea5f0ab41b0105057806e))
# [0.36.0](https://github.com/expo/entity/compare/v0.35.0...v0.36.0) (2024-06-12)
### Bug Fixes
- constrain entity fields type to string-keyed object ([#235](https://github.com/expo/entity/issues/235)) ([7e2cea1](https://github.com/expo/entity/commit/7e2cea16973a0ae1917f867cd25d6ef7c8eaecef))
# [0.35.0](https://github.com/expo/entity/compare/v0.34.0...v0.35.0) (2024-04-11)
### Bug Fixes
- Revert: require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([1bdfbd6](https://github.com/expo/entity/commit/1bdfbd6b562d1200e4029df0533b5adb2d917831))
# [0.34.0](https://github.com/expo/entity/compare/v0.33.0...v0.34.0) (2024-04-11)
### Features
- require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([8b0b31f](https://github.com/expo/entity/commit/8b0b31fdde5bd565aa527719003ef283a45f55cc))
# [0.33.0](https://github.com/expo/entity/compare/v0.32.0...v0.33.0) (2023-10-06)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.32.0](https://github.com/expo/entity/compare/v0.31.1...v0.32.0) (2023-06-10)
### Bug Fixes
- remove unused field from redis configuration ([#213](https://github.com/expo/entity/issues/213)) ([503a2fb](https://github.com/expo/entity/commit/503a2fb6a8d57f6a09f6da99daf5ab92fc3e7945))
### Features
- entity companion definition static thunk ([#210](https://github.com/expo/entity/issues/210)) ([4b18010](https://github.com/expo/entity/commit/4b18010d42be50ef329f428b08330e21bf676586))
# [0.31.0](https://github.com/expo/entity/compare/v0.30.0...v0.31.0) (2023-01-19)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.30.0](https://github.com/expo/entity/compare/v0.29.0...v0.30.0) (2022-12-23)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.29.0](https://github.com/expo/entity/compare/v0.28.0...v0.29.0) (2022-10-06)
### Features
- add example of redis mget batching using @expo/batcher ([#199](https://github.com/expo/entity/issues/199)) ([684f91b](https://github.com/expo/entity/commit/684f91b1f270851dbe331df2e2620664eea17266))
- change redis cache adapter to use redis-like interface ([#200](https://github.com/expo/entity/issues/200)) ([e381f5b](https://github.com/expo/entity/commit/e381f5b2acfc17dcade2cf6e8506fa1c1c255e73))
# [0.28.0](https://github.com/expo/entity/compare/v0.27.0...v0.28.0) (2022-09-13)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.27.0](https://github.com/expo/entity/compare/v0.26.1...v0.27.0) (2022-08-09)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
## [0.26.1](https://github.com/expo/entity/compare/v0.26.0...v0.26.1) (2022-07-28)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.26.0](https://github.com/expo/entity/compare/v0.25.3...v0.26.0) (2022-07-14)
### Features
- update ioredis to v5, make it a devDependency of the cache packages ([#172](https://github.com/expo/entity/issues/172)) ([9fcfbf7](https://github.com/expo/entity/commit/9fcfbf746a61554cefedf7fb0f1a3a9056b58ffe))
## [0.25.3](https://github.com/expo/entity/compare/v0.25.2...v0.25.3) (2022-03-12)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
## [0.25.2](https://github.com/expo/entity/compare/v0.25.1...v0.25.2) (2022-03-12)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
## [0.25.1](https://github.com/expo/entity/compare/v0.25.0...v0.25.1) (2022-03-10)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.25.0](https://github.com/expo/entity/compare/v0.24.0...v0.25.0) (2022-03-10)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.24.0](https://github.com/expo/entity/compare/v0.23.0...v0.24.0) (2022-02-16)
### Chores
- Refactor cachers to use an abstract class and interface ([#157](https://github.com/expo/entity/pull/157))
# [0.23.0](https://github.com/expo/entity/compare/v0.22.0...v0.23.0) (2022-02-09)
### Chores
- Move data transforming responsibilities to the CacheAdapter instead of the ReadThroughEntityCache. ([#153](https://github.com/expo/entity/pull/153))
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.21.0](https://github.com/expo/entity/compare/v0.20.0...v0.21.0) (2022-01-03)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.20.0](https://github.com/expo/entity/compare/v0.19.0...v0.20.0) (2021-12-29)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.19.0](https://github.com/expo/entity/compare/v0.18.0...v0.19.0) (2021-10-28)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.18.0](https://github.com/expo/entity/compare/v0.17.0...v0.18.0) (2021-10-14)
### Features
- enable noImplicitOverride tsc setting ([#135](https://github.com/expo/entity/issues/135)) ([4263cb9](https://github.com/expo/entity/commit/4263cb9b8b69fe4fb68c74ec1c7aba04508a1555))
- upgrade TypeScript to 4.4 ([#134](https://github.com/expo/entity/issues/134)) ([7612392](https://github.com/expo/entity/commit/7612392dbfd9778d25c465c1626c372d75a6d05a))
# [0.17.0](https://github.com/expo/entity/compare/v0.16.0...v0.17.0) (2021-08-16)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.16.0](https://github.com/expo/entity/compare/v0.15.0...v0.16.0) (2021-07-07)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.15.0](https://github.com/expo/entity/compare/v0.14.1...v0.15.0) (2021-05-26)
### Bug Fixes
- upgrade ioredis, knex, and pg ([#125](https://github.com/expo/entity/issues/125)) ([7c43edf](https://github.com/expo/entity/commit/7c43edf3de37e8af13e2a99bd44f03f54803d9b9))
- use column name instead of field name for redis cache key ([#124](https://github.com/expo/entity/issues/124)) ([d78f452](https://github.com/expo/entity/commit/d78f452bb3ac5527069813a03f5b2264375bd8ab))
### Features
- secondary cache loader ([#123](https://github.com/expo/entity/issues/123)) ([4cba01e](https://github.com/expo/entity/commit/4cba01eb259c87d60b3026ce776e46f781363690))
## [0.14.1](https://github.com/expo/entity/compare/v0.14.0...v0.14.1) (2021-03-24)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.14.0](https://github.com/expo/entity/compare/v0.13.0...v0.14.0) (2021-03-24)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.13.0](https://github.com/expo/entity/compare/v0.12.0...v0.13.0) (2021-02-12)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.12.0](https://github.com/expo/entity/compare/v0.11.0...v0.12.0) (2021-01-22)
### Bug Fixes
- remove cache and database adaptor flavor enums ([#109](https://github.com/expo/entity/issues/109)) ([72a77f8](https://github.com/expo/entity/commit/72a77f8b6893a4b76d3b1cce16659bdf3ce473ee))
# [0.11.0](https://github.com/expo/entity/compare/v0.10.0...v0.11.0) (2020-12-24)
### Features
- better cache adapter error handling ([#102](https://github.com/expo/entity/issues/102)) ([15546aa](https://github.com/expo/entity/commit/15546aad97734ab9ac67f1c012485f096a0e94f1))
# [0.10.0](https://github.com/expo/entity/compare/v0.9.1...v0.10.0) (2020-11-17)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
## [0.9.1](https://github.com/expo/entity/compare/v0.9.0...v0.9.1) (2020-11-06)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.9.0](https://github.com/expo/entity/compare/v0.8.1...v0.9.0) (2020-11-05)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
## [0.8.1](https://github.com/expo/entity/compare/v0.8.0...v0.8.1) (2020-10-09)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.8.0](https://github.com/expo/entity/compare/v0.7.1...v0.8.0) (2020-10-09)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
## [0.7.1](https://github.com/expo/entity/compare/v0.7.0...v0.7.1) (2020-10-01)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.7.0](https://github.com/expo/entity/compare/v0.6.0...v0.7.0) (2020-10-01)
### Bug Fixes
- upgrade packages ([#78](https://github.com/expo/entity/issues/78)) ([9891e74](https://github.com/expo/entity/commit/9891e7469467a28589f529c8d87b10fc2232d3ff))
# [0.6.0](https://github.com/expo/entity/compare/v0.5.2...v0.6.0) (2020-07-23)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
## [0.5.2](https://github.com/expo/entity/compare/v0.5.1...v0.5.2) (2020-07-02)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
## [0.5.1](https://github.com/expo/entity/compare/v0.5.0...v0.5.1) (2020-07-01)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# 0.5.0 (2020-07-01)
### Features
- support entity fields subset of db fields ([#49](https://github.com/expo/entity/issues/49)) ([4e40b2e](https://github.com/expo/entity/commit/4e40b2e521407e521d236978ec3b3b56db3990be))
+6 -13
View File
@@ -11,17 +11,10 @@ During `EntityCompanionProvider` instantiation:
```typescript
import Redis from 'ioredis';
const redisCacheAdapterContext = {
redisClient: new Redis(new URL(process.env.REDIS_URL!).toString()),
makeKeyFn(...parts: string[]): string {
const delimiter = ':';
const escapedParts = parts.map((part) =>
part.replace('\\', '\\\\').replace(delimiter, `\\${delimiter}`)
);
return escapedParts.join(delimiter);
},
const genericRedisCacherContext = {
redisClient: new Redis(new URL(process.env['REDIS_URL']!).toString()),
cacheKeyDelimiter: ':',
cacheKeyPrefix: 'ent-',
cacheKeyVersion: 1,
ttlSecondsPositive: 86400, // 1 day
ttlSecondsNegative: 600, // 10 minutes
};
@@ -35,10 +28,10 @@ export const createDefaultEntityCompanionProvider = (
...
},
{
[CacheAdapterFlavor.REDIS]: {
cacheAdapterProvider: new RedisCacheAdapterProvider(redisCacheAdapterContext),
['redis']: {
cacheAdapterProvider: new RedisCacheAdapterProvider(genericRedisCacheContext),
},
}
);
};
```
```
@@ -1,37 +1,45 @@
{
"name": "@expo/entity-cache-adapter-redis",
"version": "0.4.0",
"version": "0.69.0",
"description": "Redis cache adapter for @expo/entity",
"files": [
"build",
"src"
],
"main": "build/index.js",
"types": "build/index.d.ts",
"scripts": {
"tsc": "tsc",
"clean": "rm -rf build coverage coverage-integration",
"lint": "eslint src --ext '.ts'",
"test": "jest --rootDir --config ../../resources/jest.config.js",
"integration": "../../resources/run-with-docker yarn integration-no-setup",
"integration-no-setup": "jest --config ../../resources/jest-integration.config.js --rootDir --runInBand --passWithNoTests",
"barrelsby": "barrelsby --directory src --location top --exclude tests__ --singleQuotes --exportDefault --delete"
},
"engines": {
"node": ">=8"
},
"keywords": [
"entity"
],
"author": "Expo",
"license": "MIT",
"peerDependencies": {
"@expo/entity": "*"
"author": "Expo",
"files": [
"build",
"!*.tsbuildinfo",
"!__*",
"src"
],
"type": "module",
"main": "build/src/index.js",
"types": "build/src/index.d.ts",
"scripts": {
"build": "tsc --build",
"prepack": "rm -rf build && yarn build",
"clean": "yarn build --clean",
"lint": "yarn run --top-level oxlint --type-aware src",
"lint-fix": "yarn lint --fix",
"test": "yarn test:all --rootDir $(pwd)",
"integration": "yarn integration:all --rootDir $(pwd)"
},
"dependencies": {
"ioredis": "^4.16.1"
"@expo/entity": "workspace:^"
},
"devDependencies": {
"@expo/entity": "^0.4.0"
"@expo/batcher": "1.1.2",
"@expo/entity-testing-utils": "workspace:^",
"@jest/globals": "30.5.1",
"ioredis": "5.11.1",
"ts-mockito": "2.6.1",
"typescript": "6.0.3"
},
"peerDependencies": {
"ioredis": ">=5"
},
"engines": {
"node": ">=18"
}
}
@@ -0,0 +1,300 @@
import type {
CacheLoadResult,
EntityConfiguration,
IEntityGenericCacher,
IEntityLoadKey,
IEntityLoadValue,
} from '@expo/entity';
import {
CacheStatus,
transformCacheObjectToFields,
transformFieldsToCacheObject,
} from '@expo/entity';
import { redisTransformerMap } from './RedisCommon.ts';
import { wrapNativeRedisCallAsync } from './errors/wrapNativeRedisCallAsync.ts';
import { getSurroundingCacheKeyVersionsForInvalidation } from './utils/getSurroundingCacheKeyVersionsForInvalidation.ts';
// Sentinel value we store in Redis to negatively cache a database miss.
// The sentinel value is distinct from any (positively) cached value.
const DOES_NOT_EXIST_REDIS = '';
export interface IRedisTransaction {
set(key: string, value: string, secondsToken: 'EX', seconds: number): this;
exec(): Promise<any>;
}
export interface IRedis {
mget(...args: [...keys: string[]]): Promise<(string | null)[]>;
multi(): IRedisTransaction;
del(...args: [...keys: string[]]): Promise<any>;
}
/**
* The strategy for generating the set of cache keys to invalidate in the Redis cache after entity mutation.
*/
export enum RedisCacheInvalidationStrategy {
/**
* Invalidate just the cache key(s) for the current cacheKeyVersion of the entity.
*/
CURRENT_CACHE_KEY_VERSION = 'current-cache-key-version',
/**
* Invalidate the cache key(s) for the current cacheKeyVersion and the surrounding cache key versions
* (e.g. `1`, `2` and `3` if the current version is `2`). This can be useful for deployment safety, where
* some machines may be operating on an old version of the code and thus an old cacheKeyVersion, and some the new version.
* This strategy generates cache keys for both old and potential future new versions.
*/
SURROUNDING_CACHE_KEY_VERSIONS = 'surrounding-cache-key-versions',
/**
* Invalidate cache keys based on user-specified function from the current cacheKeyVersion to a list of cache key
* versions to invalidate.
*/
CUSTOM = 'custom',
}
export type GenericRedisCacheInvalidationConfig =
| {
/**
* Invalidation strategy for the cache.
*/
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION;
}
| {
/**
* Invalidation strategy for the cache.
*/
invalidationStrategy: RedisCacheInvalidationStrategy.SURROUNDING_CACHE_KEY_VERSIONS;
}
| {
/**
* Invalidation strategy for the cache.
*/
invalidationStrategy: RedisCacheInvalidationStrategy.CUSTOM;
/**
* Function that takes the current cache key version and returns the cache key versions to invalidate.
*/
cacheKeyVersionsToInvalidateFn: (cacheKeyVersion: number) => readonly number[];
};
function validateGenericRedisCacheContext(context: GenericRedisCacheContext): void {
if (context.cacheKeyDelimiter.length === 0) {
throw new Error('GenericRedisCacheContext.cacheKeyDelimiter must be a non-empty string');
}
if (context.cacheKeyDelimiter.includes('\\')) {
throw new Error(
`GenericRedisCacheContext.cacheKeyDelimiter must not contain the escape character "\\" (got ${JSON.stringify(context.cacheKeyDelimiter)})`,
);
}
if (!Number.isInteger(context.ttlSecondsPositive) || context.ttlSecondsPositive <= 0) {
throw new Error(
`GenericRedisCacheContext.ttlSecondsPositive must be a positive integer (got ${context.ttlSecondsPositive})`,
);
}
if (!Number.isInteger(context.ttlSecondsNegative) || context.ttlSecondsNegative <= 0) {
throw new Error(
`GenericRedisCacheContext.ttlSecondsNegative must be a positive integer (got ${context.ttlSecondsNegative})`,
);
}
}
export interface GenericRedisCacheContext {
/**
* Instance of ioredis.Redis
*/
redisClient: IRedis;
/**
* Delimiter used to join the parts of a Redis cache key (cache key prefix,
* versions, entity name, field values, etc). Typically `:`.
*
* The cacher escapes occurrences of this delimiter (and the escape character `\`)
* within each part before joining, so the encoding is injective regardless of
* what the parts contain. This prevents a value that happens to contain the
* delimiter from colliding with a different (key, value) pair.
*/
cacheKeyDelimiter: string;
/**
* Prefix prepended to all entity cache keys. Useful for adding a short, human-readable
* distintion for entity keys, e.g. `ent-`
*/
cacheKeyPrefix: string;
/**
* TTL for caching database hits. Successive entity loads within this TTL
* will be read from cache (unless invalidated).
*/
ttlSecondsPositive: number;
/**
* TTL for negatively caching database misses. Successive entity loads within
* this TTL will be assumed not present in the database (unless invalidated).
*/
ttlSecondsNegative: number;
/**
* Configuration for cache invalidation strategy.
*/
invalidationConfig: GenericRedisCacheInvalidationConfig;
}
export class GenericRedisCacher<
TFields extends Record<string, any>,
TIDField extends keyof TFields,
> implements IEntityGenericCacher<TFields, TIDField> {
constructor(
private readonly context: GenericRedisCacheContext,
private readonly entityConfiguration: EntityConfiguration<TFields, TIDField>,
) {
validateGenericRedisCacheContext(context);
}
public async loadManyAsync(
keys: readonly string[],
): Promise<ReadonlyMap<string, CacheLoadResult<TFields>>> {
if (keys.length === 0) {
return new Map();
}
const redisResults = await wrapNativeRedisCallAsync(() =>
this.context.redisClient.mget(...keys),
);
const results = new Map<string, CacheLoadResult<TFields>>();
for (let i = 0; i < keys.length; i++) {
const key = keys[i]!;
const redisResult = redisResults[i];
if (redisResult === DOES_NOT_EXIST_REDIS) {
results.set(key, {
status: CacheStatus.NEGATIVE,
});
} else if (redisResult) {
results.set(key, {
status: CacheStatus.HIT,
item: transformCacheObjectToFields(
this.entityConfiguration,
redisTransformerMap,
JSON.parse(redisResult),
),
});
} else {
results.set(key, {
status: CacheStatus.MISS,
});
}
}
return results;
}
public async cacheManyAsync(objectMap: ReadonlyMap<string, Readonly<TFields>>): Promise<void> {
if (objectMap.size === 0) {
return;
}
let redisTransaction = this.context.redisClient.multi();
objectMap.forEach((object, key) => {
redisTransaction = redisTransaction.set(
key,
JSON.stringify(
transformFieldsToCacheObject(this.entityConfiguration, redisTransformerMap, object),
),
'EX',
this.context.ttlSecondsPositive,
);
});
await wrapNativeRedisCallAsync(() => redisTransaction.exec());
}
public async cacheDBMissesAsync(keys: readonly string[]): Promise<void> {
if (keys.length === 0) {
return;
}
let redisTransaction = this.context.redisClient.multi();
keys.forEach((key) => {
redisTransaction = redisTransaction.set(
key,
DOES_NOT_EXIST_REDIS,
'EX',
this.context.ttlSecondsNegative,
);
});
await wrapNativeRedisCallAsync(() => redisTransaction.exec());
}
public async invalidateManyAsync(keys: readonly string[]): Promise<void> {
if (keys.length === 0) {
return;
}
await wrapNativeRedisCallAsync(() => this.context.redisClient.del(...keys));
}
private makeCacheKeyForCacheKeyVersion<
TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
TSerializedLoadValue,
TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
>(key: TLoadKey, value: TLoadValue, cacheKeyVersion: number): string {
const cacheKeyType = key.getLoadMethodType();
const keyAndValueParts = key.createCacheKeyPartsForLoadValue(this.entityConfiguration, value);
const allParts = [
this.context.cacheKeyPrefix,
cacheKeyType,
this.entityConfiguration.tableName,
`v4.${cacheKeyVersion}`,
...keyAndValueParts,
];
const delimiter = this.context.cacheKeyDelimiter;
return allParts
.map((part) => part.replaceAll('\\', '\\\\').replaceAll(delimiter, `\\${delimiter}`))
.join(delimiter);
}
public makeCacheKeyForStorage<
TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
TSerializedLoadValue,
TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
>(key: TLoadKey, value: TLoadValue): string {
return this.makeCacheKeyForCacheKeyVersion(
key,
value,
this.entityConfiguration.cacheKeyVersion,
);
}
public makeCacheKeysForInvalidation<
TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
TSerializedLoadValue,
TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
>(key: TLoadKey, value: TLoadValue): readonly string[] {
switch (this.context.invalidationConfig.invalidationStrategy) {
case RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION:
return [
this.makeCacheKeyForCacheKeyVersion(key, value, this.entityConfiguration.cacheKeyVersion),
];
case RedisCacheInvalidationStrategy.SURROUNDING_CACHE_KEY_VERSIONS:
return getSurroundingCacheKeyVersionsForInvalidation(
this.entityConfiguration.cacheKeyVersion,
).map((cacheKeyVersion) =>
this.makeCacheKeyForCacheKeyVersion(key, value, cacheKeyVersion),
);
case RedisCacheInvalidationStrategy.CUSTOM: {
const cacheKeyVersions = this.context.invalidationConfig.cacheKeyVersionsToInvalidateFn(
this.entityConfiguration.cacheKeyVersion,
);
if (cacheKeyVersions.length === 0) {
throw new Error(
`GenericRedisCacheContext.invalidationConfig.cacheKeyVersionsToInvalidateFn returned an empty list for cacheKeyVersion ${this.entityConfiguration.cacheKeyVersion}; this would silently disable invalidation`,
);
}
return cacheKeyVersions.map((cacheKeyVersion) =>
this.makeCacheKeyForCacheKeyVersion(key, value, cacheKeyVersion),
);
}
}
}
}
@@ -1,165 +0,0 @@
import {
EntityCacheAdapter,
EntityConfiguration,
FieldTransformerMap,
CacheLoadResult,
CacheStatus,
} from '@expo/entity';
import { Redis } from 'ioredis';
import { redisTransformerMap } from './RedisCommon';
// Sentinel value we store in Redis to negatively cache a database miss.
// The sentinel value is distinct from any (positively) cached value.
const DOES_NOT_EXIST_REDIS = '';
export interface RedisCacheAdapterContext {
/**
* Instance of ioredis.Redis
*/
redisClient: Redis;
/**
* Create a key string for key parts (cache key prefix, versions, entity name, etc).
* Most commonly a simple `parts.join(':')`. See integration test for example.
*/
makeKeyFn: (...parts: string[]) => string;
/**
* Global cache version for the entity framework. Bumping this version will
* invalidate the cache for all entities at once.
*/
cacheKeyVersion: number;
/**
* Prefix prepended to all entity cache keys. Useful for adding a short, human-readable
* distintion for entity keys, e.g. `ent-`
*/
cacheKeyPrefix: string;
/**
* TTL for caching database hits. Successive entity loads within this TTL
* will be read from cache (unless invalidated).
*/
ttlSecondsPositive: number;
/**
* TTL for negatively caching database misses. Successive entity loads within
* this TTL will be assumed not present in the database (unless invalidated).
*/
ttlSecondsNegative: number;
}
export default class RedisCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
constructor(
entityConfiguration: EntityConfiguration<TFields>,
private readonly context: RedisCacheAdapterContext
) {
super(entityConfiguration);
}
public getFieldTransformerMap(): FieldTransformerMap {
return redisTransformerMap;
}
public async loadManyAsync<N extends keyof TFields>(
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult>> {
if (fieldValues.length === 0) {
return new Map();
}
const redisKeys = fieldValues.map((fieldValue) => this.makeCacheKey(fieldName, fieldValue));
const redisResults = await this.context.redisClient.mget(...redisKeys);
const results = new Map<NonNullable<TFields[N]>, CacheLoadResult>();
for (let i = 0; i < fieldValues.length; i++) {
const fieldValue = fieldValues[i];
const redisResult = redisResults[i];
if (redisResult === DOES_NOT_EXIST_REDIS) {
results.set(fieldValue, {
status: CacheStatus.NEGATIVE,
});
} else if (redisResult) {
results.set(fieldValue, {
status: CacheStatus.HIT,
item: JSON.parse(redisResult),
});
} else {
results.set(fieldValue, {
status: CacheStatus.MISS,
});
}
}
return results;
}
public async cacheManyAsync<N extends keyof TFields>(
fieldName: N,
objectMap: ReadonlyMap<NonNullable<TFields[N]>, object>
): Promise<void> {
if (objectMap.size === 0) {
return;
}
let redisTransaction = this.context.redisClient.multi();
objectMap.forEach((object, fieldValue) => {
const redisKey = this.makeCacheKey(fieldName, fieldValue);
redisTransaction = redisTransaction.set(
redisKey,
JSON.stringify(object),
'EX',
this.context.ttlSecondsPositive
);
});
await redisTransaction.exec();
}
public async cacheDBMissesAsync<N extends keyof TFields>(
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<void> {
if (fieldValues.length === 0) {
return;
}
let redisTransaction = this.context.redisClient.multi();
fieldValues.forEach((fieldValue) => {
const redisKey = this.makeCacheKey(fieldName, fieldValue);
redisTransaction = redisTransaction.set(
redisKey,
DOES_NOT_EXIST_REDIS,
'EX',
this.context.ttlSecondsNegative
);
});
await redisTransaction.exec();
}
public async invalidateManyAsync<N extends keyof TFields>(
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<void> {
if (fieldValues.length === 0) {
return;
}
const redisKeys = fieldValues.map((fieldValue) => this.makeCacheKey(fieldName, fieldValue));
await this.context.redisClient.del(...redisKeys);
}
private makeCacheKey<N extends keyof TFields>(
fieldName: N,
fieldValue: NonNullable<TFields[N]>
): string {
return this.context.makeKeyFn(
this.context.cacheKeyPrefix,
this.entityConfiguration.tableName,
`v${this.entityConfiguration.cacheKeyVersion}`,
fieldName as string,
String(fieldValue)
);
}
}
@@ -1,13 +1,19 @@
import { IEntityCacheAdapterProvider, EntityConfiguration, EntityCacheAdapter } from '@expo/entity';
import type {
EntityConfiguration,
IEntityCacheAdapter,
IEntityCacheAdapterProvider,
} from '@expo/entity';
import { GenericEntityCacheAdapter } from '@expo/entity';
import RedisCacheAdapter, { RedisCacheAdapterContext } from './RedisCacheAdapter';
import type { GenericRedisCacheContext } from './GenericRedisCacher.ts';
import { GenericRedisCacher } from './GenericRedisCacher.ts';
export default class RedisCacheAdapterProvider implements IEntityCacheAdapterProvider {
constructor(private readonly context: RedisCacheAdapterContext) {}
export class RedisCacheAdapterProvider implements IEntityCacheAdapterProvider {
constructor(private readonly context: GenericRedisCacheContext) {}
getCacheAdapter<TFields>(
entityConfiguration: EntityConfiguration<TFields>
): EntityCacheAdapter<TFields> {
return new RedisCacheAdapter(entityConfiguration, this.context);
getCacheAdapter<TFields extends Record<string, any>, TIDField extends keyof TFields>(
entityConfiguration: EntityConfiguration<TFields, TIDField>,
): IEntityCacheAdapter<TFields, TIDField> {
return new GenericEntityCacheAdapter(new GenericRedisCacher(this.context, entityConfiguration));
}
}
@@ -1,4 +1,4 @@
import { DateField } from '@expo/entity';
import { BufferField, DateField } from '@expo/entity';
export const redisTransformerMap = new Map([
[
@@ -22,4 +22,25 @@ export const redisTransformerMap = new Map([
read: (val: any) => (val ? new Date(val) : val),
},
],
[
BufferField.name,
{
/**
* All Redis fields are serialized to JSON before being written.
* In the case of buffers, they need to be explicitly reconstructed
* as JavaScript Buffer objects upon retrieval. We also explicitly
* serialize them to base64 strings to ensure no underlying Redis
* changes affect the fields.
*
* Write behavior: Value to write will always be either a Buffer or null.
* Behavior is to pass through null and convert buffers
* to base64 strings.
* Read behavior: Value will always be either null or a base64 string.
* behavior is to convert base64 strings back into Buffer
* objects and pass through null.
*/
write: (val: Buffer) => (val ? val.toString('base64') : null),
read: (val: any) => (val ? Buffer.from(val, 'base64') : val),
},
],
]);
@@ -0,0 +1,180 @@
import { Batcher } from '@expo/batcher';
import type { GenericEntityCacheAdapter } from '@expo/entity';
import { SingleFieldHolder, SingleFieldValueHolder, ViewerContext, zipToMap } from '@expo/entity';
import nullthrows from '@expo/nullthrows';
import { afterAll, beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals';
import invariant from 'invariant';
import { Redis } from 'ioredis';
import { URL } from 'url';
import { v4 as uuidv4 } from 'uuid';
import type { GenericRedisCacheContext, IRedis, IRedisTransaction } from '../GenericRedisCacher.ts';
import { GenericRedisCacher, RedisCacheInvalidationStrategy } from '../GenericRedisCacher.ts';
import type { RedisTestEntityFields } from '../__testfixtures__/RedisTestEntity.ts';
import { RedisTestEntity } from '../__testfixtures__/RedisTestEntity.ts';
import { createRedisIntegrationTestEntityCompanionProvider } from '../__testfixtures__/createRedisIntegrationTestEntityCompanionProvider.ts';
class BatchedRedis implements IRedis {
private readonly mgetBatcher = new Batcher<string[], (string | null)[]>(
this.batchMgetAsync.bind(this),
{
maxBatchInterval: 0,
},
);
constructor(private readonly redis: Redis) {}
private async batchMgetAsync(keySets: string[][]): Promise<(string | null)[][]> {
// ordered distinct keys to fetch
const allKeysToFetch = [...new Set(keySets.flat())];
// fetch the distinct keys
const allResults = await this.redis.mget(...allKeysToFetch);
// put them into a map for fast lookup
const keysToResults = zipToMap(allKeysToFetch, allResults);
// re-associate them with original key sets
return keySets.map((keySet) =>
keySet.map((key) => {
const result = keysToResults.get(key);
invariant(result !== undefined, 'result should not be undefined');
return result;
}),
);
}
// eslint-disable-next-line @typescript-eslint/naming-convention
async mget(...args: string[]): Promise<(string | null)[]> {
return await this.mgetBatcher.batchAsync(args);
}
multi(): IRedisTransaction {
return this.redis.multi();
}
// eslint-disable-next-line @typescript-eslint/naming-convention
async del(...args: string[]): Promise<void> {
await this.redis.del(...args);
}
}
describe(GenericRedisCacher, () => {
const redis = new Redis(new URL(process.env['REDIS_URL']!).toString());
const redisClient = new BatchedRedis(redis);
let genericRedisCacheContext: GenericRedisCacheContext;
beforeAll(() => {
genericRedisCacheContext = {
redisClient,
cacheKeyDelimiter: ':',
cacheKeyPrefix: 'test-',
ttlSecondsPositive: 86400, // 1 day
ttlSecondsNegative: 600, // 10 minutes
invalidationConfig: {
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
},
};
});
beforeEach(async () => {
await redis.flushdb();
});
afterAll(async () => {
redis.disconnect();
});
it('has correct caching behavior', async () => {
// simulate two requests
const viewerContext = new ViewerContext(
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
);
const mgetSpy = jest.spyOn(redis, 'mget');
const genericCacher = (
viewerContext.entityCompanionProvider.getCompanionForEntity(RedisTestEntity)[
'tableDataCoordinator'
]['cacheAdapter'] as GenericEntityCacheAdapter<RedisTestEntityFields, 'id'>
)['genericCacher'];
const entity1Created = await RedisTestEntity.creator(viewerContext)
.setField('name', 'blah')
.createAsync();
// loading an entity should put it in cache. load by multiple requests and multiple fields in same tick to ensure batch works
mgetSpy.mockClear();
const viewerContext1 = new ViewerContext(
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
);
const viewerContext2 = new ViewerContext(
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
);
const viewerContext3 = new ViewerContext(
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
);
const [entity1, entity2, entity3] = await Promise.all([
RedisTestEntity.loader(viewerContext1).loadByIDAsync(entity1Created.getID()),
RedisTestEntity.loader(viewerContext2).loadByIDAsync(entity1Created.getID()),
RedisTestEntity.loader(viewerContext3).loadByFieldEqualingAsync(
'name',
entity1Created.getField('name'),
),
]);
expect(mgetSpy).toHaveBeenCalledTimes(1);
expect(mgetSpy.mock.calls[0]).toHaveLength(2); // should dedupe the first two loads
expect(entity1.getID()).toEqual(entity2.getID());
expect(entity2.getID()).toEqual(nullthrows(entity3).getID());
const cacheKeyEntity1 = genericCacher.makeCacheKeyForStorage(
new SingleFieldHolder('id'),
new SingleFieldValueHolder(entity1Created.getID()),
);
const cachedJSON = await redis.get(cacheKeyEntity1);
const cachedValue = JSON.parse(cachedJSON!);
expect(cachedValue).toMatchObject({
id: entity1.getID(),
name: 'blah',
});
const cacheKeyEntity1NameField = genericCacher.makeCacheKeyForStorage(
new SingleFieldHolder('name'),
new SingleFieldValueHolder(entity1Created.getField('name')),
);
await RedisTestEntity.loader(viewerContext).loadByFieldEqualingAsync(
'name',
entity1Created.getField('name'),
);
await expect(redis.get(cacheKeyEntity1NameField)).resolves.toEqual(cachedJSON);
// simulate non existent db fetch, should write negative result ('') to cache
const nonExistentId = uuidv4();
const entityNonExistentResult =
await RedisTestEntity.loaderWithAuthorizationResults(viewerContext).loadByIDAsync(
nonExistentId,
);
expect(entityNonExistentResult.ok).toBe(false);
const cacheKeyNonExistent = genericCacher.makeCacheKeyForStorage(
new SingleFieldHolder('id'),
new SingleFieldValueHolder(nonExistentId),
);
const nonExistentCachedValue = await redis.get(cacheKeyNonExistent);
expect(nonExistentCachedValue).toEqual('');
// load again through entities framework to ensure it reads negative result
const entityNonExistentResult2 =
await RedisTestEntity.loaderWithAuthorizationResults(viewerContext).loadByIDAsync(
nonExistentId,
);
expect(entityNonExistentResult2.ok).toBe(false);
// invalidate from cache to ensure it invalidates correctly in both caches
await RedisTestEntity.invalidationUtils(viewerContext).invalidateFieldsAsync(
entity1.getAllFields(),
);
await expect(redis.get(cacheKeyEntity1)).resolves.toBeNull();
await expect(redis.get(cacheKeyEntity1NameField)).resolves.toBeNull();
});
});
@@ -0,0 +1,228 @@
import type { GenericEntityCacheAdapter } from '@expo/entity';
import {
CompositeFieldHolder,
CompositeFieldValueHolder,
SingleFieldHolder,
SingleFieldValueHolder,
ViewerContext,
} from '@expo/entity';
import { enforceAsyncResult } from '@expo/results';
import { afterAll, beforeAll, beforeEach, describe, expect, it } from '@jest/globals';
import { Redis } from 'ioredis';
import { URL } from 'url';
import { v4 as uuidv4 } from 'uuid';
import type { GenericRedisCacheContext } from '../GenericRedisCacher.ts';
import { GenericRedisCacher, RedisCacheInvalidationStrategy } from '../GenericRedisCacher.ts';
import type { RedisTestEntityFields } from '../__testfixtures__/RedisTestEntity.ts';
import { RedisTestEntity } from '../__testfixtures__/RedisTestEntity.ts';
import { createRedisIntegrationTestEntityCompanionProvider } from '../__testfixtures__/createRedisIntegrationTestEntityCompanionProvider.ts';
class TestViewerContext extends ViewerContext {}
describe(GenericRedisCacher, () => {
let genericRedisCacheContext: GenericRedisCacheContext;
beforeAll(() => {
genericRedisCacheContext = {
redisClient: new Redis(new URL(process.env['REDIS_URL']!).toString()),
cacheKeyDelimiter: ':',
cacheKeyPrefix: 'test-',
ttlSecondsPositive: 86400, // 1 day
ttlSecondsNegative: 600, // 10 minutes
invalidationConfig: {
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
},
};
});
beforeEach(async () => {
await (genericRedisCacheContext.redisClient as Redis).flushdb();
});
afterAll(async () => {
(genericRedisCacheContext.redisClient as Redis).disconnect();
});
it('has correct caching behavior', async () => {
const viewerContext = new TestViewerContext(
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
);
const genericCacher = (
viewerContext.entityCompanionProvider.getCompanionForEntity(RedisTestEntity)[
'tableDataCoordinator'
]['cacheAdapter'] as GenericEntityCacheAdapter<RedisTestEntityFields, 'id'>
)['genericCacher'];
const entity1Created = await RedisTestEntity.creator(viewerContext)
.setField('name', 'blah')
.setField('dateField', new Date())
.setField('bufferField', Buffer.from('hello world'))
.createAsync();
// loading an entity should put it in cache
const entity1 = await RedisTestEntity.loader(viewerContext).loadByIDAsync(
entity1Created.getID(),
);
const cachedSingleJSON = await (genericRedisCacheContext.redisClient as Redis).get(
genericCacher.makeCacheKeyForStorage(
new SingleFieldHolder('id'),
new SingleFieldValueHolder(entity1.getID()),
),
);
const cachedSingleValue = JSON.parse(cachedSingleJSON!);
expect(cachedSingleValue).toMatchObject({
id: entity1.getID(),
name: 'blah',
});
// loading an entity by composite field should put it in cache
const entity2 = await RedisTestEntity.loader(viewerContext).loadByCompositeFieldEqualingAsync(
['name', 'id'],
{ name: 'blah', id: entity1.getID() },
);
const cachedCompositeJSON = await (genericRedisCacheContext.redisClient as Redis).get(
genericCacher.makeCacheKeyForStorage(
new CompositeFieldHolder<RedisTestEntityFields, 'id'>(['id', 'name']),
new CompositeFieldValueHolder({ id: entity2!.getID(), name: 'blah' }),
),
);
const cachedCompositeValue = JSON.parse(cachedCompositeJSON!);
expect(cachedCompositeValue).toMatchObject({
id: entity2!.getID(),
name: 'blah',
});
// simulate non existent db fetch, should write negative result ('') to cache
const nonExistentId = uuidv4();
const entityNonExistentResult =
await RedisTestEntity.loaderWithAuthorizationResults(viewerContext).loadByIDAsync(
nonExistentId,
);
expect(entityNonExistentResult.ok).toBe(false);
const nonExistentCachedValue = await (genericRedisCacheContext.redisClient as Redis).get(
genericCacher.makeCacheKeyForStorage(
new SingleFieldHolder('id'),
new SingleFieldValueHolder(nonExistentId),
),
);
expect(nonExistentCachedValue).toEqual('');
const entityNonExistentCompositeResult = await RedisTestEntity.loader(
viewerContext,
).loadByCompositeFieldEqualingAsync(['name', 'id'], { name: 'blah', id: nonExistentId });
expect(entityNonExistentCompositeResult).toBe(null);
const nonExistentCompositeCachedValue = await (
genericRedisCacheContext.redisClient as Redis
).get(
genericCacher.makeCacheKeyForStorage(
new CompositeFieldHolder<RedisTestEntityFields, 'id'>(['id', 'name']),
new CompositeFieldValueHolder({ id: nonExistentId, name: 'blah' }),
),
);
expect(nonExistentCompositeCachedValue).toEqual('');
// load again through entities framework to ensure it reads negative result
const entityNonExistentResult2 =
await RedisTestEntity.loaderWithAuthorizationResults(viewerContext).loadByIDAsync(
nonExistentId,
);
expect(entityNonExistentResult2.ok).toBe(false);
const entityNonExistentCompositeResult2 = await RedisTestEntity.loader(
viewerContext,
).loadByCompositeFieldEqualingAsync(['name', 'id'], { name: 'blah', id: nonExistentId });
expect(entityNonExistentCompositeResult2).toBe(null);
// invalidate from cache to ensure it invalidates correctly
await RedisTestEntity.invalidationUtils(viewerContext).invalidateFieldsAsync(
entity1.getAllFields(),
);
const cachedValueNullKeys = genericCacher.makeCacheKeysForInvalidation(
new SingleFieldHolder('id'),
new SingleFieldValueHolder(entity1.getID()),
);
const cachedValueNull = await (genericRedisCacheContext.redisClient as Redis).mget(
...cachedValueNullKeys,
);
expect(cachedValueNull.every((c) => c === null)).toBe(true);
const cachedValueNullCompositeKeys = genericCacher.makeCacheKeysForInvalidation(
new CompositeFieldHolder<RedisTestEntityFields, 'id'>(['id', 'name']),
new CompositeFieldValueHolder({ id: entity1.getID(), name: 'blah' }),
);
const cachedValueNullComposite = await (genericRedisCacheContext.redisClient as Redis).mget(
...cachedValueNullCompositeKeys,
);
expect(cachedValueNullComposite.every((c) => c === null)).toBe(true);
});
it('caches and restores date fields', async () => {
const viewerContext = new TestViewerContext(
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
);
const date = new Date();
const entity1 = await enforceAsyncResult(
RedisTestEntity.creatorWithAuthorizationResults(viewerContext)
.setField('dateField', date)
.createAsync(),
);
expect(entity1.getField('dateField')).toEqual(date);
const entity2 = await RedisTestEntity.loader(viewerContext).loadByIDAsync(entity1.getID());
expect(entity2.getField('dateField')).toEqual(date);
// simulate new request
const vc2 = new TestViewerContext(
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
);
const entity3 = await RedisTestEntity.loader(vc2).loadByIDAsync(entity1.getID());
expect(entity3.getField('dateField')).toEqual(date);
});
it('caches and restores empty string field keys', async () => {
const viewerContext = new TestViewerContext(
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
);
const entity1 = await enforceAsyncResult(
RedisTestEntity.creatorWithAuthorizationResults(viewerContext)
.setField('name', '')
.createAsync(),
);
const entity2 = await RedisTestEntity.loader(viewerContext).loadByFieldEqualingAsync(
'name',
'',
);
expect(entity2?.getID()).toEqual(entity1.getID());
// simulate new request
const vc2 = new TestViewerContext(
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
);
const entity3 = await RedisTestEntity.loader(vc2).loadByFieldEqualingAsync('name', '');
expect(entity3?.getID()).toEqual(entity1.getID());
});
it('caches and restores buffer fields', async () => {
const viewerContext = new TestViewerContext(
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
);
const buffer = Buffer.from('hello world');
const entity1 = await enforceAsyncResult(
RedisTestEntity.creatorWithAuthorizationResults(viewerContext)
.setField('bufferField', buffer)
.createAsync(),
);
expect(entity1.getField('bufferField')).toEqual(buffer);
const entity2 = await RedisTestEntity.loader(viewerContext).loadByIDAsync(entity1.getID());
expect(entity2.getField('bufferField')).toEqual(buffer);
// simulate new request
const vc2 = new TestViewerContext(
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
);
const entity3 = await RedisTestEntity.loader(vc2).loadByIDAsync(entity1.getID());
expect(entity3.getField('bufferField')).toEqual(buffer);
});
});
@@ -0,0 +1,117 @@
import { CacheStatus, ViewerContext } from '@expo/entity';
import { afterAll, beforeAll, beforeEach, describe, expect, it } from '@jest/globals';
import { Redis } from 'ioredis';
import { URL } from 'url';
import type { GenericRedisCacheContext } from '../GenericRedisCacher.ts';
import { GenericRedisCacher, RedisCacheInvalidationStrategy } from '../GenericRedisCacher.ts';
import type { RedisTestEntityFields } from '../__testfixtures__/RedisTestEntity.ts';
import {
RedisTestEntity,
redisTestEntityConfiguration,
} from '../__testfixtures__/RedisTestEntity.ts';
import { createRedisIntegrationTestEntityCompanionProvider } from '../__testfixtures__/createRedisIntegrationTestEntityCompanionProvider.ts';
class TestViewerContext extends ViewerContext {}
describe(GenericRedisCacher, () => {
let genericRedisCacheContext: GenericRedisCacheContext;
beforeAll(() => {
genericRedisCacheContext = {
redisClient: new Redis(new URL(process.env['REDIS_URL']!).toString()),
cacheKeyDelimiter: ':',
cacheKeyPrefix: 'test-',
ttlSecondsPositive: 86400, // 1 day
ttlSecondsNegative: 600, // 10 minutes
invalidationConfig: {
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
},
};
});
beforeEach(async () => {
await (genericRedisCacheContext.redisClient as Redis).flushdb();
});
afterAll(async () => {
(genericRedisCacheContext.redisClient as Redis).disconnect();
});
it('has correct caching and loading behavior', async () => {
const viewerContext = new TestViewerContext(
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
);
const genericRedisCacher = new GenericRedisCacher(
genericRedisCacheContext,
redisTestEntityConfiguration,
);
const date = new Date();
const buffer = Buffer.from('hello world');
const entity1Created = await RedisTestEntity.creator(viewerContext)
.setField('name', 'blah')
.setField('dateField', date)
.setField('bufferField', buffer)
.createAsync();
const testKey = `test-id-key-${entity1Created.getID()}`;
const objectMap = new Map<string, Readonly<RedisTestEntityFields>>([
[testKey, entity1Created.getAllFields()],
]);
await genericRedisCacher.cacheManyAsync(objectMap);
const cachedJSON = await (genericRedisCacheContext.redisClient as Redis).get(testKey);
const cachedValue = JSON.parse(cachedJSON!);
expect(cachedValue).toMatchObject({
id: entity1Created.getID(),
dateField: date.toISOString(),
});
const loadedObjectMap = await genericRedisCacher.loadManyAsync([testKey]);
const cacheLoadResult = loadedObjectMap.get(testKey)!;
expect(cacheLoadResult).toMatchObject({
status: CacheStatus.HIT,
item: entity1Created.getAllFields(),
});
expect(loadedObjectMap.size).toBe(1);
});
it('has correct negative caching behaviour', async () => {
const genericRedisCacher = new GenericRedisCacher(
genericRedisCacheContext,
redisTestEntityConfiguration,
);
const testKey = `test-id-key-non-existent-id`;
await genericRedisCacher.cacheDBMissesAsync([testKey]);
const loadedObjectMap = await genericRedisCacher.loadManyAsync([testKey]);
const cacheLoadResult = loadedObjectMap.get(testKey)!;
expect(cacheLoadResult.status).toBe(CacheStatus.NEGATIVE);
});
it('has correct invalidation behaviour', async () => {
const viewerContext = new TestViewerContext(
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
);
const genericRedisCacher = new GenericRedisCacher(
genericRedisCacheContext,
redisTestEntityConfiguration,
);
const date = new Date();
const buffer = Buffer.from('hello world');
const entity1Created = await RedisTestEntity.creator(viewerContext)
.setField('name', 'blah')
.setField('dateField', date)
.setField('bufferField', buffer)
.createAsync();
const testKey = `test-id-key-${entity1Created.getID()}`;
const objectMap = new Map<string, Readonly<RedisTestEntityFields>>([
[testKey, entity1Created.getAllFields()],
]);
await genericRedisCacher.cacheManyAsync(objectMap);
await genericRedisCacher.invalidateManyAsync([testKey]);
const loadedObjectMap = await genericRedisCacher.loadManyAsync([testKey]);
const cacheLoadResult = loadedObjectMap.get(testKey)!;
expect(cacheLoadResult.status).toBe(CacheStatus.MISS);
});
});
@@ -1,107 +0,0 @@
import { ViewerContext } from '@expo/entity';
import { enforceAsyncResult } from '@expo/results';
import Redis from 'ioredis';
import { URL } from 'url';
import { v4 as uuidv4 } from 'uuid';
import RedisCacheAdapter, { RedisCacheAdapterContext } from '../RedisCacheAdapter';
import RedisTestEntity from '../testfixtures/RedisTestEntity';
import { createRedisIntegrationTestEntityCompanionProvider } from '../testfixtures/createRedisIntegrationTestEntityCompanionProvider';
class TestViewerContext extends ViewerContext {}
describe(RedisCacheAdapter, () => {
let redisCacheAdapterContext: RedisCacheAdapterContext;
beforeAll(() => {
redisCacheAdapterContext = {
redisClient: new Redis(new URL(process.env.REDIS_URL!).toString()),
makeKeyFn(...parts: string[]): string {
const delimiter = ':';
const escapedParts = parts.map((part) =>
part.replace('\\', '\\\\').replace(delimiter, `\\${delimiter}`)
);
return escapedParts.join(delimiter);
},
cacheKeyPrefix: 'test-',
cacheKeyVersion: 1,
ttlSecondsPositive: 86400, // 1 day
ttlSecondsNegative: 600, // 10 minutes
};
});
beforeEach(async () => {
await redisCacheAdapterContext.redisClient.flushdb();
});
afterAll(async () => {
redisCacheAdapterContext.redisClient.disconnect();
});
it('has correct caching behavior', async () => {
const vc1 = new TestViewerContext(
createRedisIntegrationTestEntityCompanionProvider(redisCacheAdapterContext)
);
const cacheAdapter = vc1.entityCompanionProvider.getCompanionForEntity(
RedisTestEntity,
RedisTestEntity.getCompanionDefinition()
)['cacheAdapter'];
const cacheKeyMaker = cacheAdapter['makeCacheKey'].bind(cacheAdapter);
// creating an entity should put it in cache (loader loads it after creation)
const entity1 = await enforceAsyncResult(
RedisTestEntity.creator(vc1).setField('name', 'blah').createAsync()
);
const cachedJSON = await redisCacheAdapterContext.redisClient.get(
cacheKeyMaker('id', entity1.getID())
);
const cachedValue = JSON.parse(cachedJSON!);
expect(cachedValue).toMatchObject({
id: entity1.getID(),
name: 'blah',
});
// simulate non existent db fetch, should write negative result ('') to cache
const nonExistentId = uuidv4();
const entityNonExistentResult = await RedisTestEntity.loader(vc1).loadByIDAsync(nonExistentId);
expect(entityNonExistentResult.ok).toBe(false);
const nonExistentCachedValue = await redisCacheAdapterContext.redisClient.get(
cacheKeyMaker('id', nonExistentId)
);
expect(nonExistentCachedValue).toEqual('');
// load again through entities framework to ensure it reads negative result
const entityNonExistentResult2 = await RedisTestEntity.loader(vc1).loadByIDAsync(nonExistentId);
expect(entityNonExistentResult2.ok).toBe(false);
// invalidate from cache to ensure it invalidates correctly
await RedisTestEntity.loader(vc1).invalidateFieldsAsync(entity1.getAllFields());
const cachedValueNull = await redisCacheAdapterContext.redisClient.get(
cacheKeyMaker('id', entity1.getID())
);
expect(cachedValueNull).toBe(null);
});
it('caches and restores date fields', async () => {
const vc1 = new TestViewerContext(
createRedisIntegrationTestEntityCompanionProvider(redisCacheAdapterContext)
);
const date = new Date();
const entity1 = await enforceAsyncResult(
RedisTestEntity.creator(vc1).setField('dateField', date).createAsync()
);
expect(entity1.getField('dateField')).toEqual(date);
const entity2 = await RedisTestEntity.loader(vc1).enforcing().loadByIDAsync(entity1.getID());
expect(entity2.getField('dateField')).toEqual(date);
// simulate new request
const vc2 = new TestViewerContext(
createRedisIntegrationTestEntityCompanionProvider(redisCacheAdapterContext)
);
const entity3 = await RedisTestEntity.loader(vc2).enforcing().loadByIDAsync(entity1.getID());
expect(entity3.getField('dateField')).toEqual(date);
});
});
@@ -0,0 +1,45 @@
import { EntityCacheAdapterTransientError, ViewerContext } from '@expo/entity';
import { beforeAll, beforeEach, describe, expect, it } from '@jest/globals';
import { Redis } from 'ioredis';
import { URL } from 'url';
import type { GenericRedisCacheContext } from '../GenericRedisCacher.ts';
import { GenericRedisCacher, RedisCacheInvalidationStrategy } from '../GenericRedisCacher.ts';
import { RedisTestEntity } from '../__testfixtures__/RedisTestEntity.ts';
import { createRedisIntegrationTestEntityCompanionProvider } from '../__testfixtures__/createRedisIntegrationTestEntityCompanionProvider.ts';
class TestViewerContext extends ViewerContext {}
describe(GenericRedisCacher, () => {
const redisClient = new Redis(new URL(process.env['REDIS_URL']!).toString());
let genericRedisCacheContext: GenericRedisCacheContext;
beforeAll(() => {
genericRedisCacheContext = {
redisClient,
cacheKeyDelimiter: ':',
cacheKeyPrefix: 'test-',
ttlSecondsPositive: 86400, // 1 day
ttlSecondsNegative: 600, // 10 minutes
invalidationConfig: {
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
},
};
});
beforeEach(async () => {
await redisClient.flushdb();
});
it('throws when redis is disconnected', async () => {
redisClient.disconnect();
const vc1 = new TestViewerContext(
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
);
await expect(
RedisTestEntity.creator(vc1).setField('name', 'blah').createAsync(),
).rejects.toThrow(EntityCacheAdapterTransientError);
});
});
@@ -0,0 +1,83 @@
import type { EntityCompanionDefinition, ViewerContext } from '@expo/entity';
import {
AlwaysAllowPrivacyPolicyRule,
BufferField,
DateField,
Entity,
EntityConfiguration,
EntityPrivacyPolicy,
StringField,
UUIDField,
} from '@expo/entity';
export type RedisTestEntityFields = {
id: string;
name: string;
dateField: Date | null;
bufferField: Buffer | null;
};
export class RedisTestEntity extends Entity<RedisTestEntityFields, 'id', ViewerContext> {
static defineCompanionDefinition(): EntityCompanionDefinition<
RedisTestEntityFields,
'id',
ViewerContext,
RedisTestEntity,
RedisTestEntityPrivacyPolicy
> {
return {
entityClass: RedisTestEntity,
entityConfiguration: redisTestEntityConfiguration,
privacyPolicyClass: RedisTestEntityPrivacyPolicy,
};
}
}
export class RedisTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
RedisTestEntityFields,
'id',
ViewerContext,
RedisTestEntity
> {
protected override readonly createRules = [
new AlwaysAllowPrivacyPolicyRule<RedisTestEntityFields, 'id', ViewerContext, RedisTestEntity>(),
];
protected override readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<RedisTestEntityFields, 'id', ViewerContext, RedisTestEntity>(),
];
protected override readonly updateRules = [
new AlwaysAllowPrivacyPolicyRule<RedisTestEntityFields, 'id', ViewerContext, RedisTestEntity>(),
];
protected override readonly deleteRules = [
new AlwaysAllowPrivacyPolicyRule<RedisTestEntityFields, 'id', ViewerContext, RedisTestEntity>(),
];
}
export const redisTestEntityConfiguration = new EntityConfiguration<RedisTestEntityFields, 'id'>({
idField: 'id',
tableName: 'redis_test_entities',
schema: {
id: new UUIDField({
columnName: 'id',
cache: true,
}),
name: new StringField({
columnName: 'name',
cache: true,
}),
dateField: new DateField({
columnName: 'date_field',
}),
bufferField: new BufferField({
columnName: 'buffer_field',
cache: true,
}),
},
databaseAdapterFlavor: 'postgres',
cacheAdapterFlavor: 'redis',
compositeFieldDefinitions: [
{ compositeField: ['id', 'name'], cache: true },
{ compositeField: ['id', 'dateField'], cache: true },
{ compositeField: ['id', 'bufferField'], cache: true },
],
});
@@ -0,0 +1,36 @@
import type { IEntityMetricsAdapter } from '@expo/entity';
import { EntityCompanionProvider, NoOpEntityMetricsAdapter } from '@expo/entity';
import { StubDatabaseAdapterProvider, StubQueryContextProvider } from '@expo/entity-testing-utils';
import type { GenericRedisCacheContext } from '../GenericRedisCacher.ts';
import { RedisCacheAdapterProvider } from '../RedisCacheAdapterProvider.ts';
// share across all in calls in test to simulate postgres
const adapterProvider = new StubDatabaseAdapterProvider();
const queryContextProvider = new StubQueryContextProvider();
export const createRedisIntegrationTestEntityCompanionProvider = (
genericRedisCacheContext: GenericRedisCacheContext,
metricsAdapter: IEntityMetricsAdapter = new NoOpEntityMetricsAdapter(),
): EntityCompanionProvider => {
return new EntityCompanionProvider(
metricsAdapter,
new Map([
[
'postgres',
{
adapterProvider,
queryContextProvider,
},
],
]),
new Map([
[
'redis',
{
cacheAdapterProvider: new RedisCacheAdapterProvider(genericRedisCacheContext),
},
],
]),
);
};
@@ -0,0 +1,428 @@
import {
CacheStatus,
CompositeFieldHolder,
CompositeFieldValueHolder,
EntityConfiguration,
SingleFieldHolder,
SingleFieldValueHolder,
StringField,
UUIDField,
} from '@expo/entity';
import { describe, expect, it } from '@jest/globals';
import type { Pipeline, Redis } from 'ioredis';
import { anything, instance, mock, verify, when } from 'ts-mockito';
import { GenericRedisCacher, RedisCacheInvalidationStrategy } from '../GenericRedisCacher.ts';
type BlahFields = {
id: string;
fieldA: string;
fieldB: string;
};
const entityConfiguration = new EntityConfiguration<BlahFields, 'id'>({
idField: 'id',
tableName: 'blah',
cacheKeyVersion: 2,
schema: {
id: new UUIDField({ columnName: 'id', cache: true }),
fieldA: new StringField({ columnName: 'field_a', cache: true }),
fieldB: new StringField({ columnName: 'field_b', cache: true }),
},
databaseAdapterFlavor: 'postgres',
cacheAdapterFlavor: 'redis',
});
describe(GenericRedisCacher, () => {
describe('loadManyAsync', () => {
it('returns appropriate cache results', async () => {
const redisResults = new Map();
const mockRedisClient = mock<Redis>();
// need to have one anything() for each element of spread, in this case 3
when(mockRedisClient.mget(anything(), anything(), anything())).thenCall(async (...keys) =>
keys.map((k) => redisResults.get(k) ?? null),
);
const genericCacher = new GenericRedisCacher(
{
redisClient: instance(mockRedisClient),
cacheKeyDelimiter: ':',
cacheKeyPrefix: 'hello-',
ttlSecondsPositive: 1,
ttlSecondsNegative: 2,
invalidationConfig: {
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
},
},
entityConfiguration,
);
const cacheKeyWat = genericCacher['makeCacheKeyForStorage'](
new SingleFieldHolder('id'),
new SingleFieldValueHolder('wat'),
);
const cacheKeyWho = genericCacher['makeCacheKeyForStorage'](
new SingleFieldHolder('id'),
new SingleFieldValueHolder('who'),
);
const cacheKeyWhy = genericCacher['makeCacheKeyForStorage'](
new SingleFieldHolder('id'),
new SingleFieldValueHolder('why'),
);
redisResults.set(cacheKeyWat, JSON.stringify({ id: 'wat' }));
redisResults.set(cacheKeyWho, '');
const results = await genericCacher.loadManyAsync([cacheKeyWat, cacheKeyWho, cacheKeyWhy]);
expect(results.get(cacheKeyWat)).toMatchObject({
status: CacheStatus.HIT,
item: { id: 'wat' },
});
expect(results.get(cacheKeyWho)).toMatchObject({ status: CacheStatus.NEGATIVE });
expect(results.get(cacheKeyWhy)).toMatchObject({ status: CacheStatus.MISS });
expect(results.size).toBe(3);
});
it('returns empty map when passed empty array of fieldValues', async () => {
const genericCacher = new GenericRedisCacher(
{
redisClient: instance(mock<Redis>()),
cacheKeyDelimiter: ':',
cacheKeyPrefix: 'hello-',
ttlSecondsPositive: 1,
ttlSecondsNegative: 2,
invalidationConfig: {
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
},
},
entityConfiguration,
);
const results = await genericCacher.loadManyAsync([]);
expect(results).toEqual(new Map());
});
});
describe('cacheManyAsync', () => {
it('correctly caches all objects', async () => {
const redisResults = new Map();
const mockPipeline = mock<Pipeline>();
when(mockPipeline.set(anything(), anything(), anything(), anything())).thenCall(
(key, value, code, ttl) => {
redisResults.set(key, { value, code, ttl });
return pipeline;
},
);
when(mockPipeline.exec()).thenResolve([]);
const pipeline = instance(mockPipeline);
const mockRedisClient = mock<Redis>();
when(mockRedisClient.multi()).thenReturn(pipeline);
const genericCacher = new GenericRedisCacher(
{
redisClient: instance(mockRedisClient),
cacheKeyDelimiter: ':',
cacheKeyPrefix: 'hello-',
ttlSecondsPositive: 1,
ttlSecondsNegative: 2,
invalidationConfig: {
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
},
},
entityConfiguration,
);
const cacheKey = genericCacher['makeCacheKeyForStorage'](
new SingleFieldHolder('id'),
new SingleFieldValueHolder('wat'),
);
const value = { id: 'wat', fieldA: 'a', fieldB: 'b' };
await genericCacher.cacheManyAsync(new Map([[cacheKey, value]]));
expect(redisResults.get(cacheKey)).toMatchObject({
value: JSON.stringify(value),
code: 'EX',
ttl: 1,
});
});
});
describe('cacheDBMissesAsync', () => {
it('correctly caches misses', async () => {
const redisResults = new Map();
const mockPipeline = mock<Pipeline>();
when(mockPipeline.set(anything(), anything(), anything(), anything())).thenCall(
(key, value, code, ttl) => {
redisResults.set(key, { value, code, ttl });
return pipeline;
},
);
when(mockPipeline.exec()).thenResolve([]);
const pipeline = instance(mockPipeline);
const mockRedisClient = mock<Redis>();
when(mockRedisClient.multi()).thenReturn(pipeline);
const genericCacher = new GenericRedisCacher(
{
redisClient: instance(mockRedisClient),
cacheKeyDelimiter: ':',
cacheKeyPrefix: 'hello-',
ttlSecondsPositive: 1,
ttlSecondsNegative: 2,
invalidationConfig: {
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
},
},
entityConfiguration,
);
const cacheKey = genericCacher['makeCacheKeyForStorage'](
new SingleFieldHolder('id'),
new SingleFieldValueHolder('wat'),
);
await genericCacher.cacheDBMissesAsync([cacheKey]);
expect(redisResults.get(cacheKey)).toMatchObject({
value: '',
code: 'EX',
ttl: 2,
});
});
});
describe('invalidateManyAsync', () => {
it('invalidates correctly with RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION', async () => {
const mockRedisClient = mock<Redis>();
when(mockRedisClient.del()).thenResolve(1);
const genericCacher = new GenericRedisCacher(
{
redisClient: instance(mockRedisClient),
cacheKeyDelimiter: ':',
cacheKeyPrefix: 'hello-',
ttlSecondsPositive: 1,
ttlSecondsNegative: 2,
invalidationConfig: {
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
},
},
entityConfiguration,
);
const cacheKeys = genericCacher['makeCacheKeysForInvalidation'](
new SingleFieldHolder('id'),
new SingleFieldValueHolder('wat'),
);
expect(cacheKeys).toHaveLength(1);
expect(cacheKeys[0]).toBe('hello-:single:blah:v4.2:id:wat');
await genericCacher.invalidateManyAsync(cacheKeys);
verify(mockRedisClient.del(...cacheKeys)).once();
});
it('does not collapse composite cache keys with delimiter-bearing parts', () => {
const genericCacher = new GenericRedisCacher(
{
redisClient: instance(mock<Redis>()),
cacheKeyPrefix: 'hello-',
cacheKeyDelimiter: ':',
ttlSecondsPositive: 1,
ttlSecondsNegative: 2,
invalidationConfig: {
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
},
},
entityConfiguration,
);
const compositeFieldHolder = new CompositeFieldHolder<BlahFields, 'id'>(['fieldA', 'fieldB']);
const cacheKey1 = genericCacher['makeCacheKeyForStorage'](
compositeFieldHolder,
new CompositeFieldValueHolder({ fieldA: 'a:b', fieldB: 'c' }),
);
const cacheKey2 = genericCacher['makeCacheKeyForStorage'](
compositeFieldHolder,
new CompositeFieldValueHolder({ fieldA: 'a', fieldB: 'b:c' }),
);
expect(cacheKey1).not.toEqual(cacheKey2);
});
it('invalidates correctly with RedisCacheInvalidationStrategy.SURROUNDING_CACHE_KEY_VERSIONS', async () => {
const mockRedisClient = mock<Redis>();
when(mockRedisClient.del()).thenResolve(1);
const genericCacher = new GenericRedisCacher(
{
redisClient: instance(mockRedisClient),
cacheKeyDelimiter: ':',
cacheKeyPrefix: 'hello-',
ttlSecondsPositive: 1,
ttlSecondsNegative: 2,
invalidationConfig: {
invalidationStrategy: RedisCacheInvalidationStrategy.SURROUNDING_CACHE_KEY_VERSIONS,
},
},
entityConfiguration,
);
const cacheKeys = genericCacher['makeCacheKeysForInvalidation'](
new SingleFieldHolder('id'),
new SingleFieldValueHolder('wat'),
);
expect(cacheKeys).toHaveLength(3);
expect(cacheKeys[0]).toBe('hello-:single:blah:v4.1:id:wat');
expect(cacheKeys[1]).toBe('hello-:single:blah:v4.2:id:wat');
expect(cacheKeys[2]).toBe('hello-:single:blah:v4.3:id:wat');
await genericCacher.invalidateManyAsync(cacheKeys);
verify(mockRedisClient.del(...cacheKeys)).once();
});
it('invalidates correctly with RedisCacheInvalidationStrategy.CUSTOM', async () => {
const mockRedisClient = mock<Redis>();
when(mockRedisClient.del()).thenResolve(1);
const genericCacher = new GenericRedisCacher(
{
redisClient: instance(mockRedisClient),
cacheKeyDelimiter: ':',
cacheKeyPrefix: 'hello-',
ttlSecondsPositive: 1,
ttlSecondsNegative: 2,
invalidationConfig: {
invalidationStrategy: RedisCacheInvalidationStrategy.CUSTOM,
cacheKeyVersionsToInvalidateFn(cacheKeyVersion) {
return [
cacheKeyVersion,
cacheKeyVersion + 1,
cacheKeyVersion + 2,
cacheKeyVersion + 3,
];
},
},
},
entityConfiguration,
);
const cacheKeys = genericCacher['makeCacheKeysForInvalidation'](
new SingleFieldHolder('id'),
new SingleFieldValueHolder('wat'),
);
expect(cacheKeys).toHaveLength(4);
expect(cacheKeys[0]).toBe('hello-:single:blah:v4.2:id:wat');
expect(cacheKeys[1]).toBe('hello-:single:blah:v4.3:id:wat');
expect(cacheKeys[2]).toBe('hello-:single:blah:v4.4:id:wat');
expect(cacheKeys[3]).toBe('hello-:single:blah:v4.5:id:wat');
await genericCacher.invalidateManyAsync(cacheKeys);
verify(mockRedisClient.del(...cacheKeys)).once();
});
it('returns when passed empty array of fieldValues', async () => {
const genericCacher = new GenericRedisCacher(
{
redisClient: instance(mock<Redis>()),
cacheKeyDelimiter: ':',
cacheKeyPrefix: 'hello-',
ttlSecondsPositive: 1,
ttlSecondsNegative: 2,
invalidationConfig: {
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
},
},
entityConfiguration,
);
await genericCacher.invalidateManyAsync([]);
});
it('throws when CUSTOM strategy returns an empty cacheKeyVersions list', () => {
const genericCacher = new GenericRedisCacher(
{
redisClient: instance(mock<Redis>()),
cacheKeyDelimiter: ':',
cacheKeyPrefix: 'hello-',
ttlSecondsPositive: 1,
ttlSecondsNegative: 2,
invalidationConfig: {
invalidationStrategy: RedisCacheInvalidationStrategy.CUSTOM,
cacheKeyVersionsToInvalidateFn: () => [],
},
},
entityConfiguration,
);
expect(() =>
genericCacher['makeCacheKeysForInvalidation'](
new SingleFieldHolder('id'),
new SingleFieldValueHolder('wat'),
),
).toThrow(/empty list/);
});
});
describe('GenericRedisCacheContext validation', () => {
const validContext = {
redisClient: instance(mock<Redis>()),
cacheKeyDelimiter: ':',
cacheKeyPrefix: 'hello-',
ttlSecondsPositive: 1,
ttlSecondsNegative: 2,
invalidationConfig: {
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION as const,
},
};
it('throws when cacheKeyDelimiter is empty', () => {
expect(
() =>
new GenericRedisCacher({ ...validContext, cacheKeyDelimiter: '' }, entityConfiguration),
).toThrow(/cacheKeyDelimiter must be a non-empty string/);
});
it('throws when cacheKeyDelimiter contains the escape character', () => {
expect(
() =>
new GenericRedisCacher({ ...validContext, cacheKeyDelimiter: '\\' }, entityConfiguration),
).toThrow(/must not contain the escape character/);
});
it('throws when ttlSecondsPositive is not a positive integer', () => {
expect(
() =>
new GenericRedisCacher({ ...validContext, ttlSecondsPositive: 0 }, entityConfiguration),
).toThrow(/ttlSecondsPositive must be a positive integer/);
expect(
() =>
new GenericRedisCacher({ ...validContext, ttlSecondsPositive: -1 }, entityConfiguration),
).toThrow(/ttlSecondsPositive must be a positive integer/);
expect(
() =>
new GenericRedisCacher({ ...validContext, ttlSecondsPositive: 1.5 }, entityConfiguration),
).toThrow(/ttlSecondsPositive must be a positive integer/);
});
it('throws when ttlSecondsNegative is not a positive integer', () => {
expect(
() =>
new GenericRedisCacher({ ...validContext, ttlSecondsNegative: 0 }, entityConfiguration),
).toThrow(/ttlSecondsNegative must be a positive integer/);
expect(
() =>
new GenericRedisCacher({ ...validContext, ttlSecondsNegative: -1 }, entityConfiguration),
).toThrow(/ttlSecondsNegative must be a positive integer/);
expect(
() =>
new GenericRedisCacher({ ...validContext, ttlSecondsNegative: 1.5 }, entityConfiguration),
).toThrow(/ttlSecondsNegative must be a positive integer/);
});
it('accepts a valid context', () => {
expect(() => new GenericRedisCacher(validContext, entityConfiguration)).not.toThrow();
});
});
});
@@ -1,167 +0,0 @@
import { CacheStatus, UUIDField, EntityConfiguration } from '@expo/entity';
import { Redis, Pipeline } from 'ioredis';
import { mock, when, instance, anything, verify } from 'ts-mockito';
import RedisCacheAdapter from '../RedisCacheAdapter';
type BlahFields = {
id: string;
};
const entityConfiguration = new EntityConfiguration<BlahFields>({
idField: 'id',
tableName: 'blah',
schema: {
id: new UUIDField({ columnName: 'id', cache: true }),
},
});
describe(RedisCacheAdapter, () => {
describe('loadManyAsync', () => {
it('returns appropriate cache results', async () => {
const redisResults = new Map();
const mockRedisClient = mock<Redis>();
// need to have one anything() for each element of spread, in this case 3
when(mockRedisClient.mget(anything(), anything(), anything())).thenCall(async (...keys) =>
keys.map((k) => redisResults.get(k) ?? null)
);
const cacheAdapter = new RedisCacheAdapter(entityConfiguration, {
redisClient: instance(mockRedisClient),
makeKeyFn: (...parts) => parts.join(':'),
cacheKeyVersion: 1,
cacheKeyPrefix: 'hello-',
ttlSecondsPositive: 1,
ttlSecondsNegative: 2,
});
redisResults.set(cacheAdapter['makeCacheKey']('id', 'wat'), JSON.stringify({ id: 'wat' }));
redisResults.set(cacheAdapter['makeCacheKey']('id', 'who'), '');
const results = await cacheAdapter.loadManyAsync('id', ['wat', 'who', 'why']);
expect(results.get('wat')).toMatchObject({ status: CacheStatus.HIT, item: { id: 'wat' } });
expect(results.get('who')).toMatchObject({ status: CacheStatus.NEGATIVE });
expect(results.get('why')).toMatchObject({ status: CacheStatus.MISS });
expect(results.size).toBe(3);
});
it('returns empty map when passed empty array of fieldValues', async () => {
const cacheAdapter = new RedisCacheAdapter(entityConfiguration, {
redisClient: instance(mock<Redis>()),
makeKeyFn: (...parts) => parts.join(':'),
cacheKeyVersion: 1,
cacheKeyPrefix: 'hello-',
ttlSecondsPositive: 1,
ttlSecondsNegative: 2,
});
const results = await cacheAdapter.loadManyAsync('id', []);
expect(results).toEqual(new Map());
});
});
describe('cacheManyAsync', () => {
it('correctly caches all objects', async () => {
const redisResults = new Map();
const mockPipeline = mock<Pipeline>();
when(mockPipeline.set(anything(), anything(), anything(), anything())).thenCall(
(key, value, code, ttl) => {
redisResults.set(key, { value, code, ttl });
return pipeline;
}
);
when(mockPipeline.exec()).thenResolve({} as any);
const pipeline = instance(mockPipeline);
const mockRedisClient = mock<Redis>();
when(mockRedisClient.multi()).thenReturn(pipeline);
const cacheAdapter = new RedisCacheAdapter(entityConfiguration, {
redisClient: instance(mockRedisClient),
makeKeyFn: (...parts) => parts.join(':'),
cacheKeyVersion: 1,
cacheKeyPrefix: 'hello-',
ttlSecondsPositive: 1,
ttlSecondsNegative: 2,
});
await cacheAdapter.cacheManyAsync('id', new Map([['wat', { id: 'wat' }]]));
const cacheKey = cacheAdapter['makeCacheKey']('id', 'wat');
expect(redisResults.get(cacheKey)).toMatchObject({
value: JSON.stringify({ id: 'wat' }),
code: 'EX',
ttl: 1,
});
});
});
describe('cacheDBMissesAsync', () => {
it('correctly caches misses', async () => {
const redisResults = new Map();
const mockPipeline = mock<Pipeline>();
when(mockPipeline.set(anything(), anything(), anything(), anything())).thenCall(
(key, value, code, ttl) => {
redisResults.set(key, { value, code, ttl });
return pipeline;
}
);
when(mockPipeline.exec()).thenResolve({} as any);
const pipeline = instance(mockPipeline);
const mockRedisClient = mock<Redis>();
when(mockRedisClient.multi()).thenReturn(pipeline);
const cacheAdapter = new RedisCacheAdapter(entityConfiguration, {
redisClient: instance(mockRedisClient),
makeKeyFn: (...parts) => parts.join(':'),
cacheKeyVersion: 1,
cacheKeyPrefix: 'hello-',
ttlSecondsPositive: 1,
ttlSecondsNegative: 2,
});
await cacheAdapter.cacheDBMissesAsync('id', ['wat']);
const cacheKey = cacheAdapter['makeCacheKey']('id', 'wat');
expect(redisResults.get(cacheKey)).toMatchObject({
value: '',
code: 'EX',
ttl: 2,
});
});
});
describe('invalidateManyAsync', () => {
it('invalidates correctly', async () => {
const mockRedisClient = mock<Redis>();
when(mockRedisClient.del()).thenResolve(1);
const cacheAdapter = new RedisCacheAdapter(entityConfiguration, {
redisClient: instance(mockRedisClient),
makeKeyFn: (...parts) => parts.join(':'),
cacheKeyVersion: 1,
cacheKeyPrefix: 'hello-',
ttlSecondsPositive: 1,
ttlSecondsNegative: 2,
});
await cacheAdapter.invalidateManyAsync('id', ['wat']);
const cacheKey = cacheAdapter['makeCacheKey']('id', 'wat');
verify(mockRedisClient.del(cacheKey)).once();
});
it('returns when passed empty array of fieldValues', async () => {
const cacheAdapter = new RedisCacheAdapter(entityConfiguration, {
redisClient: instance(mock<Redis>()),
makeKeyFn: (...parts) => parts.join(':'),
cacheKeyVersion: 1,
cacheKeyPrefix: 'hello-',
ttlSecondsPositive: 1,
ttlSecondsNegative: 2,
});
await cacheAdapter.invalidateManyAsync('id', []);
});
});
});
@@ -0,0 +1,40 @@
import { EntityCacheAdapterTransientError } from '@expo/entity';
import { describe, expect, it } from '@jest/globals';
import { wrapNativeRedisCallAsync } from '../wrapNativeRedisCallAsync.ts';
describe(wrapNativeRedisCallAsync, () => {
it('rethrows literals', async () => {
const throwingFn = async (): Promise<void> => {
// eslint-disable-next-line no-throw-literal,@typescript-eslint/only-throw-error
throw 'hello';
};
let capturedThrownThing: any;
try {
await wrapNativeRedisCallAsync(throwingFn);
} catch (e) {
capturedThrownThing = e;
}
expect(capturedThrownThing).not.toBeInstanceOf(Error);
expect(capturedThrownThing).toEqual('hello');
});
it('wraps errors with stacks', async () => {
const throwingFn = async (): Promise<void> => {
const e = new Error('hello');
e.stack = 'world';
throw e;
};
let capturedThrownThing: any;
try {
await wrapNativeRedisCallAsync(throwingFn);
} catch (e) {
capturedThrownThing = e;
}
expect(capturedThrownThing).toBeInstanceOf(EntityCacheAdapterTransientError);
expect(capturedThrownThing.message).toEqual('hello');
expect(capturedThrownThing.stack).toEqual('world');
});
});
@@ -0,0 +1,17 @@
import { EntityCacheAdapterTransientError } from '@expo/entity';
export async function wrapNativeRedisCallAsync<T>(fn: () => Promise<T>): Promise<T> {
try {
return await fn();
} catch (e) {
if (!(e instanceof Error)) {
throw e;
}
const error = new EntityCacheAdapterTransientError(e.message, e);
if (e.stack) {
error.stack = e.stack;
}
throw error;
}
}
@@ -1,8 +1,11 @@
/* eslint-disable tsdoc/syntax */
/**
* Automatically generated by barrelsby.
* @packageDocumentation
* @module @expo/entity-cache-adapter-redis
*/
export { default as RedisCacheAdapter } from './RedisCacheAdapter';
export * from './RedisCacheAdapter';
export { default as RedisCacheAdapterProvider } from './RedisCacheAdapterProvider';
export * from './RedisCommon';
export * from './GenericRedisCacher.ts';
export * from './RedisCacheAdapterProvider.ts';
export * from './RedisCommon.ts';
export * from './errors/wrapNativeRedisCallAsync.ts';
export * from './utils/getSurroundingCacheKeyVersionsForInvalidation.ts';
@@ -1,96 +0,0 @@
import {
AlwaysAllowPrivacyPolicyRule,
EntityPrivacyPolicy,
ViewerContext,
UUIDField,
DateField,
StringField,
EntityConfiguration,
DatabaseAdapterFlavor,
CacheAdapterFlavor,
EntityCompanionDefinition,
Entity,
} from '@expo/entity';
export type RedisTestEntityFields = {
id: string;
name: string;
dateField: Date | null;
};
export default class RedisTestEntity extends Entity<RedisTestEntityFields, string, ViewerContext> {
static getCompanionDefinition(): EntityCompanionDefinition<
RedisTestEntityFields,
string,
ViewerContext,
RedisTestEntity,
RedisTestEntityPrivacyPolicy
> {
return redisTestEntityCompanionDefinition;
}
}
class RedisTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
RedisTestEntityFields,
string,
ViewerContext,
RedisTestEntity
> {
protected readonly createRules = [
new AlwaysAllowPrivacyPolicyRule<
RedisTestEntityFields,
string,
ViewerContext,
RedisTestEntity
>(),
];
protected readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<
RedisTestEntityFields,
string,
ViewerContext,
RedisTestEntity
>(),
];
protected readonly updateRules = [
new AlwaysAllowPrivacyPolicyRule<
RedisTestEntityFields,
string,
ViewerContext,
RedisTestEntity
>(),
];
protected readonly deleteRules = [
new AlwaysAllowPrivacyPolicyRule<
RedisTestEntityFields,
string,
ViewerContext,
RedisTestEntity
>(),
];
}
export const redisTestEntityConfiguration = new EntityConfiguration<RedisTestEntityFields>({
idField: 'id',
tableName: 'redis_test_entities',
schema: {
id: new UUIDField({
columnName: 'id',
cache: true,
}),
name: new StringField({
columnName: 'name',
}),
dateField: new DateField({
columnName: 'date_field',
}),
},
});
const redisTestEntityCompanionDefinition = {
entityClass: RedisTestEntity,
entityConfiguration: redisTestEntityConfiguration,
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
privacyPolicyClass: RedisTestEntityPrivacyPolicy,
};
@@ -1,32 +0,0 @@
import {
NoOpEntityMetricsAdapter,
IEntityMetricsAdapter,
EntityCompanionProvider,
CacheAdapterFlavor,
DatabaseAdapterFlavor,
StubQueryContextProvider,
StubDatabaseAdapterProvider,
} from '@expo/entity';
import { RedisCacheAdapterContext } from '../RedisCacheAdapter';
import RedisCacheAdapterProvider from '../RedisCacheAdapterProvider';
export const createRedisIntegrationTestEntityCompanionProvider = (
redisCacheAdapterContext: RedisCacheAdapterContext,
metricsAdapter: IEntityMetricsAdapter = new NoOpEntityMetricsAdapter()
): EntityCompanionProvider => {
return new EntityCompanionProvider(
metricsAdapter,
{
[DatabaseAdapterFlavor.POSTGRES]: {
adapterProvider: new StubDatabaseAdapterProvider(),
queryContextProvider: StubQueryContextProvider,
},
},
{
[CacheAdapterFlavor.REDIS]: {
cacheAdapterProvider: new RedisCacheAdapterProvider(redisCacheAdapterContext),
},
}
);
};
@@ -0,0 +1,11 @@
import { describe, expect, it } from '@jest/globals';
import { getSurroundingCacheKeyVersionsForInvalidation } from '../getSurroundingCacheKeyVersionsForInvalidation.ts';
describe(getSurroundingCacheKeyVersionsForInvalidation, () => {
it('returns the correct cache key versions to invalidate', () => {
expect(getSurroundingCacheKeyVersionsForInvalidation(0)).toEqual([0, 1]);
expect(getSurroundingCacheKeyVersionsForInvalidation(1)).toEqual([0, 1, 2]);
expect(getSurroundingCacheKeyVersionsForInvalidation(2)).toEqual([1, 2, 3]);
});
});
@@ -0,0 +1,9 @@
export function getSurroundingCacheKeyVersionsForInvalidation(
cacheKeyVersion: number,
): readonly number[] {
return [
...(cacheKeyVersion === 0 ? [] : [cacheKeyVersion - 1]),
cacheKeyVersion,
cacheKeyVersion + 1,
];
}
@@ -1,7 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "build",
},
"include": ["src"]
"extends": "../../tsconfig.base.json",
"include": ["src/**/*"],
"references": [{ "path": "../entity" }, { "path": "../entity-testing-utils" }]
}
+133
View File
@@ -0,0 +1,133 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
**Note:** Version bump only for package @expo/entity-codemod
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
**Note:** Version bump only for package @expo/entity-codemod
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
**Note:** Version bump only for package @expo/entity-codemod
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
**Note:** Version bump only for package @expo/entity-codemod
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
**Note:** Version bump only for package @expo/entity-codemod
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
**Note:** Version bump only for package @expo/entity-codemod
# [0.61.0](https://github.com/expo/entity/compare/v0.60.0...v0.61.0) (2026-03-12)
**Note:** Version bump only for package @expo/entity-codemod
# [0.60.0](https://github.com/expo/entity/compare/v0.59.0...v0.60.0) (2026-03-06)
**Note:** Version bump only for package @expo/entity-codemod
# [0.59.0](https://github.com/expo/entity/compare/v0.58.0...v0.59.0) (2026-03-03)
**Note:** Version bump only for package @expo/entity-codemod
# [0.58.0](https://github.com/expo/entity/compare/v0.57.0...v0.58.0) (2026-03-02)
**Note:** Version bump only for package @expo/entity-codemod
# [0.57.0](https://github.com/expo/entity/compare/v0.56.0...v0.57.0) (2026-02-28)
**Note:** Version bump only for package @expo/entity-codemod
# [0.56.0](https://github.com/expo/entity/compare/v0.55.0...v0.56.0) (2026-02-28)
### Bug Fixes
- refactor install method to instead be free functions ([#441](https://github.com/expo/entity/issues/441)) ([51d632d](https://github.com/expo/entity/commit/51d632da1619dc039d5c41ce6533f63254ec4337)), closes [#410](https://github.com/expo/entity/issues/410)
- revert change to codemod now that Entity.knexLoader is supported again via subclass ([#443](https://github.com/expo/entity/issues/443)) ([ed6b2e0](https://github.com/expo/entity/commit/ed6b2e05539c77ee2abd2c6da74e2a451967d06e)), closes [#441](https://github.com/expo/entity/issues/441)
### Features
- add codemod for upcoming v0.55.0-v0.56.0 ([#413](https://github.com/expo/entity/issues/413)) ([aba0ce5](https://github.com/expo/entity/commit/aba0ce5b5f38cb7b01956cde14a4714e2a03abb4)), closes [#407](https://github.com/expo/entity/issues/407) [-#410](https://github.com/-/issues/410)
# [0.55.0](https://github.com/expo/entity/compare/v0.54.0...v0.55.0) (2026-01-27)
**Note:** Version bump only for package @expo/entity-codemod
# [0.54.0](https://github.com/expo/entity/compare/v0.53.0...v0.54.0) (2026-01-21)
### Bug Fixes
- **deps:** pin dependencies ([#328](https://github.com/expo/entity/issues/328)) ([5679f27](https://github.com/expo/entity/commit/5679f27209f1515ca5e626d858c4aa054b414625))
# [0.53.0](https://github.com/expo/entity/compare/v0.52.0...v0.53.0) (2025-12-19)
**Note:** Version bump only for package @expo/entity-codemod
# [0.52.0](https://github.com/expo/entity/compare/v0.51.0...v0.52.0) (2025-12-19)
**Note:** Version bump only for package @expo/entity-codemod
# [0.51.0](https://github.com/expo/entity/compare/v0.50.0...v0.51.0) (2025-11-21)
**Note:** Version bump only for package @expo/entity-codemod
# [0.50.0](https://github.com/expo/entity/compare/v0.49.0...v0.50.0) (2025-10-07)
**Note:** Version bump only for package @expo/entity-codemod
# [0.49.0](https://github.com/expo/entity/compare/v0.48.0...v0.49.0) (2025-10-07)
**Note:** Version bump only for package @expo/entity-codemod
# [0.48.0](https://github.com/expo/entity/compare/v0.47.0...v0.48.0) (2025-09-19)
**Note:** Version bump only for package @expo/entity-codemod
# [0.47.0](https://github.com/expo/entity/compare/v0.46.0...v0.47.0) (2025-09-19)
**Note:** Version bump only for package @expo/entity-codemod
# [0.46.0](https://github.com/expo/entity/compare/v0.45.0...v0.46.0) (2025-06-26)
**Note:** Version bump only for package @expo/entity-codemod
# [0.45.0](https://github.com/expo/entity/compare/v0.44.0...v0.45.0) (2025-06-06)
**Note:** Version bump only for package @expo/entity-codemod
# [0.44.0](https://github.com/expo/entity/compare/v0.43.0...v0.44.0) (2025-05-29)
**Note:** Version bump only for package @expo/entity-codemod
# [0.43.0](https://github.com/expo/entity/compare/v0.42.0...v0.43.0) (2025-04-10)
**Note:** Version bump only for package @expo/entity-codemod
# [0.42.0](https://github.com/expo/entity/compare/v0.41.0...v0.42.0) (2025-04-10)
**Note:** Version bump only for package @expo/entity-codemod
# [0.41.0](https://github.com/expo/entity/compare/v0.40.0...v0.41.0) (2025-03-07)
### Features
- Create codemod for v0.40.0 -> v0.41.0 ([#260](https://github.com/expo/entity/issues/260)) ([4a2b61f](https://github.com/expo/entity/commit/4a2b61f2bd8edfc19109ef4c8ac780eaf0f7795c))
# [0.40.0](https://github.com/expo/entity/compare/v0.39.0...v0.40.0) (2025-02-27)
### Features
- Create codemod for v0.39.0 -> v0.40.0 ([#259](https://github.com/expo/entity/issues/259)) ([d63ddd6](https://github.com/expo/entity/commit/d63ddd65997a441a914a71b158a111339970aeab))
### Reverts
- Revert "v0.40.0" ([fa78ce8](https://github.com/expo/entity/commit/fa78ce84e2f2eb8199e36fd51779fc68905001c0))
+27
View File
@@ -0,0 +1,27 @@
# @expo/entity-codemod
A package containing jscodeshift codemods for @expo/entity upgrades.
Codemods are transformations that run on your codebase programmatically. This allows for a large amount of changes to be applied without having to manually go through every file.
## Usage
These should be used via the [jscodeshift CLI](https://github.com/facebook/jscodeshift?tab=readme-ov-file#usage-cli).
For example:
```
yarn add -D @expo/entity-codemod
```
Then, in package.json scripts:
```json
"jscodeshift": "jscodeshift --extensions=ts --parser=ts"
```
And finally:
```sh
yarn jscodeshift src -t node_modules/@expo/entity-codemod/build/v0.39.0-v0.40.0.js
```
+37
View File
@@ -0,0 +1,37 @@
{
"name": "@expo/entity-codemod",
"version": "0.69.0",
"description": "jscodeshift codemods for @expo/entity upgrades",
"keywords": [
"entity"
],
"license": "MIT",
"author": "Expo",
"files": [
"build",
"!*.tsbuildinfo",
"!__*",
"src"
],
"type": "module",
"main": "build/src/index.js",
"types": "build/src/index.d.ts",
"scripts": {
"build": "tsc --build",
"prepack": "rm -rf build && yarn build",
"clean": "yarn build --clean",
"lint": "yarn run --top-level oxlint --type-aware src",
"lint-fix": "yarn lint --fix",
"test": "yarn test:all --rootDir $(pwd)"
},
"devDependencies": {
"@jest/globals": "30.5.1",
"@types/jscodeshift": "17.3.0",
"@types/node": "24.13.5",
"jscodeshift": "17.4.0",
"typescript": "6.0.3"
},
"engines": {
"node": ">=18"
}
}
+2
View File
@@ -0,0 +1,2 @@
// oxlint-disable unicorn/no-empty-file
// placeholder file to fix tsc/typedoc
@@ -0,0 +1,79 @@
/* eslint-disable */
import { ViewerContext } from '@expo/entity';
async function testWithAuthorizationResults(viewerContext: ViewerContext): Promise<void> {
// creator
const entityResult = await TestEntity.creator(viewerContext).setField('wat', 2).createAsync();
// updater
const updatedEntityResult = await TestEntity.updater(entityResult.enforceValue()).setField('wat', 3).updateAsync();
// deleter
await TestEntity.deleteAsync(updatedEntityResult.enforceValue());
}
async function testEnforcing(viewerContext: ViewerContext): Promise<void> {
// creator
const entity = await TestEntity.creator(viewerContext).setField('wat', 2).enforceCreateAsync();
const entity2 = await TestEntity.creator(viewerContext).setField('wat', 2).setField('who', 3).enforceCreateAsync();
const entity3 = await TestEntity.creator(viewerContext)
.setField('wat', 2)
.setField('who', 3)
.setField('who', 4)
.setField('who', 5)
.setField('who', 6)
.setField('who', 7)
.setField('who', 8)
.setField('who', 9)
.enforceCreateAsync();
// updater
const updatedEntity = await TestEntity.updater(entity).setField('wat', 3).enforceUpdateAsync();
const updatedEntity2 = await TestEntity.updater(entity2).setField('wat', 3).setField('who', 4).enforceUpdateAsync();
const updatedEntity3 = await TestEntity.updater(entity3)
.setField('wat', 3)
.setField('who', 4)
.setField('who', 5)
.setField('who', 6)
.setField('who', 7)
.setField('who', 8)
.setField('who', 9)
.enforceUpdateAsync();
// deleter
await TestEntity.enforceDeleteAsync(entity);
}
async function testNoSetField(viewerContext: ViewerContext): Promise<void> {
// creator
const entity = await TestEntity.creator(viewerContext).enforceCreateAsync();
// updater
const updatedEntity = await TestEntity.updater(entity).enforceUpdateAsync();
}
async function testEnforcingWithQueryContext(viewerContext: ViewerContext): Promise<void> {
// creator
const entity = await TestEntity.creator(viewerContext, queryContext).setField('wat', 2).enforceCreateAsync();
// updater
const updatedEntity = await TestEntity.updater(entity, queryContext).setField('wat', 3).enforceUpdateAsync();
// deleter
await TestEntity.enforceDeleteAsync(entity, queryContext);
}
async function testAssociationLoader(): Promise<void> {
await this.associationLoader().loadAssociatedEntityAsync(
'another_id',
AnotherEntity,
queryContext
);
await entity.associationLoader().loadAssociatedEntityAsync(
'another_id',
AnotherEntity,
queryContext
)
}
@@ -0,0 +1,79 @@
/* eslint-disable */
import { ViewerContext } from '@expo/entity';
async function testWithAuthorizationResults(viewerContext: ViewerContext): Promise<void> {
// creator
const entityResult = await TestEntity.creator(viewerContext).withAuthorizationResults().setField('wat', 2).createAsync();
// updater
const updatedEntityResult = await TestEntity.updater(entityResult.enforceValue()).withAuthorizationResults().setField('wat', 3).updateAsync();
// deleter
await TestEntity.deleter(updatedEntityResult.enforceValue()).withAuthorizationResults().deleteAsync();
}
async function testEnforcing(viewerContext: ViewerContext): Promise<void> {
// creator
const entity = await TestEntity.creator(viewerContext).enforcing().setField('wat', 2).createAsync();
const entity2 = await TestEntity.creator(viewerContext).enforcing().setField('wat', 2).setField('who', 3).createAsync();
const entity3 = await TestEntity.creator(viewerContext).enforcing()
.setField('wat', 2)
.setField('who', 3)
.setField('who', 4)
.setField('who', 5)
.setField('who', 6)
.setField('who', 7)
.setField('who', 8)
.setField('who', 9)
.createAsync();
// updater
const updatedEntity = await TestEntity.updater(entity).enforcing().setField('wat', 3).updateAsync();
const updatedEntity2 = await TestEntity.updater(entity2).enforcing().setField('wat', 3).setField('who', 4).updateAsync();
const updatedEntity3 = await TestEntity.updater(entity3).enforcing()
.setField('wat', 3)
.setField('who', 4)
.setField('who', 5)
.setField('who', 6)
.setField('who', 7)
.setField('who', 8)
.setField('who', 9)
.updateAsync();
// deleter
await TestEntity.deleter(entity).enforcing().deleteAsync();
}
async function testNoSetField(viewerContext: ViewerContext): Promise<void> {
// creator
const entity = await TestEntity.creator(viewerContext).enforcing().createAsync();
// updater
const updatedEntity = await TestEntity.updater(entity).enforcing().updateAsync();
}
async function testEnforcingWithQueryContext(viewerContext: ViewerContext): Promise<void> {
// creator
const entity = await TestEntity.creator(viewerContext, queryContext).enforcing().setField('wat', 2).createAsync();
// updater
const updatedEntity = await TestEntity.updater(entity, queryContext).enforcing().setField('wat', 3).updateAsync();
// deleter
await TestEntity.deleter(entity, queryContext).enforcing().deleteAsync();
}
async function testAssociationLoader(): Promise<void> {
await this.associationLoader().withAuthorizationResults().loadAssociatedEntityAsync(
'another_id',
AnotherEntity,
queryContext
);
await entity.associationLoader().withAuthorizationResults().loadAssociatedEntityAsync(
'another_id',
AnotherEntity,
queryContext
)
}
@@ -0,0 +1,10 @@
/* eslint-disable */
async function testAsync(viewerContext: ViewerContext): Promise<void> {
const testTrigger = await TestTriggerEntity.createAsync(actorViewerContext, undefined, {
appId,
});
await TestTriggerEntity.enforceDeleteAsync(testTrigger);
return testTrigger;
};
@@ -0,0 +1,10 @@
/* eslint-disable */
async function testAsync(viewerContext: ViewerContext): Promise<void> {
const testTrigger = await TestTriggerEntity.createAsync(actorViewerContext, undefined, {
appId,
});
await TestTriggerEntity.deleter(testTrigger).enforcing().deleteAsync();
return testTrigger;
};
@@ -0,0 +1,5 @@
/* eslint-disable */
async function anotherCase(updater: any): Promise<any> {
return await updater.enforceUpdateAsync();
}
@@ -0,0 +1,5 @@
/* eslint-disable */
async function anotherCase(updater: any): Promise<any> {
return await updater.enforceUpdateAsync();
}
@@ -0,0 +1,100 @@
/* eslint-disable */
import { ViewerContext } from '@expo/entity';
async function testWithAuthorizationResults(viewerContext: ViewerContext): Promise<void> {
// creator
const entityResult = await TestEntity.creator(viewerContext).withAuthorizationResults().setField('wat', 2).createAsync();
// updater
const updatedEntityResult = await TestEntity.updater(entityResult.enforceValue()).withAuthorizationResults().setField('wat', 3).updateAsync();
// loader
const loadedEntityResult = await TestEntity.loader(viewerContext).withAuthorizationResults().loadByIDAsync('test');
// deleter
await TestEntity.deleter(updatedEntityResult.enforceValue()).withAuthorizationResults().deleteAsync();
}
async function testEnforcing(viewerContext: ViewerContext): Promise<void> {
// creator
const entity = await TestEntity.creator(viewerContext).enforcing().setField('wat', 2).createAsync();
const entity2 = await TestEntity.creator(viewerContext).enforcing().setField('wat', 2).setField('who', 3).createAsync();
const entity3 = await TestEntity.creator(viewerContext).enforcing()
.setField('wat', 2)
.setField('who', 3)
.setField('who', 4)
.setField('who', 5)
.setField('who', 6)
.setField('who', 7)
.setField('who', 8)
.setField('who', 9)
.createAsync();
// updater
const updatedEntity = await TestEntity.updater(entity).enforcing().setField('wat', 3).updateAsync();
const updatedEntity2 = await TestEntity.updater(entity2).enforcing().setField('wat', 3).setField('who', 4).updateAsync();
const updatedEntity3 = await TestEntity.updater(entity3).enforcing()
.setField('wat', 3)
.setField('who', 4)
.setField('who', 5)
.setField('who', 6)
.setField('who', 7)
.setField('who', 8)
.setField('who', 9)
.updateAsync();
// loader
const loadedEntity = await TestEntity.loader(viewerContext).enforcing().loadByIDAsync('test');
// deleter
await TestEntity.deleter(entity).enforcing().deleteAsync();
}
async function testNoSetField(viewerContext: ViewerContext): Promise<void> {
// creator
const entity = await TestEntity.creator(viewerContext).enforcing().createAsync();
// updater
const updatedEntity = await TestEntity.updater(entity).enforcing().updateAsync();
}
async function testEnforcingWithQueryContext(viewerContext: ViewerContext): Promise<void> {
// creator
const entity = await TestEntity.creator(viewerContext, queryContext).enforcing().setField('wat', 2).createAsync();
// updater
const updatedEntity = await TestEntity.updater(entity, queryContext).enforcing().setField('wat', 3).updateAsync();
// loader
const loadedEntity = await TestEntity.loader(viewerContext, queryContext).enforcing().loadByIDAsync('test');
// deleter
await TestEntity.deleter(entity, queryContext).enforcing().deleteAsync();
}
async function testAssociationLoader(): Promise<void> {
await this.associationLoader().withAuthorizationResults().loadAssociatedEntityAsync(
'another_id',
AnotherEntity,
queryContext
);
await entity.associationLoader().withAuthorizationResults().loadAssociatedEntityAsync(
'another_id',
AnotherEntity,
queryContext
)
await this.associationLoader().enforcing().loadAssociatedEntityAsync(
'another_id',
AnotherEntity,
queryContext
);
await entity.associationLoader().enforcing().loadAssociatedEntityAsync(
'another_id',
AnotherEntity,
queryContext
)
}
@@ -0,0 +1,100 @@
/* eslint-disable */
import { ViewerContext } from '@expo/entity';
async function testWithAuthorizationResults(viewerContext: ViewerContext): Promise<void> {
// creator
const entityResult = await TestEntity.creatorWithAuthorizationResults(viewerContext).setField('wat', 2).createAsync();
// updater
const updatedEntityResult = await TestEntity.updaterWithAuthorizationResults(entityResult.enforceValue()).setField('wat', 3).updateAsync();
// loader
const loadedEntityResult = await TestEntity.loaderWithAuthorizationResults(viewerContext).loadByIDAsync('test');
// deleter
await TestEntity.deleterWithAuthorizationResults(updatedEntityResult.enforceValue()).deleteAsync();
}
async function testEnforcing(viewerContext: ViewerContext): Promise<void> {
// creator
const entity = await TestEntity.creator(viewerContext).setField('wat', 2).createAsync();
const entity2 = await TestEntity.creator(viewerContext).setField('wat', 2).setField('who', 3).createAsync();
const entity3 = await TestEntity.creator(viewerContext)
.setField('wat', 2)
.setField('who', 3)
.setField('who', 4)
.setField('who', 5)
.setField('who', 6)
.setField('who', 7)
.setField('who', 8)
.setField('who', 9)
.createAsync();
// updater
const updatedEntity = await TestEntity.updater(entity).setField('wat', 3).updateAsync();
const updatedEntity2 = await TestEntity.updater(entity2).setField('wat', 3).setField('who', 4).updateAsync();
const updatedEntity3 = await TestEntity.updater(entity3)
.setField('wat', 3)
.setField('who', 4)
.setField('who', 5)
.setField('who', 6)
.setField('who', 7)
.setField('who', 8)
.setField('who', 9)
.updateAsync();
// loader
const loadedEntity = await TestEntity.loader(viewerContext).loadByIDAsync('test');
// deleter
await TestEntity.deleter(entity).deleteAsync();
}
async function testNoSetField(viewerContext: ViewerContext): Promise<void> {
// creator
const entity = await TestEntity.creator(viewerContext).createAsync();
// updater
const updatedEntity = await TestEntity.updater(entity).updateAsync();
}
async function testEnforcingWithQueryContext(viewerContext: ViewerContext): Promise<void> {
// creator
const entity = await TestEntity.creator(viewerContext, queryContext).setField('wat', 2).createAsync();
// updater
const updatedEntity = await TestEntity.updater(entity, queryContext).setField('wat', 3).updateAsync();
// loader
const loadedEntity = await TestEntity.loader(viewerContext, queryContext).loadByIDAsync('test');
// deleter
await TestEntity.deleter(entity, queryContext).deleteAsync();
}
async function testAssociationLoader(): Promise<void> {
await this.associationLoaderWithAuthorizationResults().loadAssociatedEntityAsync(
'another_id',
AnotherEntity,
queryContext
);
await entity.associationLoaderWithAuthorizationResults().loadAssociatedEntityAsync(
'another_id',
AnotherEntity,
queryContext
)
await this.associationLoader().loadAssociatedEntityAsync(
'another_id',
AnotherEntity,
queryContext
);
await entity.associationLoader().loadAssociatedEntityAsync(
'another_id',
AnotherEntity,
queryContext
)
}
@@ -0,0 +1,37 @@
import { ViewerContext } from '@expo/entity';
import { PostEntity } from './entities/PostEntity.ts';
import { UserEntity } from './entities/UserEntity.ts';
async function loadUser(viewerContext: ViewerContext) {
// Basic loader calls - only transformed when using knex-specific methods
const userLoader = UserEntity.loader(viewerContext);
const postLoader = PostEntity.loader(viewerContext);
// These use knex-specific methods, so they should be transformed
const posts = await postLoader.loadManyByFieldEqualityConjunctionAsync([
{ fieldName: 'status', fieldValue: 'published' }
]);
const firstPost = await postLoader.loadFirstByFieldEqualityConjunctionAsync([
{ fieldName: 'id', fieldValue: '123' }
]);
// Loader with authorization results - only transformed when using knex methods
const userLoaderWithAuth = UserEntity.loaderWithAuthorizationResults(viewerContext);
const rawResults = await userLoaderWithAuth.loadManyByRawWhereClauseAsync('age > ?', [18]);
// Loader that doesn't use knex methods - should NOT be transformed
const standardLoader = PostEntity.loader(viewerContext);
const post = await standardLoader.loadByIDAsync('456');
// Should not transform instance methods or other properties
const user = await userLoader.loadByIDAsync('123');
const userLoadMethod = user.loader; // This should not be transformed
// Should not transform lowercase object methods
const customLoader = {
loader: (ctx: any) => ctx,
};
customLoader.loader(viewerContext);
return user;
}
@@ -0,0 +1,37 @@
import { ViewerContext } from '@expo/entity';
import { PostEntity } from './entities/PostEntity.ts';
import { UserEntity } from './entities/UserEntity.ts';
async function loadUser(viewerContext: ViewerContext) {
// Basic loader calls - only transformed when using knex-specific methods
const userLoader = UserEntity.loader(viewerContext);
const postLoader = PostEntity.knexLoader(viewerContext);
// These use knex-specific methods, so they should be transformed
const posts = await postLoader.loadManyByFieldEqualityConjunctionAsync([
{ fieldName: 'status', fieldValue: 'published' }
]);
const firstPost = await postLoader.loadFirstByFieldEqualityConjunctionAsync([
{ fieldName: 'id', fieldValue: '123' }
]);
// Loader with authorization results - only transformed when using knex methods
const userLoaderWithAuth = UserEntity.knexLoaderWithAuthorizationResults(viewerContext);
const rawResults = await userLoaderWithAuth.loadManyByRawWhereClauseAsync('age > ?', [18]);
// Loader that doesn't use knex methods - should NOT be transformed
const standardLoader = PostEntity.loader(viewerContext);
const post = await standardLoader.loadByIDAsync('456');
// Should not transform instance methods or other properties
const user = await userLoader.loadByIDAsync('123');
const userLoadMethod = user.loader; // This should not be transformed
// Should not transform lowercase object methods
const customLoader = {
loader: (ctx: any) => ctx,
};
customLoader.loader(viewerContext);
return user;
}
@@ -0,0 +1,33 @@
import { ViewerContext } from '@expo/entity';
import { CommentEntity } from './entities/CommentEntity.ts';
// Chained calls
const loadComments = async (viewerContext: ViewerContext) => {
// Direct chaining with knex-specific method
const comments = await CommentEntity.loader(viewerContext)
.loadManyByFieldEqualityConjunctionAsync([
{ fieldName: 'postId', fieldValue: '123' }
]);
// Direct chaining with regular method - should NOT be transformed
const singleComment = await CommentEntity
.loader(viewerContext)
.loadByIDAsync('456');
// With authorization results and knex method
const commentsWithAuth = await CommentEntity
.loaderWithAuthorizationResults(viewerContext)
.loadManyByRawWhereClauseAsync('postId = ?', ['456']);
// Edge cases - these should NOT be transformed
const anotherEntity = {
loader: (ctx: any) => ctx, // This is not an entity class
};
anotherEntity.loader(viewerContext); // Should NOT be transformed (lowercase object)
// Complex chaining with regular method - should NOT be transformed
return CommentEntity
.loader(viewerContext)
.withAuthenticationResults()
.loadByIDAsync('789');
};
@@ -0,0 +1,33 @@
import { ViewerContext } from '@expo/entity';
import { CommentEntity } from './entities/CommentEntity.ts';
// Chained calls
const loadComments = async (viewerContext: ViewerContext) => {
// Direct chaining with knex-specific method
const comments = await CommentEntity.knexLoader(viewerContext)
.loadManyByFieldEqualityConjunctionAsync([
{ fieldName: 'postId', fieldValue: '123' }
]);
// Direct chaining with regular method - should NOT be transformed
const singleComment = await CommentEntity
.loader(viewerContext)
.loadByIDAsync('456');
// With authorization results and knex method
const commentsWithAuth = await CommentEntity
.knexLoaderWithAuthorizationResults(viewerContext)
.loadManyByRawWhereClauseAsync('postId = ?', ['456']);
// Edge cases - these should NOT be transformed
const anotherEntity = {
loader: (ctx: any) => ctx, // This is not an entity class
};
anotherEntity.loader(viewerContext); // Should NOT be transformed (lowercase object)
// Complex chaining with regular method - should NOT be transformed
return CommentEntity
.loader(viewerContext)
.withAuthenticationResults()
.loadByIDAsync('789');
};
@@ -0,0 +1,28 @@
import { readFile, readdir } from 'fs/promises';
import { defineInlineTest } from 'jscodeshift/src/testUtils.js';
async function readfixtureAsync(mod: string, name: string): Promise<string> {
return await readFile(new URL(`../__testfixtures__/${mod}/${name}`, import.meta.url), {
encoding: 'utf8',
});
}
function prefixes(suffix: string): (names: string[]) => string[] {
return (names) => names.filter((n) => n.endsWith(suffix)).map((n) => n.replace(suffix, ''));
}
const modules = await readdir(new URL('../', import.meta.url)).then(prefixes('.ts'));
for (const mod of modules) {
const transform = await import(`../${mod}.ts`);
const fixtureDir = new URL(`../__testfixtures__/${mod}`, import.meta.url);
const fixtures = await readdir(fixtureDir).then(prefixes('.input.ts'));
for (const fixture of fixtures) {
defineInlineTest(
transform,
{},
await readfixtureAsync(mod, `${fixture}.input.ts`),
await readfixtureAsync(mod, `${fixture}.output.ts`),
);
}
}
@@ -0,0 +1,213 @@
import type { API, Collection, FileInfo, Options } from 'jscodeshift';
function transformEnforcingEntityChainMethod(
j: API['jscodeshift'],
root: Collection<any>,
type: 'enforcing' | 'withAuthorizationResults',
crudType: 'creator' | 'updater',
methodBefore: string,
methodAfter: string,
): void {
// Find all entity creator expressions of the form
// `TestEntity.creator(viewerContext).setField('wat', 2).enforceCreateAsync()`
root
.find(j.CallExpression, {
callee: {
type: 'MemberExpression',
property: {
name: methodBefore,
},
},
})
.forEach((path) => {
const enforceCreateAsyncCallExpression = path.node; // TestEntity.creator(viewerContext).setField('wat', 2).enforceCreateAsync()
const enforceCreateAsyncCallee = enforceCreateAsyncCallExpression.callee; // TestEntity.creator(viewerContext).setField('wat', 2).enforceCreateAsync
if (enforceCreateAsyncCallee.type !== 'MemberExpression') {
return;
}
if (enforceCreateAsyncCallee.property.type !== 'Identifier') {
return;
}
// traverse in until we find the first non-setField call
let lastCallee = enforceCreateAsyncCallee;
while (true) {
// TestEntity.creator(viewerContext).setField('wat', 2)
const maybeSetFieldObjectCallExpression = lastCallee.object;
if (maybeSetFieldObjectCallExpression.type !== 'CallExpression') {
break;
}
// TestEntity.creator(viewerContext).setField
const maybeSetFieldObjectCallee = maybeSetFieldObjectCallExpression.callee;
if (maybeSetFieldObjectCallee.type !== 'MemberExpression') {
break;
}
const maybeSetFieldObjectCalleeProperty = maybeSetFieldObjectCallee.property;
if (maybeSetFieldObjectCalleeProperty.type !== 'Identifier') {
break;
}
if (maybeSetFieldObjectCalleeProperty.name === 'setField') {
lastCallee = maybeSetFieldObjectCallee;
} else {
break;
}
}
// TestEntity.creator(viewerContext)
const maybeCreatorCallExpression = lastCallee.object;
if (maybeCreatorCallExpression.type !== 'CallExpression') {
return;
}
if (
maybeCreatorCallExpression.callee.type !== 'MemberExpression' ||
maybeCreatorCallExpression.callee.property.type !== 'Identifier' ||
maybeCreatorCallExpression.callee.property.name !== crudType
) {
return;
}
// TestEntity.creator(viewerContext).enforcing()
const newCreatorCallExpression = j.callExpression(
j.memberExpression(maybeCreatorCallExpression, j.identifier(type)),
[],
);
// TestEntity.creator(viewerContext).enforcing().setField('wat', 2).createAsync()
lastCallee.object = newCreatorCallExpression;
// change enforceCreateAsync to createAsync at the end so if this returns early it doesn't mutate
enforceCreateAsyncCallee.property.name = methodAfter;
});
}
function transformEnforcingEntityDeleteMethod(
j: API['jscodeshift'],
root: Collection<any>,
type: 'enforcing' | 'withAuthorizationResults',
methodBefore: string,
methodAfter: string,
): void {
// Find all entity creator expressions of the form
// `TestEntity.enforceDeleteAsync(entity)`
root
.find(j.CallExpression, {
callee: {
type: 'MemberExpression',
property: {
name: methodBefore,
},
},
})
.forEach((path) => {
const enforceDeleteAsyncCallExpression = path.node; // TestEntity.enforceDeleteAsync(entity)
const args = enforceDeleteAsyncCallExpression.arguments; // [entity]
enforceDeleteAsyncCallExpression.arguments = [];
const enforceDeleteAsyncCallee = enforceDeleteAsyncCallExpression.callee; // TestEntity.enforceDeleteAsync
if (enforceDeleteAsyncCallee.type !== 'MemberExpression') {
return;
}
// change enforceDeleteAsync to deleteAsync
if (enforceDeleteAsyncCallee.property.type !== 'Identifier') {
return;
}
enforceDeleteAsyncCallee.property.name = methodAfter;
enforceDeleteAsyncCallee.object = j.callExpression(
j.memberExpression(
j.callExpression(
j.memberExpression(enforceDeleteAsyncCallee.object, j.identifier('deleter')),
args,
),
j.identifier(type),
),
[],
);
});
}
function transformAssociationLoaderMethod(j: API['jscodeshift'], root: Collection<any>): void {
// Find all entity associationLoader expressions of the form
// `this.associationLoader()`
root
.find(j.CallExpression, {
callee: {
type: 'MemberExpression',
property: {
name: 'associationLoader',
},
},
})
.forEach((path) => {
const associationLoaderCallExpression = path.node; // this.associationLoader()
// replace with this.associationLoader().withAuthorizationResults()
path.replace(
j.callExpression(
j.memberExpression(
associationLoaderCallExpression,
j.identifier('withAuthorizationResults'),
),
[],
),
);
});
}
export default function transformer(file: FileInfo, api: API, _options: Options): string {
const j = api.jscodeshift;
const root = j.withParser('ts')(file.source);
// do authorization results first since it uses the same detection pattern as enforcing post-transform (i.e. it looks for createAsync)
transformEnforcingEntityChainMethod(
j,
root,
'withAuthorizationResults',
'creator',
'createAsync',
'createAsync',
);
transformEnforcingEntityChainMethod(
j,
root,
'withAuthorizationResults',
'updater',
'updateAsync',
'updateAsync',
);
transformEnforcingEntityDeleteMethod(
j,
root,
'withAuthorizationResults',
'deleteAsync',
'deleteAsync',
);
// now do enforcing
transformEnforcingEntityChainMethod(
j,
root,
'enforcing',
'creator',
'enforceCreateAsync',
'createAsync',
);
transformEnforcingEntityChainMethod(
j,
root,
'enforcing',
'updater',
'enforceUpdateAsync',
'updateAsync',
);
transformEnforcingEntityDeleteMethod(j, root, 'enforcing', 'enforceDeleteAsync', 'deleteAsync');
// association loader
transformAssociationLoaderMethod(j, root);
return root.toSource();
}
@@ -0,0 +1,81 @@
import type { API, Collection, FileInfo, Options } from 'jscodeshift';
function transformWithAuthorizationResultsEntityChainMethod(
j: API['jscodeshift'],
root: Collection<any>,
): void {
// Find all entity expressions of the form
// `TestEntity.thing(viewerContext).withAuthorizationResults()`
root
.find(j.CallExpression, {
callee: {
type: 'MemberExpression',
property: {
name: 'withAuthorizationResults',
},
},
})
.forEach((path) => {
const withAuthorizationResultsCallExpression = path.node; // TestEntity.thing(viewerContext).withAuthorizationResults()
const withAuthorizationResultsCallee = withAuthorizationResultsCallExpression.callee; // TestEntity.thing(viewerContext).withAuthorizationResults
if (withAuthorizationResultsCallee.type !== 'MemberExpression') {
return;
}
const thingCallExpression = withAuthorizationResultsCallee.object; // TestEntity.thing(viewerContext)
if (thingCallExpression.type !== 'CallExpression') {
return;
}
const thingCallee = thingCallExpression.callee; // TestEntity.thing
if (thingCallee.type !== 'MemberExpression') {
return;
}
if (thingCallee.property.type !== 'Identifier') {
return;
}
const newCreatorCalleeName = thingCallee.property.name + 'WithAuthorizationResults';
thingCallee.property.name = newCreatorCalleeName;
path.replace(thingCallExpression);
});
}
function transformEnforcingEntityChainMethod(j: API['jscodeshift'], root: Collection<any>): void {
// Find all entity expressions of the form
// `TestEntity.thing(viewerContext).enforcing()`
root
.find(j.CallExpression, {
callee: {
type: 'MemberExpression',
property: {
name: 'enforcing',
},
},
})
.forEach((path) => {
const enforcingCallExpression = path.node; // TestEntity.thing(viewerContext).enforcing()
const enforcingCallee = enforcingCallExpression.callee; // TestEntity.thing(viewerContext).enforcing
if (enforcingCallee.type !== 'MemberExpression') {
return;
}
const thingCallExpression = enforcingCallee.object; // TestEntity.thing(viewerContext)
if (thingCallExpression.type !== 'CallExpression') {
return;
}
path.replace(thingCallExpression);
});
}
export default function transformer(file: FileInfo, api: API, _options: Options): string {
const j = api.jscodeshift;
const root = j.withParser('ts')(file.source);
transformWithAuthorizationResultsEntityChainMethod(j, root);
transformEnforcingEntityChainMethod(j, root);
return root.toSource();
}
@@ -0,0 +1,165 @@
import type { API, Collection, FileInfo, Options } from 'jscodeshift';
const KNEX_SPECIFIC_METHODS = [
'loadFirstByFieldEqualityConjunctionAsync',
'loadManyByFieldEqualityConjunctionAsync',
'loadManyByRawWhereClauseAsync',
];
function isKnexSpecificMethodUsed(j: API['jscodeshift'], node: any): boolean {
// Check if this loader call is followed by a knex-specific method
// We need to traverse the AST to find usages of the loader result
const parent = node.parent;
// Check if the loader call is directly chained with a knex method
if (parent?.value.type === 'MemberExpression' && parent.value.object === node.value) {
const grandParent = parent.parent;
if (grandParent?.value.type === 'CallExpression' && grandParent.value.callee === parent.value) {
if (
parent.value.property.type === 'Identifier' &&
KNEX_SPECIFIC_METHODS.includes(parent.value.property.name)
) {
return true;
}
}
}
// Check if the loader is assigned to a variable and then used with knex methods
if (parent?.value.type === 'VariableDeclarator' && parent.value.init === node.value) {
const variableName = parent.value.id.name;
const scope = parent.scope;
// Find all references to this variable in the same scope
const references = j(scope.path)
.find(j.Identifier, { name: variableName })
.filter((path) => {
// Check if this identifier is used as object in member expression
const parentNode = path.parent.value;
if (parentNode.type === 'MemberExpression' && parentNode.object === path.value) {
const prop = parentNode.property;
if (prop.type === 'Identifier' && KNEX_SPECIFIC_METHODS.includes(prop.name)) {
return true;
}
}
return false;
});
if (references.size() > 0) {
return true;
}
}
// Check await expressions
if (parent?.value.type === 'AwaitExpression' && parent.value.argument === node.value) {
const awaitParent = parent.parent;
if (awaitParent?.value.type === 'VariableDeclarator') {
const variableName = awaitParent.value.id.name;
const scope = awaitParent.scope;
// Find all references to this variable in the same scope
const references = j(scope.path)
.find(j.Identifier, { name: variableName })
.filter((path) => {
const parentNode = path.parent.value;
if (parentNode.type === 'MemberExpression' && parentNode.object === path.value) {
const prop = parentNode.property;
if (prop.type === 'Identifier' && KNEX_SPECIFIC_METHODS.includes(prop.name)) {
return true;
}
}
return false;
});
if (references.size() > 0) {
return true;
}
}
}
return false;
}
function transformLoaderToKnexLoader(j: API['jscodeshift'], root: Collection<any>): void {
// Find all entity expressions of the form `Entity.loader(viewerContext)`
root
.find(j.CallExpression, {
callee: {
type: 'MemberExpression',
property: {
name: 'loader',
},
},
})
.forEach((path) => {
const loaderCallExpression = path.node; // Entity.loader(viewerContext)
const loaderCallee = loaderCallExpression.callee; // Entity.loader
if (loaderCallee.type !== 'MemberExpression') {
return;
}
// Make sure this is a static method call on an entity (not on an instance)
// Typically entity names start with uppercase letter
if (loaderCallee.object.type === 'Identifier') {
const firstChar = loaderCallee.object.name[0];
if (firstChar === firstChar?.toUpperCase()) {
// Check if this loader uses knex-specific methods
if (isKnexSpecificMethodUsed(j, path)) {
// Rename loader to knexLoader
if (loaderCallee.property.type === 'Identifier') {
loaderCallee.property.name = 'knexLoader';
}
}
}
}
});
}
function transformLoaderWithAuthorizationResultsToKnexLoaderWithAuthorizationResults(
j: API['jscodeshift'],
root: Collection<any>,
): void {
// Find all entity expressions of the form `Entity.loaderWithAuthorizationResults(viewerContext)`
root
.find(j.CallExpression, {
callee: {
type: 'MemberExpression',
property: {
name: 'loaderWithAuthorizationResults',
},
},
})
.forEach((path) => {
const loaderCallExpression = path.node; // Entity.loaderWithAuthorizationResults(viewerContext)
const loaderCallee = loaderCallExpression.callee; // Entity.loaderWithAuthorizationResults
if (loaderCallee.type !== 'MemberExpression') {
return;
}
// Make sure this is a static method call on an entity (not on an instance)
// Typically entity names start with uppercase letter
if (loaderCallee.object.type === 'Identifier') {
const firstChar = loaderCallee.object.name[0];
if (firstChar === firstChar?.toUpperCase()) {
// Check if this loader uses knex-specific methods
if (isKnexSpecificMethodUsed(j, path)) {
// Rename loaderWithAuthorizationResults to knexLoaderWithAuthorizationResults
if (loaderCallee.property.type === 'Identifier') {
loaderCallee.property.name = 'knexLoaderWithAuthorizationResults';
}
}
}
}
});
}
export default function transformer(file: FileInfo, api: API, _options: Options): string {
const j = api.jscodeshift;
const root = j.withParser('ts')(file.source);
transformLoaderToKnexLoader(j, root);
transformLoaderWithAuthorizationResultsToKnexLoaderWithAuthorizationResults(j, root);
return root.toSource();
}
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src/**/*"],
"exclude": ["**/__testfixtures__/**"]
}
@@ -0,0 +1,66 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
### Features
- add forShare and skipLocked selection modifiers to knex loader ([#705](https://github.com/expo/entity/issues/705)) ([243a038](https://github.com/expo/entity/commit/243a0386fb9755f3926ade8dcc1b92adf38cd923))
- add forUpdate to knex loader ([#704](https://github.com/expo/entity/issues/704)) ([727f166](https://github.com/expo/entity/commit/727f16662d8baa3757bb8e94f51d837f17fd333d))
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
# [0.61.0](https://github.com/expo/entity/compare/v0.60.0...v0.61.0) (2026-03-12)
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
# [0.60.0](https://github.com/expo/entity/compare/v0.59.0...v0.60.0) (2026-03-06)
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
# [0.59.0](https://github.com/expo/entity/compare/v0.58.0...v0.59.0) (2026-03-03)
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
# [0.58.0](https://github.com/expo/entity/compare/v0.57.0...v0.58.0) (2026-03-02)
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
# [0.57.0](https://github.com/expo/entity/compare/v0.56.0...v0.57.0) (2026-02-28)
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
# [0.56.0](https://github.com/expo/entity/compare/v0.55.0...v0.56.0) (2026-02-28)
### Bug Fixes
- move EntityPrivacyUtils back into core package ([#429](https://github.com/expo/entity/issues/429)) ([56ec27d](https://github.com/expo/entity/commit/56ec27d5f201d83930a2cc05dfc380a054bff1e1))
- pass in augmentable classes to installation ([#437](https://github.com/expo/entity/issues/437)) ([f5c728b](https://github.com/expo/entity/commit/f5c728be565e01c0a8ace9ef51f6de8f49184dab)), closes [/github.com/expo/entity/pull/410#pullrequestreview-3765893898](https://github.com//github.com/expo/entity/pull/410/issues/pullrequestreview-3765893898)
- refactor install method to instead be free functions ([#441](https://github.com/expo/entity/issues/441)) ([51d632d](https://github.com/expo/entity/commit/51d632da1619dc039d5c41ce6533f63254ec4337)), closes [#410](https://github.com/expo/entity/issues/410)
### Features
- add entity-database-adapter-knex-testing-utils containing StubPostgresDatabaseAdapter ([#412](https://github.com/expo/entity/issues/412)) ([eeccf59](https://github.com/expo/entity/commit/eeccf594c11bf090aa83bbf71fb4ead8668aec7d))
- add entityField SQL helper ([#481](https://github.com/expo/entity/issues/481)) ([459a5ff](https://github.com/expo/entity/commit/459a5ff85014a8c7396c0856dffacb1491ccb963))
- Add raw sqlfragment orderBy capability for pagination ([#457](https://github.com/expo/entity/issues/457)) ([0615ffa](https://github.com/expo/entity/commit/0615ffacffbe9e903a9bf03b171c102f0554b992))
- add support for nulls ordering in order by clauses ([#468](https://github.com/expo/entity/issues/468)) ([6c0838d](https://github.com/expo/entity/commit/6c0838dc577a3b6190e3551ee834925d7bd53bc6))
@@ -0,0 +1,3 @@
# @expo/entity-database-adapter-knex-testing-utils
Testing utilities for applications using Entity with Knex database adapter.
@@ -0,0 +1,45 @@
{
"name": "@expo/entity-database-adapter-knex-testing-utils",
"version": "0.69.0",
"description": "Testing utilities for applications using Entity with Knex database adapter",
"keywords": [
"entity"
],
"license": "MIT",
"author": "Expo",
"files": [
"build",
"!*.tsbuildinfo",
"!__*",
"src"
],
"type": "module",
"main": "build/src/index.js",
"types": "build/src/index.d.ts",
"scripts": {
"build": "tsc --build",
"prepack": "rm -rf build && yarn build",
"clean": "yarn build --clean",
"lint": "yarn run --top-level oxlint --type-aware src",
"lint-fix": "yarn run lint --fix",
"test": "yarn test:all --rootDir $(pwd)"
},
"dependencies": {
"@expo/entity": "workspace:^",
"@expo/entity-database-adapter-knex": "workspace:^",
"invariant": "^2.2.4",
"uuid": "^14.0.0"
},
"devDependencies": {
"@jest/globals": "30.5.1",
"@types/invariant": "2.2.37",
"@types/node": "24.13.5",
"typescript": "6.0.3"
},
"peerDependencies": {
"@jest/globals": "*"
},
"engines": {
"node": ">=18"
}
}
@@ -0,0 +1,323 @@
import type { EntityConfiguration, FieldTransformerMap } from '@expo/entity';
import {
computeIfAbsent,
getDatabaseFieldForEntityField,
IntField,
mapMap,
StringField,
transformFieldsToDatabaseObject,
} from '@expo/entity';
import type {
SQLFragment,
TableFieldMultiValueEqualityCondition,
TableFieldSingleValueEqualityCondition,
TableOrderByClause,
TableQuerySelectionModifiers,
} from '@expo/entity-database-adapter-knex';
import {
BasePostgresEntityDatabaseAdapter,
NullsOrdering,
OrderByOrdering,
} from '@expo/entity-database-adapter-knex';
import invariant from 'invariant';
import { v7 as uuidv7 } from 'uuid';
export class StubPostgresDatabaseAdapter<
TFields extends Record<string, any>,
TIDField extends keyof TFields,
> extends BasePostgresEntityDatabaseAdapter<TFields, TIDField> {
constructor(
private readonly entityConfiguration2: EntityConfiguration<TFields, TIDField>,
private readonly dataStore: Map<string, Readonly<{ [key: string]: any }>[]>,
) {
super(entityConfiguration2);
}
public static convertFieldObjectsToDataStore<
TFields extends Record<string, any>,
TIDField extends keyof TFields,
>(
entityConfiguration: EntityConfiguration<TFields, TIDField>,
dataStore: Map<string, Readonly<TFields>[]>,
): Map<string, Readonly<{ [key: string]: any }>[]> {
return mapMap(dataStore, (objectsForTable) =>
objectsForTable.map((objectForTable) =>
transformFieldsToDatabaseObject(entityConfiguration, new Map(), objectForTable),
),
);
}
public getObjectCollectionForTable(tableName: string): { [key: string]: any }[] {
return computeIfAbsent(this.dataStore, tableName, () => []);
}
protected getFieldTransformerMap(): FieldTransformerMap {
return new Map();
}
private static uniqBy<T>(a: T[], keyExtractor: (k: T) => string): T[] {
const seen = new Set();
return a.filter((item) => {
const k = keyExtractor(item);
if (seen.has(k)) {
return false;
}
seen.add(k);
return true;
});
}
protected async fetchManyWhereInternalAsync(
_queryInterface: any,
tableName: string,
tableColumns: readonly string[],
tableTuples: (readonly any[])[],
): Promise<object[]> {
const objectCollection = this.getObjectCollectionForTable(tableName);
const results = StubPostgresDatabaseAdapter.uniqBy(tableTuples, (tuple) =>
JSON.stringify(tuple),
).reduce(
(acc, tableTuple) => {
return acc.concat(
objectCollection.filter((obj) => {
return tableColumns.every((tableColumn, index) => {
return obj[tableColumn] === tableTuple[index];
});
}),
);
},
[] as { [key: string]: any }[],
);
return [...results];
}
protected async fetchOneWhereInternalAsync(
queryInterface: any,
tableName: string,
tableColumns: readonly string[],
tableTuple: readonly any[],
): Promise<object | null> {
const results = await this.fetchManyWhereInternalAsync(
queryInterface,
tableName,
tableColumns,
[tableTuple],
);
return results[0] ?? null;
}
private static compareByOrderBys<TFields extends Record<string, any>>(
orderBys: TableOrderByClause<TFields>[],
objectA: { [key: string]: any },
objectB: { [key: string]: any },
): 0 | 1 | -1 {
if (orderBys.length === 0) {
return 0;
}
const currentOrderBy = orderBys[0]!;
if (!('columnName' in currentOrderBy)) {
throw new Error('SQL fragment order by not supported for StubDatabaseAdapter');
}
const aField = objectA[currentOrderBy.columnName];
const bField = objectB[currentOrderBy.columnName];
// Determine effective nulls ordering:
// - If explicitly set, use that
// - Otherwise use PostgreSQL defaults: NULLS LAST for ASC, NULLS FIRST for DESC
const nullsFirst =
currentOrderBy.nulls !== undefined
? currentOrderBy.nulls === NullsOrdering.FIRST
: currentOrderBy.order === OrderByOrdering.DESCENDING;
if (aField === null && bField === null) {
return this.compareByOrderBys(orderBys.slice(1), objectA, objectB);
} else if (aField === null) {
return nullsFirst ? -1 : 1;
} else if (bField === null) {
return nullsFirst ? 1 : -1;
}
switch (currentOrderBy.order) {
case OrderByOrdering.DESCENDING: {
return aField > bField
? -1
: aField < bField
? 1
: this.compareByOrderBys(orderBys.slice(1), objectA, objectB);
}
case OrderByOrdering.ASCENDING: {
return bField > aField
? -1
: bField < aField
? 1
: this.compareByOrderBys(orderBys.slice(1), objectA, objectB);
}
}
}
protected async fetchManyByFieldEqualityConjunctionInternalAsync(
_queryInterface: any,
tableName: string,
tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
): Promise<object[]> {
let filteredObjects = this.getObjectCollectionForTable(tableName);
for (const { tableField, tableValue } of tableFieldSingleValueEqualityOperands) {
filteredObjects = filteredObjects.filter((obj) => obj[tableField] === tableValue);
}
for (const { tableField, tableValues } of tableFieldMultiValueEqualityOperands) {
filteredObjects = filteredObjects.filter((obj) => tableValues.includes(obj[tableField]));
}
const orderBy = querySelectionModifiers.orderBy;
if (orderBy !== undefined) {
filteredObjects = filteredObjects.sort((a, b) =>
StubPostgresDatabaseAdapter.compareByOrderBys(orderBy, a, b),
);
}
const offset = querySelectionModifiers.offset;
if (offset !== undefined) {
filteredObjects = filteredObjects.slice(offset);
}
const limit = querySelectionModifiers.limit;
if (limit !== undefined) {
filteredObjects = filteredObjects.slice(0, 0 + limit);
}
return filteredObjects;
}
protected fetchManyBySQLFragmentInternalAsync(
_queryInterface: any,
_tableName: string,
_sqlFragment: SQLFragment<TFields>,
_querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
): Promise<object[]> {
throw new Error('SQL fragments not supported for StubDatabaseAdapter');
}
protected async countByFieldEqualityConjunctionInternalAsync(
queryInterface: any,
tableName: string,
tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
): Promise<number> {
const results = await this.fetchManyByFieldEqualityConjunctionInternalAsync(
queryInterface,
tableName,
tableFieldSingleValueEqualityOperands,
tableFieldMultiValueEqualityOperands,
{
orderBy: undefined,
offset: undefined,
limit: undefined,
forUpdate: undefined,
forShare: undefined,
skipLocked: undefined,
},
);
return results.length;
}
protected countBySQLFragmentInternalAsync(
_queryInterface: any,
_tableName: string,
_sqlFragment: SQLFragment<TFields>,
): Promise<number> {
throw new Error('SQL fragment count not supported for StubDatabaseAdapter');
}
private generateRandomID(): any {
const idSchemaField = this.entityConfiguration2.schema.get(this.entityConfiguration2.idField);
invariant(
idSchemaField,
`No schema field found for ${String(this.entityConfiguration2.idField)}`,
);
if (idSchemaField instanceof StringField) {
return uuidv7();
} else if (idSchemaField instanceof IntField) {
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
} else {
throw new Error(
`Unsupported ID type for StubPostgresDatabaseAdapter: ${idSchemaField.constructor.name}`,
);
}
}
protected async insertInternalAsync(
_queryInterface: any,
tableName: string,
object: object,
): Promise<object[]> {
const objectCollection = this.getObjectCollectionForTable(tableName);
const idField = getDatabaseFieldForEntityField(
this.entityConfiguration2,
this.entityConfiguration2.idField,
);
const objectToInsert = {
[idField]: this.generateRandomID(),
...object,
};
objectCollection.push(objectToInsert);
return [objectToInsert];
}
protected async updateInternalAsync(
_queryInterface: any,
tableName: string,
tableIdField: string,
id: any,
object: object,
): Promise<{ updatedRowCount: number }> {
// SQL does not support empty updates, mirror behavior here for better test simulation
if (Object.keys(object).length === 0) {
throw new Error(`Empty update (${tableIdField} = ${id})`);
}
const objectCollection = this.getObjectCollectionForTable(tableName);
const objectIndex = objectCollection.findIndex((obj) => {
return obj[tableIdField] === id;
});
// SQL updates to a nonexistent row succeed but affect 0 rows,
// mirror that behavior here for better test simulation
if (objectIndex < 0) {
return { updatedRowCount: 0 };
}
objectCollection[objectIndex] = {
...objectCollection[objectIndex],
...object,
};
return { updatedRowCount: 1 };
}
protected async deleteInternalAsync(
_queryInterface: any,
tableName: string,
tableIdField: string,
id: any,
): Promise<number> {
const objectCollection = this.getObjectCollectionForTable(tableName);
const objectIndex = objectCollection.findIndex((obj) => {
return obj[tableIdField] === id;
});
// SQL deletes to a nonexistent row succeed and affect 0 rows,
// mirror that behavior here for better test simulation
if (objectIndex < 0) {
return 0;
}
objectCollection.splice(objectIndex, 1);
return 1;
}
}
@@ -0,0 +1,17 @@
import type {
EntityConfiguration,
EntityDatabaseAdapter,
IEntityDatabaseAdapterProvider,
} from '@expo/entity';
import { StubPostgresDatabaseAdapter } from './StubPostgresDatabaseAdapter.ts';
export class StubPostgresDatabaseAdapterProvider implements IEntityDatabaseAdapterProvider {
private readonly objectCollection = new Map();
getDatabaseAdapter<TFields extends Record<string, any>, TIDField extends keyof TFields>(
entityConfiguration: EntityConfiguration<TFields, TIDField>,
): EntityDatabaseAdapter<TFields, TIDField> {
return new StubPostgresDatabaseAdapter(entityConfiguration, this.objectCollection);
}
}
@@ -0,0 +1,61 @@
import type { EntityCompanionDefinition, ViewerContext } from '@expo/entity';
import {
AlwaysAllowPrivacyPolicyRule,
DateField,
Entity,
EntityConfiguration,
EntityPrivacyPolicy,
} from '@expo/entity';
export type DateIDTestFields = {
id: Date;
};
export const dateIDTestEntityConfiguration = new EntityConfiguration<DateIDTestFields, 'id'>({
idField: 'id',
tableName: 'simple_test_entity_should_not_write_to_db',
schema: {
id: new DateField({
columnName: 'custom_id',
cache: true,
}),
},
databaseAdapterFlavor: 'postgres',
cacheAdapterFlavor: 'redis',
});
export class DateIDTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
DateIDTestFields,
'id',
ViewerContext,
DateIDTestEntity
> {
protected override readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<DateIDTestFields, 'id', ViewerContext, DateIDTestEntity>(),
];
protected override readonly createRules = [
new AlwaysAllowPrivacyPolicyRule<DateIDTestFields, 'id', ViewerContext, DateIDTestEntity>(),
];
protected override readonly updateRules = [
new AlwaysAllowPrivacyPolicyRule<DateIDTestFields, 'id', ViewerContext, DateIDTestEntity>(),
];
protected override readonly deleteRules = [
new AlwaysAllowPrivacyPolicyRule<DateIDTestFields, 'id', ViewerContext, DateIDTestEntity>(),
];
}
export class DateIDTestEntity extends Entity<DateIDTestFields, 'id', ViewerContext> {
static defineCompanionDefinition(): EntityCompanionDefinition<
DateIDTestFields,
'id',
ViewerContext,
DateIDTestEntity,
DateIDTestEntityPrivacyPolicy
> {
return {
entityClass: DateIDTestEntity,
entityConfiguration: dateIDTestEntityConfiguration,
privacyPolicyClass: DateIDTestEntityPrivacyPolicy,
};
}
}
@@ -0,0 +1,94 @@
import type { EntityCompanionDefinition, ViewerContext } from '@expo/entity';
import {
AlwaysAllowPrivacyPolicyRule,
Entity,
EntityConfiguration,
EntityPrivacyPolicy,
UUIDField,
} from '@expo/entity';
export type SimpleTestFields = {
id: string;
};
export type SimpleTestFieldSelection = keyof SimpleTestFields;
export const simpleTestEntityConfiguration = new EntityConfiguration<SimpleTestFields, 'id'>({
idField: 'id',
tableName: 'simple_test_entity_should_not_write_to_db',
schema: {
id: new UUIDField({
columnName: 'custom_id',
cache: true,
}),
},
databaseAdapterFlavor: 'postgres',
cacheAdapterFlavor: 'redis',
});
export class SimpleTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
SimpleTestFields,
'id',
ViewerContext,
SimpleTestEntity,
SimpleTestFieldSelection
> {
protected override readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<
SimpleTestFields,
'id',
ViewerContext,
SimpleTestEntity,
SimpleTestFieldSelection
>(),
];
protected override readonly createRules = [
new AlwaysAllowPrivacyPolicyRule<
SimpleTestFields,
'id',
ViewerContext,
SimpleTestEntity,
SimpleTestFieldSelection
>(),
];
protected override readonly updateRules = [
new AlwaysAllowPrivacyPolicyRule<
SimpleTestFields,
'id',
ViewerContext,
SimpleTestEntity,
SimpleTestFieldSelection
>(),
];
protected override readonly deleteRules = [
new AlwaysAllowPrivacyPolicyRule<
SimpleTestFields,
'id',
ViewerContext,
SimpleTestEntity,
SimpleTestFieldSelection
>(),
];
}
export class SimpleTestEntity extends Entity<
SimpleTestFields,
'id',
ViewerContext,
SimpleTestFieldSelection
> {
static defineCompanionDefinition(): EntityCompanionDefinition<
SimpleTestFields,
'id',
ViewerContext,
SimpleTestEntity,
SimpleTestEntityPrivacyPolicy,
SimpleTestFieldSelection
> {
return {
entityClass: SimpleTestEntity,
entityConfiguration: simpleTestEntityConfiguration,
privacyPolicyClass: SimpleTestEntityPrivacyPolicy,
};
}
}
@@ -0,0 +1,130 @@
import type { EntityCompanionDefinition, ViewerContext } from '@expo/entity';
import {
AlwaysAllowPrivacyPolicyRule,
DateField,
Entity,
EntityConfiguration,
EntityPrivacyPolicy,
IntField,
StringField,
UUIDField,
} from '@expo/entity';
import type { Result } from '@expo/results';
import { result } from '@expo/results';
export type TestFields = {
customIdField: string;
testIndexedField: string;
stringField: string;
intField: number;
dateField: Date;
nullableField: string | null;
};
export const testEntityConfiguration = new EntityConfiguration<TestFields, 'customIdField'>({
idField: 'customIdField',
tableName: 'test_entity_should_not_write_to_db',
schema: {
customIdField: new UUIDField({
columnName: 'custom_id',
cache: true,
}),
testIndexedField: new StringField({
columnName: 'test_index',
cache: true,
}),
stringField: new StringField({
columnName: 'string_field',
}),
intField: new IntField({
columnName: 'number_field',
}),
dateField: new DateField({
columnName: 'date_field',
}),
nullableField: new StringField({
columnName: 'nullable_field',
}),
},
databaseAdapterFlavor: 'postgres',
cacheAdapterFlavor: 'redis',
compositeFieldDefinitions: [
{ compositeField: ['stringField', 'intField'], cache: false },
{ compositeField: ['stringField', 'testIndexedField'], cache: true },
{ compositeField: ['nullableField', 'testIndexedField'], cache: true },
],
});
export class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<
TestFields,
'customIdField',
ViewerContext,
TestEntity
> {
protected override readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<TestFields, 'customIdField', ViewerContext, TestEntity>(),
];
protected override readonly createRules = [
new AlwaysAllowPrivacyPolicyRule<TestFields, 'customIdField', ViewerContext, TestEntity>(),
];
protected override readonly updateRules = [
new AlwaysAllowPrivacyPolicyRule<TestFields, 'customIdField', ViewerContext, TestEntity>(),
];
protected override readonly deleteRules = [
new AlwaysAllowPrivacyPolicyRule<TestFields, 'customIdField', ViewerContext, TestEntity>(),
];
}
export class TestEntity extends Entity<TestFields, 'customIdField', ViewerContext> {
static defineCompanionDefinition(): EntityCompanionDefinition<
TestFields,
'customIdField',
ViewerContext,
TestEntity,
TestEntityPrivacyPolicy
> {
return {
entityClass: TestEntity,
entityConfiguration: testEntityConfiguration,
privacyPolicyClass: TestEntityPrivacyPolicy,
};
}
getBlah(): string {
return 'Hello World!';
}
static async helloAsync(
viewerContext: ViewerContext,
testValue: string,
): Promise<Result<TestEntity>> {
const fields = {
customIdField: testValue,
testIndexedField: 'hello',
stringField: 'hello',
intField: 1,
dateField: new Date(),
nullableField: null,
};
return result(
new TestEntity({
viewerContext,
id: testValue,
databaseFields: fields,
selectedFields: fields,
}),
);
}
static async returnErrorAsync(_viewerContext: ViewerContext): Promise<Result<TestEntity>> {
return result(new Error('return entity'));
}
static async throwErrorAsync(_viewerContext: ViewerContext): Promise<Result<TestEntity>> {
throw new Error('threw entity');
}
static async nonResultAsync(_viewerContext: ViewerContext, testValue: string): Promise<string> {
return testValue;
}
}
@@ -0,0 +1,61 @@
import type { EntityCompanionDefinition, ViewerContext } from '@expo/entity';
import {
AlwaysAllowPrivacyPolicyRule,
Entity,
EntityConfiguration,
EntityPrivacyPolicy,
IntField,
} from '@expo/entity';
export type NumberKeyFields = {
id: number;
};
export const numberKeyEntityConfiguration = new EntityConfiguration<NumberKeyFields, 'id'>({
idField: 'id',
tableName: 'simple_test_entity_should_not_write_to_db',
schema: {
id: new IntField({
columnName: 'custom_id',
cache: false,
}),
},
databaseAdapterFlavor: 'postgres',
cacheAdapterFlavor: 'redis',
});
export class NumberKeyPrivacyPolicy extends EntityPrivacyPolicy<
NumberKeyFields,
'id',
ViewerContext,
NumberKeyEntity
> {
protected override readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<NumberKeyFields, 'id', ViewerContext, NumberKeyEntity>(),
];
protected override readonly createRules = [
new AlwaysAllowPrivacyPolicyRule<NumberKeyFields, 'id', ViewerContext, NumberKeyEntity>(),
];
protected override readonly updateRules = [
new AlwaysAllowPrivacyPolicyRule<NumberKeyFields, 'id', ViewerContext, NumberKeyEntity>(),
];
protected override readonly deleteRules = [
new AlwaysAllowPrivacyPolicyRule<NumberKeyFields, 'id', ViewerContext, NumberKeyEntity>(),
];
}
export class NumberKeyEntity extends Entity<NumberKeyFields, 'id', ViewerContext> {
static defineCompanionDefinition(): EntityCompanionDefinition<
NumberKeyFields,
'id',
ViewerContext,
NumberKeyEntity,
NumberKeyPrivacyPolicy
> {
return {
entityClass: NumberKeyEntity,
entityConfiguration: numberKeyEntityConfiguration,
privacyPolicyClass: NumberKeyPrivacyPolicy,
};
}
}
@@ -0,0 +1,32 @@
import { expect, it } from '@jest/globals';
import { readFile } from 'fs/promises';
it.each([
'StubPostgresDatabaseAdapter',
'StubPostgresDatabaseAdapterProvider',
'createUnitTestPostgresEntityCompanionProvider',
])('%s is the same as in @expo/entity-database-adapter-knex', async (fileName) => {
// These stub adapters need to be shared for testing, but we can't have them in the main
// entity-database-adapter-knex package since they would be exposed in production.
// Therefore, we duplicate them and ensure they stay in sync.
const fileContentsFromEntityDatabaseAdapterKnex = await readFile(
new URL(
`../../../entity-database-adapter-knex/src/__tests__/fixtures/${fileName}.ts`,
import.meta.url,
),
'utf-8',
);
const fileContentsFromTestingUtils = await readFile(
new URL(`../${fileName}.ts`, import.meta.url),
'utf-8',
);
const trimmedFiles = [
fileContentsFromEntityDatabaseAdapterKnex,
fileContentsFromTestingUtils,
].map((file) => file.substring(file.indexOf('export')));
expect(trimmedFiles[0]?.length).toBeGreaterThan(0);
expect(trimmedFiles[0]).toEqual(trimmedFiles[1]);
});
@@ -0,0 +1,16 @@
import { EntityCompanionProvider, ViewerContext } from '@expo/entity';
import { describe, expect, it } from '@jest/globals';
import { TestEntity } from '../__testfixtures__/TestEntity.ts';
import { createUnitTestPostgresEntityCompanionProvider } from '../createUnitTestPostgresEntityCompanionProvider.ts';
describe(createUnitTestPostgresEntityCompanionProvider, () => {
it('creates a new EntityCompanionProvider', async () => {
const companionProvider = createUnitTestPostgresEntityCompanionProvider();
expect(companionProvider).toBeInstanceOf(EntityCompanionProvider);
const viewerContext = new ViewerContext(companionProvider);
await expect(TestEntity.creator(viewerContext).createAsync()).resolves.toBeInstanceOf(
TestEntity,
);
});
});
@@ -0,0 +1,39 @@
import type { IEntityMetricsAdapter } from '@expo/entity';
import { EntityCompanionProvider, NoOpEntityMetricsAdapter } from '@expo/entity';
import {
InMemoryFullCacheStubCacheAdapterProvider,
StubQueryContextProvider,
} from '@expo/entity-testing-utils';
import { StubPostgresDatabaseAdapterProvider } from './StubPostgresDatabaseAdapterProvider.ts';
const queryContextProvider = new StubQueryContextProvider();
/**
* Entity companion provider for use in knex unit tests. All database and cache implementations
* are replaced with in-memory simulations.
*/
export const createUnitTestPostgresEntityCompanionProvider = (
metricsAdapter: IEntityMetricsAdapter = new NoOpEntityMetricsAdapter(),
): EntityCompanionProvider => {
return new EntityCompanionProvider(
metricsAdapter,
new Map([
[
'postgres',
{
adapterProvider: new StubPostgresDatabaseAdapterProvider(),
queryContextProvider,
},
],
]),
new Map([
[
'redis',
{
cacheAdapterProvider: new InMemoryFullCacheStubCacheAdapterProvider(),
},
],
]),
);
};
@@ -0,0 +1,9 @@
/* eslint-disable tsdoc/syntax */
/**
* @packageDocumentation
* @module @expo/entity-database-adapter-knex-testing-utils
*/
export * from './createUnitTestPostgresEntityCompanionProvider.ts';
export * from './StubPostgresDatabaseAdapter.ts';
export * from './StubPostgresDatabaseAdapterProvider.ts';
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src/**/*"],
"references": [{ "path": "../entity" }, { "path": "../entity-database-adapter-knex" }]
}
@@ -0,0 +1,425 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
### Features
- add ID-based and field-based knex database loaders for use with locking modifiers ([#706](https://github.com/expo/entity/issues/706)) ([e87e2a3](https://github.com/expo/entity/commit/e87e2a3a28f4279f5911bfd0189964b94a2ced7f))
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
### Features
- add forShare and skipLocked selection modifiers to knex loader ([#705](https://github.com/expo/entity/issues/705)) ([243a038](https://github.com/expo/entity/commit/243a0386fb9755f3926ade8dcc1b92adf38cd923))
- add forUpdate to knex loader ([#704](https://github.com/expo/entity/issues/704)) ([727f166](https://github.com/expo/entity/commit/727f16662d8baa3757bb8e94f51d837f17fd333d))
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
### Bug Fixes
- only throw EntityDatabaseAdapterPaginationCursorInvalidError from cursor decoding logic ([#537](https://github.com/expo/entity/issues/537)) ([e3cee07](https://github.com/expo/entity/commit/e3cee07091af8d1e6c5bb2d82f13e837c7658ea9))
# [0.61.0](https://github.com/expo/entity/compare/v0.60.0...v0.61.0) (2026-03-12)
### Features
- update the SQLExpression and SQLFragmentHelpers APIs ([#516](https://github.com/expo/entity/issues/516)) ([112d4dc](https://github.com/expo/entity/commit/112d4dc9b379da5317ff6acaf1d2e9bd597128a6))
# [0.60.0](https://github.com/expo/entity/compare/v0.59.0...v0.60.0) (2026-03-06)
### Features
- Add SQLExpression class that supports fluent comparison method API ([#506](https://github.com/expo/entity/issues/506)) ([535b681](https://github.com/expo/entity/commit/535b6811e08578aba1aadb095f3e401cfe7eaa26))
# [0.59.0](https://github.com/expo/entity/compare/v0.58.0...v0.59.0) (2026-03-03)
### Features
- add support to SQLFragment for `= ANY(?)` ([#498](https://github.com/expo/entity/issues/498)) ([b931daa](https://github.com/expo/entity/commit/b931daa9a082cdb33f4559d6d32e1655f0459b79))
# [0.58.0](https://github.com/expo/entity/compare/v0.57.0...v0.58.0) (2026-03-02)
### Bug Fixes
- fix some security review suggestions ([#485](https://github.com/expo/entity/issues/485)) ([e263bd2](https://github.com/expo/entity/commit/e263bd218cda579365766d0918c4961304bd1d9f))
# [0.57.0](https://github.com/expo/entity/compare/v0.56.0...v0.57.0) (2026-02-28)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.56.0](https://github.com/expo/entity/compare/v0.55.0...v0.56.0) (2026-02-28)
### Bug Fixes
- Add default for TSelectedFields for application-instantiated types ([#438](https://github.com/expo/entity/issues/438)) ([3d4aa0e](https://github.com/expo/entity/commit/3d4aa0e9efa519a36f3db69bd8f117a993c100fa))
- Apply same viewer context subclass type mechanism to knexLoader and related methods ([#472](https://github.com/expo/entity/issues/472)) ([018a40b](https://github.com/expo/entity/commit/018a40bc01d579ffbf88f308e641921e6fc39097))
- correct discriminated union type for forward/backward pagination ([#474](https://github.com/expo/entity/issues/474)) ([90ae6e2](https://github.com/expo/entity/commit/90ae6e299bda26ac9a80d83e83334d4152c022dd))
- correct pagination behavior for standard pagination with descending ordering clauses ([#477](https://github.com/expo/entity/issues/477)) ([e32c4c2](https://github.com/expo/entity/commit/e32c4c2bcc05f225a556f95c90f1ee0f4e24de74))
- correct precedence parentheses for SQLFragment joins ([#476](https://github.com/expo/entity/issues/476)) ([2c4c649](https://github.com/expo/entity/commit/2c4c649a4bca5bc9ffdc41eb3135b2dfe4db5e57)), closes [/github.com/expo/entity/blob/main/packages/entity-database-adapter-knex/src/internal/EntityKnexDataManager.ts#L303](https://github.com//github.com/expo/entity/blob/main/packages/entity-database-adapter-knex/src/internal/EntityKnexDataManager.ts/issues/L303)
- eliminate some unnecessary any casts ([#448](https://github.com/expo/entity/issues/448)) ([cc78029](https://github.com/expo/entity/commit/cc78029117c14510b0934b5e0d19876a45570f8d))
- explicitly document behavior when cursor entity no longer exists ([#453](https://github.com/expo/entity/issues/453)) ([4dc156c](https://github.com/expo/entity/commit/4dc156c7a8dc50bc9e2802be133b9e258b289f54)), closes [#422](https://github.com/expo/entity/issues/422) [#431](https://github.com/expo/entity/issues/431)
- move EntityPrivacyUtils back into core package ([#429](https://github.com/expo/entity/issues/429)) ([56ec27d](https://github.com/expo/entity/commit/56ec27d5f201d83930a2cc05dfc380a054bff1e1))
- pass in augmentable classes to installation ([#437](https://github.com/expo/entity/issues/437)) ([f5c728b](https://github.com/expo/entity/commit/f5c728be565e01c0a8ace9ef51f6de8f49184dab)), closes [/github.com/expo/entity/pull/410#pullrequestreview-3765893898](https://github.com//github.com/expo/entity/pull/410/issues/pullrequestreview-3765893898)
- refactor install method to instead be free functions ([#441](https://github.com/expo/entity/issues/441)) ([51d632d](https://github.com/expo/entity/commit/51d632da1619dc039d5c41ce6533f63254ec4337)), closes [#410](https://github.com/expo/entity/issues/410)
- update integration test to use BlahEntity.knexLoader syntax ([#444](https://github.com/expo/entity/issues/444)) ([457f58a](https://github.com/expo/entity/commit/457f58ad2c4e1cf09f3132f3b9af5008ba0290c7)), closes [#441](https://github.com/expo/entity/issues/441)
- Use TSelectedFields for knex loader order by method ([#424](https://github.com/expo/entity/issues/424)) ([a9d09f4](https://github.com/expo/entity/commit/a9d09f42b67378d06133bcfb5b99c9afb17aac0d))
### Features
- add entity-database-adapter-knex-testing-utils containing StubPostgresDatabaseAdapter ([#412](https://github.com/expo/entity/issues/412)) ([eeccf59](https://github.com/expo/entity/commit/eeccf594c11bf090aa83bbf71fb4ead8668aec7d))
- add entityField SQL helper ([#481](https://github.com/expo/entity/issues/481)) ([459a5ff](https://github.com/expo/entity/commit/459a5ff85014a8c7396c0856dffacb1491ccb963))
- Add ilike and trigram similarity search to pagination ([#431](https://github.com/expo/entity/issues/431)) ([f72cbca](https://github.com/expo/entity/commit/f72cbcaf5a722e6bb8299644ec32b07f1d3362a9)), closes [#422](https://github.com/expo/entity/issues/422)
- add method to get pagination cursor for single entity ([#475](https://github.com/expo/entity/issues/475)) ([898f71c](https://github.com/expo/entity/commit/898f71ce082c2593585575b60bd6ef2546717124))
- Add paginated loader to entity-database-adapter-knex ([#422](https://github.com/expo/entity/issues/422)) ([58deffd](https://github.com/expo/entity/commit/58deffd426b0214073ede608cd47c04db2596d60))
- Add pagination max page size configuration ([#436](https://github.com/expo/entity/issues/436)) ([3ec4b5f](https://github.com/expo/entity/commit/3ec4b5f0cd53bf18d6bcfa27dd29b70d3f4d5f19))
- add PostgresEntity/ReadonlyPostgresEntity classes that expose knexLoader methods ([#442](https://github.com/expo/entity/issues/442)) ([40b409f](https://github.com/expo/entity/commit/40b409f6f889febf8b90b8837caecac26ff64174)), closes [#441](https://github.com/expo/entity/issues/441)
- Add raw sqlfragment orderBy capability for pagination ([#457](https://github.com/expo/entity/issues/457)) ([0615ffa](https://github.com/expo/entity/commit/0615ffacffbe9e903a9bf03b171c102f0554b992))
- add support for nulls ordering in order by clauses ([#468](https://github.com/expo/entity/issues/468)) ([6c0838d](https://github.com/expo/entity/commit/6c0838dc577a3b6190e3551ee834925d7bd53bc6))
- support nullable search fields with postgres transforms ([#470](https://github.com/expo/entity/issues/470)) ([53b0604](https://github.com/expo/entity/commit/53b0604152b8ca5ad9fc616d18478f93473c7eb0))
# [0.55.0](https://github.com/expo/entity/compare/v0.54.0...v0.55.0) (2026-01-27)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.54.0](https://github.com/expo/entity/compare/v0.53.0...v0.54.0) (2026-01-21)
### Bug Fixes
- **deps:** pin dependencies ([#328](https://github.com/expo/entity/issues/328)) ([5679f27](https://github.com/expo/entity/commit/5679f27209f1515ca5e626d858c4aa054b414625))
# [0.53.0](https://github.com/expo/entity/compare/v0.52.0...v0.53.0) (2025-12-19)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.52.0](https://github.com/expo/entity/compare/v0.51.0...v0.52.0) (2025-12-19)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.51.0](https://github.com/expo/entity/compare/v0.50.0...v0.51.0) (2025-11-21)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.50.0](https://github.com/expo/entity/compare/v0.49.0...v0.50.0) (2025-10-07)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.49.0](https://github.com/expo/entity/compare/v0.48.0...v0.49.0) (2025-10-07)
### Features
- throw specific errors for database adapter issues ([#311](https://github.com/expo/entity/issues/311)) ([f6639f9](https://github.com/expo/entity/commit/f6639f9def4ac20b628dd48fdca8d7cf61b6347b))
# [0.48.0](https://github.com/expo/entity/compare/v0.47.0...v0.48.0) (2025-09-19)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.47.0](https://github.com/expo/entity/compare/v0.46.0...v0.47.0) (2025-09-19)
### Features
- add support for Buffer fields ([#305](https://github.com/expo/entity/issues/305)) ([31b52a5](https://github.com/expo/entity/commit/31b52a5f75842f4810f66c12ad03a10d9192f889))
# [0.46.0](https://github.com/expo/entity/compare/v0.45.0...v0.46.0) (2025-06-26)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.45.0](https://github.com/expo/entity/compare/v0.44.0...v0.45.0) (2025-06-06)
### Features
- add createOrGetExistingAsync and createWithUniqueConstraintRecoveryAsync utility methods ([#291](https://github.com/expo/entity/issues/291)) ([78bc264](https://github.com/expo/entity/commit/78bc264b3328692b4b6722388a57d298f02e65c5))
# [0.44.0](https://github.com/expo/entity/compare/v0.43.0...v0.44.0) (2025-05-29)
### Bug Fixes
- introduce transactional dataloader mode and fix global transaction ID generation ([#289](https://github.com/expo/entity/issues/289)) ([d3e89bd](https://github.com/expo/entity/commit/d3e89bdd82e6df5c5b35522bc3574fd9e1cfddff))
### Features
- add transaction-scoped dataloaders ([#284](https://github.com/expo/entity/issues/284)) ([e054a0d](https://github.com/expo/entity/commit/e054a0d74b3a39158ca7d73d05f59d3633113400)), closes [#98](https://github.com/expo/entity/issues/98) [#194](https://github.com/expo/entity/issues/194)
# [0.43.0](https://github.com/expo/entity/compare/v0.42.0...v0.43.0) (2025-04-10)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.42.0](https://github.com/expo/entity/compare/v0.41.0...v0.42.0) (2025-04-10)
### Features
- add @expo/entity-testing-utils package ([#280](https://github.com/expo/entity/issues/280)) ([485894a](https://github.com/expo/entity/commit/485894af16e233d533a9480285bbda56812cbe0b))
- add composite field loading and caching ([#272](https://github.com/expo/entity/issues/272)) ([f0aa0da](https://github.com/expo/entity/commit/f0aa0dafebdb56418cbd22dda437233d850db3e7))
- change entity id field generic to field name and derive type where necessary ([#278](https://github.com/expo/entity/issues/278)) ([b7e524c](https://github.com/expo/entity/commit/b7e524c41892797608d0b884410b8d520a80a9ef))
- convert batched/cached loader interface to holder pattern ([#271](https://github.com/expo/entity/issues/271)) ([06b3cb7](https://github.com/expo/entity/commit/06b3cb7f8076ef9e5b83cdf154da159801bd14cc)), closes [#201](https://github.com/expo/entity/issues/201)
- enforce explicit id field cache property ([#276](https://github.com/expo/entity/issues/276)) ([1da5cc0](https://github.com/expo/entity/commit/1da5cc01e1affc3b3338a0ab2050504b5853ac6f))
# [0.41.0](https://github.com/expo/entity/compare/v0.40.0...v0.41.0) (2025-03-07)
- feat!: Default all loaders and mutators to enforcing (#256) ([a976e76](https://github.com/expo/entity/commit/a976e765098bca27049585e43a3bed5c71b9ac6b)), closes [#256](https://github.com/expo/entity/issues/256)
### BREAKING CHANGES
- Default all loaders and mutators to enforcing
# [0.40.0](https://github.com/expo/entity/compare/v0.39.0...v0.40.0) (2025-02-27)
### Reverts
- Revert "v0.40.0" ([fa78ce8](https://github.com/expo/entity/commit/fa78ce84e2f2eb8199e36fd51779fc68905001c0))
# [0.39.0](https://github.com/expo/entity/compare/v0.38.0...v0.39.0) (2024-11-21)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.38.0](https://github.com/expo/entity/compare/v0.37.0...v0.38.0) (2024-06-27)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.37.0](https://github.com/expo/entity/compare/v0.36.0...v0.37.0) (2024-06-12)
### Bug Fixes
- update barrels ([#241](https://github.com/expo/entity/issues/241)) ([8c5f81b](https://github.com/expo/entity/commit/8c5f81bcca107e9b7bbea5f0ab41b0105057806e))
# [0.36.0](https://github.com/expo/entity/compare/v0.35.0...v0.36.0) (2024-06-12)
### Bug Fixes
- constrain entity fields type to string-keyed object ([#235](https://github.com/expo/entity/issues/235)) ([7e2cea1](https://github.com/expo/entity/commit/7e2cea16973a0ae1917f867cd25d6ef7c8eaecef))
# [0.35.0](https://github.com/expo/entity/compare/v0.34.0...v0.35.0) (2024-04-11)
### Bug Fixes
- Revert: require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([1bdfbd6](https://github.com/expo/entity/commit/1bdfbd6b562d1200e4029df0533b5adb2d917831))
# [0.34.0](https://github.com/expo/entity/compare/v0.33.0...v0.34.0) (2024-04-11)
### Features
- require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([8b0b31f](https://github.com/expo/entity/commit/8b0b31fdde5bd565aa527719003ef283a45f55cc))
# [0.33.0](https://github.com/expo/entity/compare/v0.32.0...v0.33.0) (2023-10-06)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.32.0](https://github.com/expo/entity/compare/v0.31.1...v0.32.0) (2023-06-10)
### Features
- add ability to specify knex transaction config ([#207](https://github.com/expo/entity/issues/207)) ([2069a0d](https://github.com/expo/entity/commit/2069a0d9fd1d8f7d090a71a90a758b2429ea5dd3))
- entity companion definition static thunk ([#210](https://github.com/expo/entity/issues/210)) ([4b18010](https://github.com/expo/entity/commit/4b18010d42be50ef329f428b08330e21bf676586))
# [0.31.0](https://github.com/expo/entity/compare/v0.30.0...v0.31.0) (2023-01-19)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.30.0](https://github.com/expo/entity/compare/v0.29.0...v0.30.0) (2022-12-23)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.29.0](https://github.com/expo/entity/compare/v0.28.0...v0.29.0) (2022-10-06)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.28.0](https://github.com/expo/entity/compare/v0.27.0...v0.28.0) (2022-09-13)
### Features
- add BigIntField ([#193](https://github.com/expo/entity/issues/193)) ([acbad82](https://github.com/expo/entity/commit/acbad826a37bb5a054b4dcf9be91d11aa5f7f29a))
- nested transactions ([#194](https://github.com/expo/entity/issues/194)) ([a77b914](https://github.com/expo/entity/commit/a77b914ad6dc018d1e309c9caf4971088a553ecf))
# [0.27.0](https://github.com/expo/entity/compare/v0.26.1...v0.27.0) (2022-08-09)
### Features
- add orderByRaw to loadManyByRawWhereClauseAsync ([#185](https://github.com/expo/entity/issues/185)) ([2817d78](https://github.com/expo/entity/commit/2817d78392dfecd0093111c24d581bdf01dba06d))
## [0.26.1](https://github.com/expo/entity/compare/v0.26.0...v0.26.1) (2022-07-28)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.26.0](https://github.com/expo/entity/compare/v0.25.3...v0.26.0) (2022-07-14)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
## [0.25.3](https://github.com/expo/entity/compare/v0.25.2...v0.25.3) (2022-03-12)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
## [0.25.2](https://github.com/expo/entity/compare/v0.25.1...v0.25.2) (2022-03-12)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
## [0.25.1](https://github.com/expo/entity/compare/v0.25.0...v0.25.1) (2022-03-10)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.25.0](https://github.com/expo/entity/compare/v0.24.0...v0.25.0) (2022-03-10)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.24.0](https://github.com/expo/entity/compare/v0.23.0...v0.24.0) (2022-02-16)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.23.0](https://github.com/expo/entity/compare/v0.22.0...v0.23.0) (2022-02-09)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
### Features
- Upgrade knex to 1.x in `@expo/entity-database-adapter-knex` ([#150](https://github.com/expo/entity/issues/150)) ([e1b0ee5](https://github.com/expo/entity/commit/e1b0ee528175646239d88aa71ffce73983eeb52f))
# [0.21.0](https://github.com/expo/entity/compare/v0.20.0...v0.21.0) (2022-01-03)
### Features
- add pre-commit callbacks on EntityQueryContext ([#147](https://github.com/expo/entity/issues/147)) ([f1d9847](https://github.com/expo/entity/commit/f1d9847210f5775de964509e44ce493e44482b21))
# [0.20.0](https://github.com/expo/entity/compare/v0.19.0...v0.20.0) (2021-12-29)
### Features
- add cascading deletion info ([#145](https://github.com/expo/entity/issues/145)) ([3727191](https://github.com/expo/entity/commit/372719176742d3ead6b7cbca29cb66b09b3fe09c))
# [0.19.0](https://github.com/expo/entity/compare/v0.18.0...v0.19.0) (2021-10-28)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.18.0](https://github.com/expo/entity/compare/v0.17.0...v0.18.0) (2021-10-14)
### Features
- enable noImplicitOverride tsc setting ([#135](https://github.com/expo/entity/issues/135)) ([4263cb9](https://github.com/expo/entity/commit/4263cb9b8b69fe4fb68c74ec1c7aba04508a1555))
- upgrade TypeScript to 4.4 ([#134](https://github.com/expo/entity/issues/134)) ([7612392](https://github.com/expo/entity/commit/7612392dbfd9778d25c465c1626c372d75a6d05a))
# [0.17.0](https://github.com/expo/entity/compare/v0.16.0...v0.17.0) (2021-08-16)
### Features
- Add IntField and FloatField, deprecate NumberField ([#131](https://github.com/expo/entity/issues/131)) ([2f2d963](https://github.com/expo/entity/commit/2f2d963cf92c6575d78f8eeca344a4aadcc9b8d9))
# [0.16.0](https://github.com/expo/entity/compare/v0.15.0...v0.16.0) (2021-07-07)
### Features
- allow null field values in loadManyByFieldEqualityConjunctionAsync ([#130](https://github.com/expo/entity/issues/130)) ([2f37dc6](https://github.com/expo/entity/commit/2f37dc6a37d368b12b3c8375e04f0777fd448797))
# [0.15.0](https://github.com/expo/entity/compare/v0.14.1...v0.15.0) (2021-05-26)
### Bug Fixes
- upgrade ioredis, knex, and pg ([#125](https://github.com/expo/entity/issues/125)) ([7c43edf](https://github.com/expo/entity/commit/7c43edf3de37e8af13e2a99bd44f03f54803d9b9))
### Features
- secondary cache loader ([#123](https://github.com/expo/entity/issues/123)) ([4cba01e](https://github.com/expo/entity/commit/4cba01eb259c87d60b3026ce776e46f781363690))
## [0.14.1](https://github.com/expo/entity/compare/v0.14.0...v0.14.1) (2021-03-24)
### Bug Fixes
- typescript typedef of EntityMutationInfo ([#121](https://github.com/expo/entity/issues/121)) ([8e8f0dc](https://github.com/expo/entity/commit/8e8f0dc8636e343e29a6012d155e4111477a8989))
# [0.14.0](https://github.com/expo/entity/compare/v0.13.0...v0.14.0) (2021-03-24)
### Bug Fixes
- mutation validator and trigger cyclic import structure ([#118](https://github.com/expo/entity/issues/118)) ([b11dbc0](https://github.com/expo/entity/commit/b11dbc087ba5ad13c748032698dd73279b7960b0))
# [0.13.0](https://github.com/expo/entity/compare/v0.12.0...v0.13.0) (2021-02-12)
### Features
- add basic field type runtime validators ([#113](https://github.com/expo/entity/issues/113)) ([6c4d4b0](https://github.com/expo/entity/commit/6c4d4b03b5404dd776a12c5eb6f0af77a8c964f8))
# [0.12.0](https://github.com/expo/entity/compare/v0.11.0...v0.12.0) (2021-01-22)
### Bug Fixes
- move runInTransaction to ViewerContext ([#108](https://github.com/expo/entity/issues/108)) ([b7309e1](https://github.com/expo/entity/commit/b7309e18a2dd780b64cd306d566de1f12bf8df3a))
- remove cache and database adaptor flavor enums ([#109](https://github.com/expo/entity/issues/109)) ([72a77f8](https://github.com/expo/entity/commit/72a77f8b6893a4b76d3b1cce16659bdf3ce473ee))
# [0.11.0](https://github.com/expo/entity/compare/v0.10.0...v0.11.0) (2020-12-24)
### Features
- better cache adapter error handling ([#102](https://github.com/expo/entity/issues/102)) ([15546aa](https://github.com/expo/entity/commit/15546aad97734ab9ac67f1c012485f096a0e94f1))
- better database adapter error handling ([#101](https://github.com/expo/entity/issues/101)) ([5208aee](https://github.com/expo/entity/commit/5208aeedb0192f0460ba3d575770a04175f5c3aa))
# [0.10.0](https://github.com/expo/entity/compare/v0.9.1...v0.10.0) (2020-11-17)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
## [0.9.1](https://github.com/expo/entity/compare/v0.9.0...v0.9.1) (2020-11-06)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.9.0](https://github.com/expo/entity/compare/v0.8.1...v0.9.0) (2020-11-05)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
## [0.8.1](https://github.com/expo/entity/compare/v0.8.0...v0.8.1) (2020-10-09)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.8.0](https://github.com/expo/entity/compare/v0.7.1...v0.8.0) (2020-10-09)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
## [0.7.1](https://github.com/expo/entity/compare/v0.7.0...v0.7.1) (2020-10-01)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.7.0](https://github.com/expo/entity/compare/v0.6.0...v0.7.0) (2020-10-01)
### Bug Fixes
- fix index export for EntityMutationTrigger and EntityMutationValidator ([#73](https://github.com/expo/entity/issues/73)) ([7e829f3](https://github.com/expo/entity/commit/7e829f30fde0bd343cb8a584ccda5530cf61ec33))
- move cache invalidation to after transaction commit ([#77](https://github.com/expo/entity/issues/77)) ([dbc3c81](https://github.com/expo/entity/commit/dbc3c81b359a668b5134189ce6de49b5893bb5e7))
- upgrade packages ([#78](https://github.com/expo/entity/issues/78)) ([9891e74](https://github.com/expo/entity/commit/9891e7469467a28589f529c8d87b10fc2232d3ff))
# [0.6.0](https://github.com/expo/entity/compare/v0.5.2...v0.6.0) (2020-07-23)
### Bug Fixes
- separate out EntityMutationValidator ([#68](https://github.com/expo/entity/issues/68)) ([547a1ef](https://github.com/expo/entity/commit/547a1efcecd17cc085702d1a3e9888ce5b644b13))
### Features
- entity mutation validators ([#67](https://github.com/expo/entity/issues/67)) ([fc4377d](https://github.com/expo/entity/commit/fc4377d8839da07417b88afc138f73556383d896))
- mutation triggers ([#65](https://github.com/expo/entity/issues/65)) ([fd6060c](https://github.com/expo/entity/commit/fd6060cc844f60635b3ce4c400c4877f2df8fa44))
## [0.5.2](https://github.com/expo/entity/compare/v0.5.1...v0.5.2) (2020-07-02)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
## [0.5.1](https://github.com/expo/entity/compare/v0.5.0...v0.5.1) (2020-07-01)
### Bug Fixes
- correct some typing issues and exports ([#57](https://github.com/expo/entity/issues/57)) ([e91cfba](https://github.com/expo/entity/commit/e91cfba99321c2c4da2078aafd8627cbf0ceca50))
# 0.5.0 (2020-07-01)
### Features
- support entity fields subset of db fields ([#49](https://github.com/expo/entity/issues/49)) ([4e40b2e](https://github.com/expo/entity/commit/4e40b2e521407e521d236978ec3b3b56db3990be))
@@ -9,16 +9,16 @@
During `EntityCompanionProvider` instantiation:
```typescript
import Knex from 'knex';
import { knex, Knex } from 'knex';
const knexInstance = Knex({
const knexInstance = knex({
client: 'pg',
connection: {
user: process.env.PGUSER,
password: process.env.PGPASSWORD,
host: process.env.PGHOST,
port: parseInt(process.env.PGPORT!, 10),
database: process.env.PGDATABASE,
user: process.env['PGUSER'],
password: process.env['PGPASSWORD'],
host: process.env['PGHOST'],
port: parseInt(nullthrows(process.env['PGPORT']), 10),
database: process.env['PGDATABASE'],
},
});
@@ -29,7 +29,7 @@ export const createDefaultEntityCompanionProvider = (
metricsAdapter,
{
// add the knex database adapter flavor
[DatabaseAdapterFlavor.POSTGRES]: {
['postgres']: {
adapter: PostgresEntityDatabaseAdapter,
queryContextProvider: new PostgresEntityQueryContextProvider(knexInstance),
},
@@ -39,4 +39,4 @@ export const createDefaultEntityCompanionProvider = (
}
);
};
```
```
@@ -1,37 +1,42 @@
{
"name": "@expo/entity-database-adapter-knex",
"version": "0.4.0",
"version": "0.69.0",
"description": "Knex database adapter for @expo/entity",
"files": [
"build",
"src"
],
"main": "build/index.js",
"types": "build/index.d.ts",
"scripts": {
"tsc": "tsc",
"clean": "rm -rf build coverage coverage-integration",
"lint": "eslint src --ext '.ts'",
"test": "jest --rootDir --config ../../resources/jest.config.js --passWithNoTests",
"integration": "../../resources/run-with-docker yarn integration-no-setup",
"integration-no-setup": "jest --config ../../resources/jest-integration.config.js --rootDir --runInBand --passWithNoTests",
"barrelsby": "barrelsby --directory src --location top --exclude tests__ --singleQuotes --exportDefault --delete"
},
"engines": {
"node": ">=8"
},
"keywords": [
"entity"
],
"author": "Expo",
"license": "MIT",
"peerDependencies": {
"@expo/entity": "*"
"author": "Expo",
"files": [
"build",
"!*.tsbuildinfo",
"!__*",
"src"
],
"type": "module",
"main": "build/src/index.js",
"types": "build/src/index.d.ts",
"scripts": {
"build": "tsc --build",
"prepack": "rm -rf build && yarn build",
"clean": "yarn build --clean",
"lint": "yarn run --top-level oxlint --type-aware src",
"lint-fix": "yarn lint --fix",
"test": "yarn test:all --rootDir $(pwd)",
"integration": "yarn integration:all --rootDir $(pwd)"
},
"dependencies": {
"knex": "^0.20.11"
"@expo/entity": "workspace:^",
"knex": "^3.2.9"
},
"devDependencies": {
"@expo/entity": "^0.4.0"
"@expo/entity-testing-utils": "workspace:^",
"@jest/globals": "30.5.1",
"pg": "8.23.0",
"ts-mockito": "2.6.1",
"typescript": "6.0.3"
},
"engines": {
"node": ">=18"
}
}
@@ -0,0 +1,762 @@
import type {
EntityConfiguration,
EntityConstructionUtils,
EntityPrivacyPolicy,
EntityQueryContext,
IEntityClass,
IEntityMetricsAdapter,
ReadonlyEntity,
ViewerContext,
} from '@expo/entity';
import { EntityNotFoundError, mapMap } from '@expo/entity';
import type { Result } from '@expo/results';
import { result } from '@expo/results';
import assert from 'assert';
import type {
FieldEqualityCondition,
NullsOrdering,
OrderByOrdering,
} from './BasePostgresEntityDatabaseAdapter.ts';
import { isSingleValueFieldEqualityCondition } from './BasePostgresEntityDatabaseAdapter.ts';
import { BaseSQLQueryBuilder } from './BaseSQLQueryBuilder.ts';
import type { PaginationStrategy } from './PaginationStrategy.ts';
import type { SQLFragment } from './SQLOperator.ts';
import type {
Connection,
EntityKnexDataManager,
PageInfo,
} from './internal/EntityKnexDataManager.ts';
import type { NonNullableKeys } from './internal/utilityTypes.ts';
export type EntityLoaderBaseOrderByClause = {
/**
* The OrderByOrdering to order by.
*/
order: OrderByOrdering;
/**
* Optional nulls ordering for this order by clause. If not provided, no specific nulls ordering is applied.
*/
nulls?: NullsOrdering | undefined;
};
export type EntityLoaderFieldNameOrderByClause<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields = keyof TFields,
> = EntityLoaderBaseOrderByClause & {
/**
* The field name to order by.
*/
fieldName: TSelectedFields;
};
export type EntityLoaderFieldFragmentOrderByClause<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields = keyof TFields,
> = EntityLoaderBaseOrderByClause & {
/**
* The SQL fragment to order by, which can reference selected fields. Example: `COALESCE(NULLIF(display_name, ''), split_part(full_name, '/', 2))`.
* May not contain ASC or DESC, as ordering direction is controlled by the `order` property.
*/
fieldFragment: SQLFragment<Pick<TFields, TSelectedFields>>;
};
export type EntityLoaderOrderByClause<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields = keyof TFields,
> =
| EntityLoaderFieldNameOrderByClause<TFields, TSelectedFields>
| EntityLoaderFieldFragmentOrderByClause<TFields, TSelectedFields>;
/**
* Row locking modifier that locks the selected rows for update using `SELECT ... FOR UPDATE`.
* The lock is held until the end of the transaction, so the query context must be transactional.
*/
export interface EntityLoaderForUpdateRowLockingModifier {
forUpdate: true;
forShare?: never;
}
/**
* Row locking modifier that locks the selected rows in share mode using `SELECT ... FOR SHARE`.
* The lock is held until the end of the transaction, so the query context must be transactional.
*/
export interface EntityLoaderForShareRowLockingModifier {
forShare: true;
forUpdate?: never;
}
/**
* Row locking modifiers for load methods that never report a missing row as an error. Exactly one of
* `forUpdate` or `forShare` is required. `skipLocked` may be set to skip rows already locked by another
* transaction (using `SKIP LOCKED`) instead of waiting for them; skipped rows are reported as missing.
*/
export type EntityLoaderRowLockingModifiers = (
| EntityLoaderForUpdateRowLockingModifier
| EntityLoaderForShareRowLockingModifier
) & {
skipLocked?: boolean;
};
/**
* Row locking modifiers for load methods that throw when a row is missing. Exactly one of `forUpdate`
* or `forShare` is required. `skipLocked` is not permitted since a row skipped because another transaction
* holds a lock on it would be indistinguishable from a row that does not exist.
*/
export type EntityLoaderRowLockingModifiersWithoutSkipLocked = (
| EntityLoaderForUpdateRowLockingModifier
| EntityLoaderForShareRowLockingModifier
) & {
skipLocked?: never;
};
/**
* SQL modifiers that only affect the selection but not the projection.
*/
export interface EntityLoaderQuerySelectionModifiers<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields = keyof TFields,
> {
/**
* Order the entities by specified columns and orders.
*/
orderBy?: readonly EntityLoaderOrderByClause<TFields, TSelectedFields>[];
/**
* Skip the specified number of entities queried before returning.
*/
offset?: number;
/**
* Limit the number of entities returned.
*/
limit?: number;
/**
* Lock the selected rows for update using `SELECT ... FOR UPDATE`. The lock is held until the
* end of the transaction, so the query context must be transactional. Mutually exclusive with `forShare`.
*/
forUpdate?: boolean;
/**
* Lock the selected rows in share mode using `SELECT ... FOR SHARE`. The lock is held until the
* end of the transaction, so the query context must be transactional. Mutually exclusive with `forUpdate`.
*/
forShare?: boolean;
/**
* Skip rows that are already locked by another transaction using `SKIP LOCKED` instead of waiting for them.
* Requires `forUpdate` or `forShare`.
*/
skipLocked?: boolean;
}
/**
* Function to be used for constructing search fields based on field names.
* The function takes a field name and returns a SQLFragment that can be used in the search field portion.
*/
export type EntityLoaderFieldNameConstructorFn<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields = keyof TFields,
> = (fieldName: TSelectedFields) => SQLFragment<TFields>;
/**
* Specification for a search field that is a manually constructed SQLFragment. Useful for complex search fields that require
* transformations to make nullable fields non-null or to make combinations of multiple fields.
*/
export type EntityLoaderSearchFieldSQLFragmentFnSpecification<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields = keyof TFields,
> = {
/**
* Function that constructs the SQLFragment for the search field.
*
* @param getFragmentForFieldName - A helper function that takes a field name and returns a SQLFragment for that field, which should be used
* to construct the final SQLFragment for the search field.
* @returns a SQLFragment representing the search field, which must reference selected fields through the provided helper function.
*
* @example
* For example, to construct a search field that searches by display name (nullable column) with fallback to full name,
* the fieldConstructor function could be implemented as follows:
* ```
* fieldConstructor: (getFragmentForFieldName) => {
* const displayNameFragment = getFragmentForFieldName('display_name');
* const fullNameFragment = getFragmentForFieldName('full_name');
* return sql`COALESCE(NULLIF(${displayNameFragment}, ''), split_part(${fullNameFragment}, '/', 2))`;
* }
* ```
*/
fieldConstructor: (
/**
* Helper function to get a SQLFragment for a given field name, which should be used to construct the final SQLFragment for the search field.
*/
getFragmentForFieldName: EntityLoaderFieldNameConstructorFn<TFields, TSelectedFields>,
) => SQLFragment<Pick<TFields, TSelectedFields>>;
};
/**
* Set of selected fields that are non-nullable, which can be used for search fields that require non-nullable fields.
* To use a nullable field as a search field, use an EntityLoaderSearchFieldSQLFragmentFnSpecification with appropriate SQL
* to handle null values, such as `COALESCE(field_name, '')` to treat nulls as empty strings for searching.
*/
export type NonNullableSelectedFields<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields,
> = TSelectedFields & NonNullableKeys<TFields>;
/**
* Specification for a search field that can be either a simple selected field name (which must be non-nullable) or a manually constructed
* SQLFragment using EntityLoaderSearchFieldSQLFragmentFnSpecification.
*/
export type EntityLoaderSearchFieldSpecification<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields = keyof TFields,
> =
| NonNullableSelectedFields<TFields, TSelectedFields>
| EntityLoaderSearchFieldSQLFragmentFnSpecification<TFields, TSelectedFields>;
/**
* Base specification shared by all search-based pagination strategies.
*/
export interface SearchSpecificationBase<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields = keyof TFields,
> {
/**
* The search term to search for. Must be a non-empty string.
*/
term: string;
/**
* The fields to search within.
*/
fields: readonly EntityLoaderSearchFieldSpecification<TFields, TSelectedFields>[];
}
/**
* Search specification for case-insensitive pattern matching using the SQL ILIKE operator.
*/
export interface ILikeSearchSpecification<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields = keyof TFields,
> extends SearchSpecificationBase<TFields, TSelectedFields> {
/**
* Case-insensitive pattern matching search using SQL ILIKE operator.
* Results are ordered by the fields being searched within in the order specified, then by ID for tie-breaking and stable pagination.
*/
strategy: PaginationStrategy.ILIKE_SEARCH;
}
/**
* Search specification for similarity search using PostgreSQL trigram similarity.
*/
export interface TrigramSearchSpecification<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields = keyof TFields,
> extends SearchSpecificationBase<TFields, TSelectedFields> {
/**
* Similarity search using PostgreSQL trigram similarity. Results are ordered by exact match priority, then by similarity score, then by specified extra order by fields if provided, then by ID for tie-breaking and stable pagination.
* Note that trigram similarity search can be significantly slower than ILIKE search, especially on large datasets without appropriate indexes, and results may not be as relevant as more advanced full-text search solutions.
* It is recommended to use this strategy only when ILIKE search does not meet the application's needs and to ensure appropriate database indexing for performance.
*/
strategy: PaginationStrategy.TRIGRAM_SEARCH;
/**
* Similarity threshold for trigram matching.
* Must be between 0 and 1, where:
* - 0 matches everything
* - 1 requires exact match
*
* Recommended threshold values:
* - 0.3: Loose matching, allows more variation (default PostgreSQL similarity threshold)
* - 0.4-0.5: Moderate matching, good balance for most use cases
* - 0.6+: Strict matching, requires high similarity
*/
threshold: number;
/**
* Optional additional fields to order by after similarity score and before ID for tie-breaking.
* These fields are independent of search fields and can be used to provide meaningful
* ordering when multiple results have the same similarity score.
*/
extraOrderByFields?: readonly EntityLoaderSearchFieldSpecification<TFields, TSelectedFields>[];
}
/**
* Pagination specification for standard (non-search) pagination.
*/
export interface StandardPaginationSpecification<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields = keyof TFields,
> {
/**
* Standard pagination without search. Results are ordered by the specified orderBy fields.
*/
strategy: PaginationStrategy.STANDARD;
/**
* Order the entities by specified columns and orders. If the ID field is not included, it will be automatically added for stable pagination.
*/
orderBy: readonly EntityLoaderOrderByClause<TFields, TSelectedFields>[];
}
/**
* Pagination specification for SQL-based pagination (with or without search).
*/
export type PaginationSpecification<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields = keyof TFields,
> =
| StandardPaginationSpecification<TFields, TSelectedFields>
| ILikeSearchSpecification<TFields, TSelectedFields>
| TrigramSearchSpecification<TFields, TSelectedFields>;
/**
* Base unified pagination arguments
*/
interface EntityLoaderBaseUnifiedPaginationArgs<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields = keyof TFields,
> {
/**
* SQLFragment representing the WHERE clause to filter the entities being paginated.
*/
where?: SQLFragment<Pick<TFields, TSelectedFields>>;
/**
* Pagination specification determining how to order and paginate results.
*/
pagination: PaginationSpecification<TFields, TSelectedFields>;
}
/**
* Forward unified pagination arguments
*/
export interface EntityLoaderForwardUnifiedPaginationArgs<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields = keyof TFields,
> extends EntityLoaderBaseUnifiedPaginationArgs<TFields, TSelectedFields> {
/**
* The number of entities to return starting from the entity after the cursor. Must be a positive integer.
*/
first: number;
/**
* The number of entities to return starting from the entity before the cursor. Must be a positive integer.
* Not allowed with forward pagination.
*/
last?: never;
/**
* The cursor to paginate after for forward pagination.
*/
after?: string;
/**
* The cursor to paginate before for backward pagination.
* Not allowed with forward pagination.
*/
before?: never;
}
/**
* Backward unified pagination arguments
*/
export interface EntityLoaderBackwardUnifiedPaginationArgs<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields = keyof TFields,
> extends EntityLoaderBaseUnifiedPaginationArgs<TFields, TSelectedFields> {
/**
* The number of entities to return starting from the entity after the cursor. Must be a positive integer.
* Not allowed with backward pagination.
*/
first?: never;
/**
* The number of entities to return starting from the entity before the cursor. Must be a positive integer.
*/
last: number;
/**
* The cursor to paginate after for forward pagination.
* Not allowed with backward pagination.
*/
after?: never;
/**
* The cursor to paginate before for backward pagination.
*/
before?: string;
}
/**
* Load page pagination arguments, which can be either forward or backward unified pagination arguments.
*/
export type EntityLoaderLoadPageArgs<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields = keyof TFields,
> =
| EntityLoaderForwardUnifiedPaginationArgs<TFields, TSelectedFields>
| EntityLoaderBackwardUnifiedPaginationArgs<TFields, TSelectedFields>;
/**
* Authorization-result-based knex entity loader for non-data-loader-based load methods.
* All loads through this loader are results (or null for some loader methods), where an
* unsuccessful result means an authorization error or entity construction error occurred.
* Other errors are thrown.
*/
export class AuthorizationResultBasedKnexEntityLoader<
TFields extends Record<string, any>,
TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TIDField,
TViewerContext,
TEntity,
TSelectedFields
>,
TSelectedFields extends keyof TFields = keyof TFields,
> {
constructor(
private readonly queryContext: EntityQueryContext,
private readonly entityConfiguration: EntityConfiguration<TFields, TIDField>,
private readonly entityClass: IEntityClass<
TFields,
TIDField,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
>,
private readonly knexDataManager: EntityKnexDataManager<TFields, TIDField>,
protected readonly metricsAdapter: IEntityMetricsAdapter,
private readonly constructionUtils: EntityConstructionUtils<
TFields,
TIDField,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
>,
) {}
/**
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
* @returns entity result for matching ID, where result error can be UnauthorizedError or EntityNotFoundError.
*/
async loadByIDFromDatabaseAsync(
id: TFields[TIDField],
modifiers: EntityLoaderRowLockingModifiersWithoutSkipLocked,
): Promise<Result<TEntity>> {
const entityResults = await this.loadManyByIDsFromDatabaseAsync([id], modifiers);
const entityResult = entityResults.get(id);
assert(entityResult !== undefined, `${id} should be guaranteed to be present in returned map`);
return entityResult;
}
/**
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
* @returns entity result for matching ID, or null if no entity exists for ID, where result error can be UnauthorizedError.
*/
async loadByIDNullableFromDatabaseAsync(
id: TFields[TIDField],
modifiers: EntityLoaderRowLockingModifiers,
): Promise<Result<TEntity> | null> {
const entityResults = await this.loadManyByIDsNullableFromDatabaseAsync([id], modifiers);
return entityResults.get(id) ?? null;
}
/**
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
* @returns map from ID to corresponding entity result, where result error can be UnauthorizedError or EntityNotFoundError.
*/
async loadManyByIDsFromDatabaseAsync(
ids: readonly TFields[TIDField][],
modifiers: EntityLoaderRowLockingModifiersWithoutSkipLocked,
): Promise<ReadonlyMap<TFields[TIDField], Result<TEntity>>> {
const entityResults = await this.loadManyByIDsNullableFromDatabaseAsync(ids, modifiers);
return mapMap(
entityResults,
(entityResult, id) =>
entityResult ??
result(
new EntityNotFoundError({
entityClass: this.entityClass,
fieldName: this.entityConfiguration.idField,
fieldValue: id,
}),
),
);
}
/**
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
* @returns map from ID to nullable corresponding entity result, where result error can be UnauthorizedError.
*/
async loadManyByIDsNullableFromDatabaseAsync(
ids: readonly TFields[TIDField][],
modifiers: EntityLoaderRowLockingModifiers,
): Promise<ReadonlyMap<TFields[TIDField], Result<TEntity> | null>> {
const idField = this.entityConfiguration.idField;
this.constructionUtils.validateFieldAndValues(idField, ids);
const fieldObjects =
ids.length > 0
? await this.knexDataManager.loadManyByFieldEqualityConjunctionAsync(
this.queryContext,
[{ fieldName: idField, fieldValues: ids }],
modifiers,
)
: [];
const idsToFieldObjects = new Map<TFields[TIDField], readonly Readonly<TFields>[]>(
ids.map((id) => [id, []]),
);
for (const fieldObject of fieldObjects) {
const id = fieldObject[idField];
idsToFieldObjects.set(id, [...(idsToFieldObjects.get(id) ?? []), fieldObject]);
}
const idsToEntityResults =
await this.constructionUtils.constructAndAuthorizeEntitiesAsync(idsToFieldObjects);
return mapMap(idsToEntityResults, (entityResults) => entityResults[0] ?? null);
}
/**
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
* @returns entity result where uniqueFieldName equals fieldValue, or null if no entity matches the condition, where result error can be UnauthorizedError.
* @throws when multiple entities match the condition
*/
async loadByFieldEqualingFromDatabaseAsync<N extends keyof Pick<TFields, TSelectedFields>>(
uniqueFieldName: N,
fieldValue: NonNullable<TFields[N]>,
modifiers: EntityLoaderRowLockingModifiers,
): Promise<Result<TEntity> | null> {
const entityResults = await this.loadManyByFieldEqualingFromDatabaseAsync(
uniqueFieldName,
fieldValue,
modifiers,
);
assert(
entityResults.length <= 1,
`loadByFieldEqualingFromDatabase: Multiple entities of type ${this.entityClass.name} found for ${String(
uniqueFieldName,
)}=${fieldValue}`,
);
return entityResults[0] ?? null;
}
/**
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
* @returns array of entity results where fieldName equals fieldValue, where result error can be UnauthorizedError
*/
async loadManyByFieldEqualingFromDatabaseAsync<N extends keyof Pick<TFields, TSelectedFields>>(
fieldName: N,
fieldValue: NonNullable<TFields[N]>,
modifiers: EntityLoaderRowLockingModifiers,
): Promise<readonly Result<TEntity>[]> {
return await this.loadManyByFieldEqualityConjunctionAsync(
[{ fieldName, fieldValue }],
modifiers,
);
}
/**
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
* @returns the first entity results that matches the query, where result error can be
* UnauthorizedError
*/
async loadFirstByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[],
querySelectionModifiers: Omit<
EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields>,
'limit'
> &
Required<Pick<EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields>, 'orderBy'>>,
): Promise<Result<TEntity> | null> {
const results = await this.loadManyByFieldEqualityConjunctionAsync(fieldEqualityOperands, {
...querySelectionModifiers,
limit: 1,
});
return results[0] ?? null;
}
/**
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
* @returns array of entity results that match the query, where result error can be UnauthorizedError
*/
async loadManyByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[],
querySelectionModifiers: EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields> = {},
): Promise<readonly Result<TEntity>[]> {
for (const fieldEqualityOperand of fieldEqualityOperands) {
const fieldValues = isSingleValueFieldEqualityCondition(fieldEqualityOperand)
? [fieldEqualityOperand.fieldValue]
: fieldEqualityOperand.fieldValues;
this.constructionUtils.validateFieldAndValues(fieldEqualityOperand.fieldName, fieldValues);
}
const fieldObjects = await this.knexDataManager.loadManyByFieldEqualityConjunctionAsync(
this.queryContext,
fieldEqualityOperands,
querySelectionModifiers,
);
return await this.constructionUtils.constructAndAuthorizeEntitiesArrayAsync(fieldObjects);
}
/**
* Count entities matching the conjunction of field equality operands.
* This does not perform authorization since count does not load full entities.
* Note that this should be used with the same caution as loadManyByFieldEqualityConjunctionAsync
* regarding indexing since counts can be expensive on large datasets without appropriate indexes.
*
* @returns count of entities matching the filters
*/
async countByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[],
): Promise<number> {
for (const fieldEqualityOperand of fieldEqualityOperands) {
const fieldValues = isSingleValueFieldEqualityCondition(fieldEqualityOperand)
? [fieldEqualityOperand.fieldValue]
: fieldEqualityOperand.fieldValues;
this.constructionUtils.validateFieldAndValues(fieldEqualityOperand.fieldName, fieldValues);
}
return await this.knexDataManager.countByFieldEqualityConjunctionAsync(
this.queryContext,
fieldEqualityOperands,
);
}
/**
* Count entities matching a SQL fragment.
* This does not perform authorization since count does not load full entities.
* Note that this should be used with the same caution as loadManyBySQL regarding indexing
* since counts can be expensive on large datasets without appropriate indexes.
*
* @returns count of entities matching the query
*/
async countBySQLAsync(fragment: SQLFragment<Pick<TFields, TSelectedFields>>): Promise<number> {
return await this.knexDataManager.countBySQLFragmentAsync(this.queryContext, fragment);
}
/**
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
* @returns SQL query builder for building and executing SQL queries that when executed returns entity results where result error can be UnauthorizedError.
*/
loadManyBySQL(
fragment: SQLFragment<Pick<TFields, TSelectedFields>>,
modifiers: EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields> = {},
): AuthorizationResultBasedSQLQueryBuilder<
TFields,
TIDField,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
> {
return new AuthorizationResultBasedSQLQueryBuilder(
this.knexDataManager,
this.constructionUtils,
this.queryContext,
fragment,
modifiers,
);
}
/**
* Load a page of entities with Relay-style cursor pagination using a unified pagination specification.
* Only returns successfully authorized entities for cursor stability; failed authorization results are filtered out.
*
* @returns Connection with only successfully authorized entities
*/
async loadPageAsync(
args: EntityLoaderLoadPageArgs<TFields, TSelectedFields>,
): Promise<Connection<TEntity>> {
const pageResult = await this.knexDataManager.loadPageAsync(this.queryContext, args);
const edgeResults = await Promise.all(
pageResult.edges.map(async (edge) => {
const entityResult = await this.constructionUtils.constructAndAuthorizeEntityAsync(
edge.node,
);
if (!entityResult.ok) {
return null;
}
return {
...edge,
node: entityResult.value,
};
}),
);
const edges = edgeResults.filter((edge) => edge !== null);
const pageInfo: PageInfo = {
...pageResult.pageInfo,
startCursor: edges[0]?.cursor ?? null,
endCursor: edges[edges.length - 1]?.cursor ?? null,
};
return {
edges,
pageInfo,
};
}
/**
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
* @returns The pagination cursor for the given entity.
*/
getPaginationCursorForEntity(entity: TEntity): string {
return this.knexDataManager.getCursorForEntityID(entity.getID());
}
}
/**
* SQL query builder implementation for AuthorizationResultBasedKnexEntityLoader.
*/
export class AuthorizationResultBasedSQLQueryBuilder<
TFields extends Record<string, any>,
TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TIDField,
TViewerContext,
TEntity,
TSelectedFields
>,
TSelectedFields extends keyof TFields,
> extends BaseSQLQueryBuilder<TFields, TSelectedFields, Result<TEntity>> {
constructor(
private readonly knexDataManager: EntityKnexDataManager<TFields, TIDField>,
private readonly constructionUtils: EntityConstructionUtils<
TFields,
TIDField,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
>,
private readonly queryContext: EntityQueryContext,
sqlFragment: SQLFragment<Pick<TFields, TSelectedFields>>,
modifiers: EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields>,
) {
super(sqlFragment, modifiers);
}
/**
* Execute the query and return results.
*/
async executeInternalAsync(): Promise<readonly Result<TEntity>[]> {
const fieldObjects = await this.knexDataManager.loadManyBySQLFragmentAsync(
this.queryContext,
this.getSQLFragment(),
this.getModifiers(),
);
return await this.constructionUtils.constructAndAuthorizeEntitiesArrayAsync(fieldObjects);
}
}
@@ -0,0 +1,369 @@
import type { EntityQueryContext } from '@expo/entity';
import {
EntityDatabaseAdapter,
getDatabaseFieldForEntityField,
transformDatabaseObjectToFields,
} from '@expo/entity';
import type { Knex } from 'knex';
import type { SQLFragment } from './SQLOperator.ts';
/**
* Equality operand that is used for selecting entities with a field with a single value.
*/
export interface SingleValueFieldEqualityCondition<
TFields extends Record<string, any>,
N extends keyof TFields = keyof TFields,
> {
fieldName: N;
fieldValue: TFields[N];
}
/**
* Equality operand that is used for selecting entities with a field matching one of multiple values.
*/
export interface MultiValueFieldEqualityCondition<
TFields extends Record<string, any>,
N extends keyof TFields = keyof TFields,
> {
fieldName: N;
fieldValues: readonly TFields[N][];
}
/**
* A single equality operand for use in a selection clause.
* See EntityLoader.loadManyByFieldEqualityConjunctionAsync documentation for examples.
*/
export type FieldEqualityCondition<
TFields extends Record<string, any>,
N extends keyof TFields = keyof TFields,
> = SingleValueFieldEqualityCondition<TFields, N> | MultiValueFieldEqualityCondition<TFields, N>;
export function isSingleValueFieldEqualityCondition<
TFields extends Record<string, any>,
N extends keyof TFields = keyof TFields,
>(
condition: FieldEqualityCondition<TFields, N>,
): condition is SingleValueFieldEqualityCondition<TFields, N> {
return (condition as SingleValueFieldEqualityCondition<TFields, N>).fieldValue !== undefined;
}
export interface TableFieldSingleValueEqualityCondition {
tableField: string;
tableValue: any;
}
export interface TableFieldMultiValueEqualityCondition {
tableField: string;
tableValues: readonly any[];
}
export enum NullsOrdering {
FIRST = 'first',
LAST = 'last',
}
/**
* Ordering options for `orderBy` clauses.
*/
export enum OrderByOrdering {
/**
* Ascending order (lowest to highest).
* Ascending order puts smaller values first, where "smaller" is defined in terms of the %3C operator.
*/
ASCENDING = 'asc',
/**
* Descending order (highest to lowest).
* Descending order puts larger values first, where "larger" is defined in terms of the %3E operator.
*/
DESCENDING = 'desc',
}
export type PostgresOrderByClause<TFields extends Record<string, any>> =
| {
/**
* The field name to order by.
*/
fieldName: keyof TFields;
/**
* The OrderByOrdering to order by.
*/
order: OrderByOrdering;
/**
* Optional nulls ordering. If not provided, the database default is used
* (NULLS LAST for ASC, NULLS FIRST for DESC in PostgreSQL).
*/
nulls?: NullsOrdering | undefined;
}
| {
/**
* A raw SQL fragment to order by. May not contain ASC or DESC, as ordering direction is determined by the `order` property.
*/
fieldFragment: SQLFragment<TFields>;
/**
* The OrderByOrdering to order by.
*/
order: OrderByOrdering;
/**
* Optional nulls ordering. If not provided, the database default is used
* (NULLS LAST for ASC, NULLS FIRST for DESC in PostgreSQL).
*/
nulls?: NullsOrdering | undefined;
};
/**
* SQL modifiers that only affect the selection but not the projection.
*/
export interface PostgresQuerySelectionModifiers<TFields extends Record<string, any>> {
/**
* Order the entities by specified columns and orders.
*/
orderBy?: readonly PostgresOrderByClause<TFields>[];
/**
* Skip the specified number of entities queried before returning.
*/
offset?: number;
/**
* Limit the number of entities returned.
*/
limit?: number;
/**
* Lock the selected rows for update using `SELECT ... FOR UPDATE`. Mutually exclusive with `forShare`.
*/
forUpdate?: boolean;
/**
* Lock the selected rows in share mode using `SELECT ... FOR SHARE`. Mutually exclusive with `forUpdate`.
*/
forShare?: boolean;
/**
* Skip rows that are already locked by another transaction using `SKIP LOCKED`. Requires `forUpdate` or `forShare`.
*/
skipLocked?: boolean;
}
export type TableOrderByClause<TFields extends Record<string, any>> =
| {
columnName: string;
order: OrderByOrdering;
nulls: NullsOrdering | undefined;
}
| {
columnFragment: SQLFragment<TFields>;
order: OrderByOrdering;
nulls: NullsOrdering | undefined;
};
export interface TableQuerySelectionModifiers<TFields extends Record<string, any>> {
orderBy: TableOrderByClause<TFields>[] | undefined;
offset: number | undefined;
limit: number | undefined;
forUpdate: boolean | undefined;
forShare: boolean | undefined;
skipLocked: boolean | undefined;
}
export abstract class BasePostgresEntityDatabaseAdapter<
TFields extends Record<string, any>,
TIDField extends keyof TFields,
> extends EntityDatabaseAdapter<TFields, TIDField> {
/**
* Get the maximum page size for pagination.
* @returns maximum page size if configured, undefined otherwise
*/
get paginationMaxPageSize(): number | undefined {
return undefined;
}
/**
* Fetch many objects matching the conjunction of where clauses constructed from
* specified field equality operands.
*
* @param queryContext - query context with which to perform the fetch
* @param fieldEqualityOperands - list of field equality where clause operand specifications
* @param querySelectionModifiers - limit, offset, orderBy, and orderByRaw for the query
* @returns array of objects matching the query
*/
async fetchManyByFieldEqualityConjunctionAsync<N extends keyof TFields>(
queryContext: EntityQueryContext,
fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[],
querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>,
): Promise<readonly Readonly<TFields>[]> {
const tableFieldSingleValueOperands: TableFieldSingleValueEqualityCondition[] = [];
const tableFieldMultipleValueOperands: TableFieldMultiValueEqualityCondition[] = [];
for (const operand of fieldEqualityOperands) {
if (isSingleValueFieldEqualityCondition(operand)) {
tableFieldSingleValueOperands.push({
tableField: getDatabaseFieldForEntityField(this.entityConfiguration, operand.fieldName),
tableValue: operand.fieldValue,
});
} else {
tableFieldMultipleValueOperands.push({
tableField: getDatabaseFieldForEntityField(this.entityConfiguration, operand.fieldName),
tableValues: operand.fieldValues,
});
}
}
const results = await this.fetchManyByFieldEqualityConjunctionInternalAsync(
queryContext.getQueryInterface(),
this.entityConfiguration.tableName,
tableFieldSingleValueOperands,
tableFieldMultipleValueOperands,
this.convertToTableQueryModifiers(querySelectionModifiers),
);
return results.map((result) =>
transformDatabaseObjectToFields(this.entityConfiguration, this.fieldTransformerMap, result),
);
}
protected abstract fetchManyByFieldEqualityConjunctionInternalAsync(
queryInterface: Knex,
tableName: string,
tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
): Promise<object[]>;
/**
* Fetch many objects matching the SQL fragment.
*
* @param queryContext - query context with which to perform the fetch
* @param sqlFragment - SQLFragment for the WHERE clause of the query
* @param querySelectionModifiers - limit, offset, and orderByFragment for the query
* @returns array of objects matching the query
*/
async fetchManyBySQLFragmentAsync(
queryContext: EntityQueryContext,
sqlFragment: SQLFragment<TFields>,
querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>,
): Promise<readonly Readonly<TFields>[]> {
const results = await this.fetchManyBySQLFragmentInternalAsync(
queryContext.getQueryInterface(),
this.entityConfiguration.tableName,
sqlFragment,
this.convertToTableQueryModifiers(querySelectionModifiers),
);
return results.map((result) =>
transformDatabaseObjectToFields(this.entityConfiguration, this.fieldTransformerMap, result),
);
}
protected abstract fetchManyBySQLFragmentInternalAsync(
queryInterface: Knex,
tableName: string,
sqlFragment: SQLFragment<TFields>,
querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
): Promise<object[]>;
/**
* Count objects matching the conjunction of where clauses constructed from
* specified field equality operands.
*
* @param queryContext - query context with which to perform the count
* @param fieldEqualityOperands - list of field equality where clause operand specifications
* @returns count of objects matching the query
*/
async countByFieldEqualityConjunctionAsync<N extends keyof TFields>(
queryContext: EntityQueryContext,
fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[],
): Promise<number> {
const tableFieldSingleValueOperands: TableFieldSingleValueEqualityCondition[] = [];
const tableFieldMultipleValueOperands: TableFieldMultiValueEqualityCondition[] = [];
for (const operand of fieldEqualityOperands) {
if (isSingleValueFieldEqualityCondition(operand)) {
tableFieldSingleValueOperands.push({
tableField: getDatabaseFieldForEntityField(this.entityConfiguration, operand.fieldName),
tableValue: operand.fieldValue,
});
} else {
tableFieldMultipleValueOperands.push({
tableField: getDatabaseFieldForEntityField(this.entityConfiguration, operand.fieldName),
tableValues: operand.fieldValues,
});
}
}
return await this.countByFieldEqualityConjunctionInternalAsync(
queryContext.getQueryInterface(),
this.entityConfiguration.tableName,
tableFieldSingleValueOperands,
tableFieldMultipleValueOperands,
);
}
protected abstract countByFieldEqualityConjunctionInternalAsync(
queryInterface: Knex,
tableName: string,
tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
): Promise<number>;
/**
* Count objects matching the SQL fragment.
*
* @param queryContext - query context with which to perform the count
* @param sqlFragment - SQLFragment for the WHERE clause of the query
* @returns count of objects matching the query
*/
async countBySQLFragmentAsync(
queryContext: EntityQueryContext,
sqlFragment: SQLFragment<TFields>,
): Promise<number> {
return await this.countBySQLFragmentInternalAsync(
queryContext.getQueryInterface(),
this.entityConfiguration.tableName,
sqlFragment,
);
}
protected abstract countBySQLFragmentInternalAsync(
queryInterface: Knex,
tableName: string,
sqlFragment: SQLFragment<TFields>,
): Promise<number>;
private convertToTableQueryModifiers(
querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>,
): TableQuerySelectionModifiers<TFields> {
const orderBy = querySelectionModifiers.orderBy;
return {
orderBy:
orderBy !== undefined
? orderBy.map((orderBySpecification): TableOrderByClause<TFields> => {
if ('fieldName' in orderBySpecification) {
return {
columnName: getDatabaseFieldForEntityField(
this.entityConfiguration,
orderBySpecification.fieldName,
),
order: orderBySpecification.order,
nulls: orderBySpecification.nulls,
};
} else {
return {
columnFragment: orderBySpecification.fieldFragment,
order: orderBySpecification.order,
nulls: orderBySpecification.nulls,
};
}
})
: undefined,
offset: querySelectionModifiers.offset,
limit: querySelectionModifiers.limit,
forUpdate: querySelectionModifiers.forUpdate,
forShare: querySelectionModifiers.forShare,
skipLocked: querySelectionModifiers.skipLocked,
};
}
}
Loaded 100 of 403 files, more files were not shown because too many files have changed in this diff. Show more