mirror of
https://github.com/expo/entity.git
synced 2026-11-13 20:28:06 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b61341057b |
No files matched your search
@@ -1,38 +0,0 @@
|
||||
# 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".
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"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__/**" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"universe/node",
|
||||
"universe/shared/typescript-analysis"
|
||||
],
|
||||
"plugins": [
|
||||
"eslint-plugin-tsdoc"
|
||||
],
|
||||
"rules": {
|
||||
"no-restricted-imports": ["error",{ "paths": [".", "..", "../.."] }],
|
||||
"tsdoc/syntax": "warn",
|
||||
"no-console": "warn",
|
||||
"handle-callback-err": "off"
|
||||
},
|
||||
"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 +0,0 @@
|
||||
packages/entity-codemod/**/__testfixtures__/**
|
||||
@@ -1 +0,0 @@
|
||||
yarn.lock -diff linguist-generated
|
||||
@@ -0,0 +1,27 @@
|
||||
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: 12.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
|
||||
@@ -1,20 +0,0 @@
|
||||
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 }}
|
||||
+29
-45
@@ -2,53 +2,37 @@ name: tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
# renovate: datasource=github-releases depName=jdx/mise
|
||||
MISE_VERSION: v2026.5.13
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: ${{ env.MISE_VERSION }}
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
cache: yarn
|
||||
- run: mise run ci
|
||||
- uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
directory: ./coverage
|
||||
flags: unittest
|
||||
fail_ci_if_error: true
|
||||
- uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: ${{ env.MISE_VERSION }}
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.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
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.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
|
||||
+1
-13
@@ -23,24 +23,12 @@ 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/
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"sortImports": {
|
||||
"groups": [
|
||||
["builtin", "external"],
|
||||
["internal", "subpath"],
|
||||
["parent", "sibling", "index"],
|
||||
"unknown"
|
||||
],
|
||||
"ignoreCase": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"jsxBracketSameLine": true
|
||||
}
|
||||
Vendored
+2
-11
@@ -1,12 +1,3 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
npmMinimalAgeGate: 2d
|
||||
|
||||
npmPreapprovedPackages:
|
||||
- '@expo/*'
|
||||
- 'oxlint-config-universe'
|
||||
+206
-439
@@ -3,606 +3,373 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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
|
||||
|
||||
* 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
|
||||
* `@expo/entity`
|
||||
* Initial Version
|
||||
* `@expo/entity-cache-adapter-redis`
|
||||
* Initial Version
|
||||
* `@expo/entity-database-adapter-knex`
|
||||
* Initial Version
|
||||
@@ -4,23 +4,24 @@ Entity is a privacy-aware data layer for defining, caching, and authorizing acce
|
||||
|
||||
[](https://github.com/expo/entity/actions?query=workflow%3Atests)
|
||||
[](https://expo.github.io/entity/)
|
||||
[](https://codecov.io/gh/expo/entity)
|
||||
[](https://codecov.io/gh/expo/entity)
|
||||
[](https://www.npmjs.com/package/@expo/entity)
|
||||
[](https://www.npmjs.com/package/@expo/entity)
|
||||
|
||||
## Core Features
|
||||
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
## Getting Started
|
||||
|
||||
- [Example application](https://github.com/expo/entity/tree/main/packages/entity-example)
|
||||
- [Example application](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.
|
||||
@@ -54,7 +55,10 @@ The Entity framework solves this by adding an additional property to the system:
|
||||
|
||||
```typescript
|
||||
class PhotoPrivacyPolicy {
|
||||
readRules = [new AllowIfOwnerRule(), new AllowIfOrganizationPermissionRule()];
|
||||
const readRules = [
|
||||
new AllowIfOwnerRule(),
|
||||
new AllowIfOrganizationPermissionRule(),
|
||||
];
|
||||
}
|
||||
|
||||
// in the view, for example
|
||||
@@ -67,7 +71,6 @@ 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.
|
||||
@@ -75,24 +78,20 @@ 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`
|
||||
To release a new version, let's say `v1.0.0` for example:
|
||||
1. `git checkout -b bump-v1.0.0`
|
||||
1. `yarn lerna version minor -- --no-push --conventional-commits`
|
||||
1. `git push -u origin bump-v1.0.0`
|
||||
1. Create a PR from the `bump-v1.0.0` branch, wait for tests, and commit the PR using GitHub interface.
|
||||
1. `git checkout master && git pull`
|
||||
1. `git tag -d v1.0.0`
|
||||
1. `git tag v1.0.0`
|
||||
1. `git push origin v1.0.0`
|
||||
1. In GitHub release interface, create a new release from the tag, copy changelog changes to release description.
|
||||
1. `yarn clean && yarn tsc`
|
||||
1. In each public subpackage, run `npm publish` and enter 2FA token.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default {
|
||||
presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import unitConfig from './jest.config.js';
|
||||
|
||||
export default {
|
||||
...unitConfig,
|
||||
coverageDirectory: 'coverage-integration',
|
||||
testMatch: ['**/__integration-tests__/**/*-test.ts'],
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import { stripTypeScriptTypes } from 'node:module';
|
||||
|
||||
export function process(sourceText, sourcePath) {
|
||||
return {
|
||||
code: stripTypeScriptTypes(sourceText, {
|
||||
mode: 'transform',
|
||||
sourceMap: true,
|
||||
sourceUrl: sourcePath,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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'],
|
||||
};
|
||||
+5
-3
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"packages": ["packages/*"],
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"version": "0.64.0",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
"useWorkspaces": true,
|
||||
"version": "0.24.0"
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
[tools]
|
||||
node = "24.14.1"
|
||||
|
||||
[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",
|
||||
]
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"targetDefaults": {
|
||||
"tsc": {
|
||||
"dependsOn": ["^clean", "clean", "^tsc"]
|
||||
},
|
||||
"build": {
|
||||
"dependsOn": ["^clean", "clean", "^build"]
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": ["^tsc"]
|
||||
},
|
||||
"integration": {
|
||||
"dependsOn": ["^tsc"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
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-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',
|
||||
},
|
||||
},
|
||||
],
|
||||
ignorePatterns: [
|
||||
'packages/entity-codemod/**/__testfixtures__/**',
|
||||
'packages/*/build',
|
||||
'coverage',
|
||||
'coverage-integration',
|
||||
'doc',
|
||||
],
|
||||
});
|
||||
+42
-31
@@ -4,39 +4,50 @@
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"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"
|
||||
"tsc": "lerna run tsc",
|
||||
"clean": "lerna run clean",
|
||||
"lint": "lerna run lint",
|
||||
"lint-fix": "lerna run lint-fix",
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bases": "1.0.24",
|
||||
"@types/node": "24.12.4",
|
||||
"ctix": "2.8.0",
|
||||
"jest": "30.3.0",
|
||||
"lerna": "9.0.7",
|
||||
"oxfmt": "0.50.0",
|
||||
"oxlint": "1.60.0",
|
||||
"oxlint-config-universe": "0.0.3",
|
||||
"oxlint-tsgolint": "0.20.0",
|
||||
"prettier": "3.8.3",
|
||||
"typedoc": "0.28.19",
|
||||
"typescript": "6.0.3"
|
||||
"@expo/entity": "file:packages/entity",
|
||||
"@expo/results": "^1.0.0",
|
||||
"@types/lru-cache": "^5.1.1",
|
||||
"@types/invariant": "^2.2.33",
|
||||
"@types/ioredis": "^4.26.4",
|
||||
"@types/jest": "^26.0.10",
|
||||
"@types/jsbn": "^1.2.29",
|
||||
"@types/node": "^14.6.0",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.14.0",
|
||||
"@typescript-eslint/parser": "^4.14.0",
|
||||
"barrelsby": "^2.2.0",
|
||||
"eslint": "^7.7.0",
|
||||
"eslint-config-universe": "^7.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.11",
|
||||
"jest": "^26.6.3",
|
||||
"lerna": "^4.0.0",
|
||||
"nullthrows": "^1.1.1",
|
||||
"pg": "^8.6.0",
|
||||
"prettier": "^2.3.2",
|
||||
"ts-jest": "~26.4.4",
|
||||
"ts-mockito": "^2.6.1",
|
||||
"typedoc": "^0.21.9",
|
||||
"typescript": "^4.4.2",
|
||||
"uuid": "^8.3.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"collect-v8-coverage": "^1.0.3"
|
||||
},
|
||||
"packageManager": "[email protected]+sha512.5c20ba010c99815433e5c8453112165e673f1c7948d8d2b267f4b5e52097538658388ebc9f9580656d9b75c5cc996f990f611f99304a2197d4c56d21eea370e7"
|
||||
"dependencies": {
|
||||
"@expo/entity": "file:packages/entity",
|
||||
"@expo/entity-cache-adapter-local-memory": "file:packages/entity-cache-adapter-local-memory",
|
||||
"@expo/entity-cache-adapter-redis": "file:packages/entity-cache-adapter-redis",
|
||||
"@expo/entity-database-adapter-knex": "file:packages/entity-database-adapter-knex",
|
||||
"@expo/entity-secondary-cache-redis": "file:packages/entity-secondary-cache-redis",
|
||||
"@expo/entity-ip-address-field": "file:packages/entity-ip-address-field"
|
||||
}
|
||||
}
|
||||
@@ -3,210 +3,9 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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))
|
||||
* add local memory cache package ([#158](https://github.com/expo/entity/issues/158)) ([85d3575](https://github.com/expo/entity/commit/85d35752fa0919a62efb776a762a6829e83ca7af))
|
||||
@@ -3,26 +3,21 @@
|
||||
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
|
||||
Because this is an in-memory cache, cross-box invalidation is not possible. Do not use this cache
|
||||
if you have the following use cases:
|
||||
|
||||
- The objects stored are mutable
|
||||
- The objects stored have high mutability
|
||||
- Reading a stale object from the cache is not acceptable in your application
|
||||
- Cross-machine invalidation is not possible
|
||||
- Cross-box 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
|
||||
- The objects stored are mostly immutable
|
||||
- You have a low TTL setting in your cache
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -1,40 +1,38 @@
|
||||
{
|
||||
"name": "@expo/entity-cache-adapter-local-memory",
|
||||
"version": "0.64.0",
|
||||
"version": "0.24.0",
|
||||
"description": "Cross-request local memory 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",
|
||||
"lint-fix": "eslint src --fix",
|
||||
"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": ">=12"
|
||||
},
|
||||
"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)"
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@expo/entity": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/entity": "workspace:^"
|
||||
"lru-cache": "^7.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/entity-testing-utils": "workspace:^",
|
||||
"@isaacs/ttlcache": "2.1.4",
|
||||
"@jest/globals": "30.3.0",
|
||||
"typescript": "6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"@expo/entity": "^0.24.0"
|
||||
}
|
||||
}
|
||||
@@ -1,71 +1,109 @@
|
||||
import type {
|
||||
CacheLoadResult,
|
||||
EntityConfiguration,
|
||||
IEntityGenericCacher,
|
||||
IEntityLoadKey,
|
||||
IEntityLoadValue,
|
||||
} from '@expo/entity';
|
||||
import { CacheStatus } from '@expo/entity';
|
||||
import { CacheLoadResult, CacheStatus, IEntityGenericCacher } from '@expo/entity';
|
||||
import LRUCache from 'lru-cache';
|
||||
|
||||
// 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 LocalMemoryCacheValue<TFields> = Readonly<TFields> | typeof DOES_NOT_EXIST_LOCAL_MEMORY_CACHE;
|
||||
export type LocalMemoryCache<TFields> = LRUCache<string, LocalMemoryCacheValue<TFields>>;
|
||||
|
||||
/**
|
||||
* 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>> {
|
||||
type LRUCacheOptionsV7<K, V> = {
|
||||
/**
|
||||
* Gets a value from the cache for specified key.
|
||||
* @param key - key to get
|
||||
* @returns the cached value, or undefined if not present
|
||||
* the number of most recently used items to keep.
|
||||
* note that we may store fewer items than this if maxSize is hit.
|
||||
*/
|
||||
get(key: string): LocalMemoryCacheValue<TFields> | undefined;
|
||||
max: number;
|
||||
|
||||
/**
|
||||
* Sets a value in the cache for specified key.
|
||||
* @param key - key to set
|
||||
* @param value - value to set
|
||||
* if you wish to track item size, you must provide a maxSize
|
||||
* note that we still will only keep up to max *actual items*,
|
||||
* so size tracking may cause fewer than max items to be stored.
|
||||
* At the extreme, a single item of maxSize size will cause everything
|
||||
* else in the cache to be dropped when it is added. Use with caution!
|
||||
* Note also that size tracking can negatively impact performance,
|
||||
* though for most cases, only minimally.
|
||||
*/
|
||||
set(key: string, value: LocalMemoryCacheValue<TFields>): void;
|
||||
|
||||
maxSize?: number;
|
||||
|
||||
/**
|
||||
* Deletes a value from the cache for specified key.
|
||||
* @param key - key to delete
|
||||
* function to calculate size of items. useful if storing strings or
|
||||
* buffers or other items where memory size depends on the object itself.
|
||||
* also note that oversized items do NOT immediately get dropped from
|
||||
* the cache, though they will cause faster turnover in the storage.
|
||||
*/
|
||||
delete(key: string): void;
|
||||
}
|
||||
sizeCalculation?: (value: V, key: K) => number;
|
||||
|
||||
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>,
|
||||
) {}
|
||||
/**
|
||||
* function to call when the item is removed from the cache
|
||||
* Note that using this can negatively impact performance.
|
||||
*/
|
||||
dispose?: (value: V, key: K) => void;
|
||||
|
||||
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 {},
|
||||
/**
|
||||
* max time to live for items before they are considered stale
|
||||
* note that stale items are NOT preemptively removed by default,
|
||||
* and MAY live in the cache, contributing to its LRU max, long after
|
||||
* they have expired.
|
||||
* Also, as this cache is optimized for LRU/MRU operations, some of
|
||||
* the staleness/TTL checks will reduce performance, as they will incur
|
||||
* overhead by deleting items.
|
||||
* Must be a positive integer in ms, defaults to 0, which means "no TTL"
|
||||
*/
|
||||
ttl?: number;
|
||||
|
||||
/**
|
||||
* return stale items from cache.get() before disposing of them
|
||||
* boolean, default false
|
||||
*/
|
||||
allowStale?: boolean;
|
||||
|
||||
/**
|
||||
* update the age of items on cache.get(), renewing their TTL
|
||||
* boolean, default false
|
||||
*/
|
||||
updateAgeOnGet?: boolean;
|
||||
|
||||
/**
|
||||
* update the age of items on cache.has(), renewing their TTL
|
||||
* boolean, default false
|
||||
*/
|
||||
updateAgeOnHas?: boolean;
|
||||
|
||||
/**
|
||||
* update the "recently-used"-ness of items on cache.has()
|
||||
* boolean, default false
|
||||
*/
|
||||
updateRecencyOnHas?: boolean;
|
||||
};
|
||||
|
||||
export default class GenericLocalMemoryCacher<TFields> implements IEntityGenericCacher<TFields> {
|
||||
constructor(private readonly localMemoryCache: LocalMemoryCache<TFields>) {}
|
||||
|
||||
static createLRUCache<TFields>(
|
||||
options: { maxSize?: number; ttlSeconds?: number } = {}
|
||||
): LocalMemoryCache<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;
|
||||
const lruCacheOptions: LRUCacheOptionsV7<string, TFields> = {
|
||||
max: options.maxSize ?? DEFAULT_LRU_CACHE_SIZE,
|
||||
sizeCalculation: (value: LocalMemoryCacheValue<TFields>) =>
|
||||
value === DOES_NOT_EXIST_LOCAL_MEMORY_CACHE ? 0 : 1,
|
||||
ttl: maxAgeSeconds * 1000, // convert to ms
|
||||
};
|
||||
return new LRUCache<string, LocalMemoryCacheValue<TFields>>(lruCacheOptions as any);
|
||||
}
|
||||
|
||||
static createNoOpCache<TFields>(): LocalMemoryCache<TFields> {
|
||||
return new LRUCache<string, LocalMemoryCacheValue<TFields>>({
|
||||
max: 0,
|
||||
maxAge: -1,
|
||||
});
|
||||
}
|
||||
|
||||
public async loadManyAsync(
|
||||
keys: readonly string[],
|
||||
keys: readonly string[]
|
||||
): Promise<ReadonlyMap<string, CacheLoadResult<TFields>>> {
|
||||
const cacheResults = new Map<string, CacheLoadResult<TFields>>();
|
||||
for (const key of keys) {
|
||||
@@ -102,38 +140,15 @@ export class GenericLocalMemoryCacher<
|
||||
|
||||
public async invalidateManyAsync(keys: readonly string[]): Promise<void> {
|
||||
for (const key of keys) {
|
||||
this.localMemoryCache.delete(key);
|
||||
this.localMemoryCache.del(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,
|
||||
];
|
||||
|
||||
public makeCacheKey(parts: string[]): string {
|
||||
const delimiter = ':';
|
||||
const escapedParts = parts.map((part) =>
|
||||
part.replaceAll('\\', '\\\\').replaceAll(delimiter, `\\${delimiter}`),
|
||||
part.replace('\\', '\\\\').replace(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,80 @@
|
||||
import { EntityCacheAdapter, CacheLoadResult, EntityConfiguration, mapKeys } from '@expo/entity';
|
||||
import invariant from 'invariant';
|
||||
|
||||
import GenericLocalMemoryCacher, { LocalMemoryCache } from './GenericLocalMemoryCacher';
|
||||
|
||||
export default class LocalMemoryCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
|
||||
private readonly genericLocalMemoryCacher: GenericLocalMemoryCacher<TFields>;
|
||||
|
||||
constructor(
|
||||
entityConfiguration: EntityConfiguration<TFields>,
|
||||
localMemoryCache: LocalMemoryCache<TFields>
|
||||
) {
|
||||
super(entityConfiguration);
|
||||
this.genericLocalMemoryCacher = new GenericLocalMemoryCacher(localMemoryCache);
|
||||
}
|
||||
|
||||
public async loadManyAsync<N extends keyof TFields>(
|
||||
fieldName: N,
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult<TFields>>> {
|
||||
const localMemoryCacheKeyToFieldValueMapping = new Map(
|
||||
fieldValues.map((fieldValue) => [this.makeCacheKey(fieldName, fieldValue), fieldValue])
|
||||
);
|
||||
const cacheResults = await this.genericLocalMemoryCacher.loadManyAsync(
|
||||
Array.from(localMemoryCacheKeyToFieldValueMapping.keys())
|
||||
);
|
||||
|
||||
return mapKeys(cacheResults, (cacheKey) => {
|
||||
const fieldValue = localMemoryCacheKeyToFieldValueMapping.get(cacheKey);
|
||||
invariant(
|
||||
fieldValue !== undefined,
|
||||
'Unspecified cache key %s returned from generic local memory cacher',
|
||||
cacheKey
|
||||
);
|
||||
return fieldValue;
|
||||
});
|
||||
}
|
||||
|
||||
public async cacheManyAsync<N extends keyof TFields>(
|
||||
fieldName: N,
|
||||
objectMap: ReadonlyMap<NonNullable<TFields[N]>, Readonly<TFields>>
|
||||
): Promise<void> {
|
||||
await this.genericLocalMemoryCacher.cacheManyAsync(
|
||||
mapKeys(objectMap, (fieldValue) => this.makeCacheKey(fieldName, fieldValue))
|
||||
);
|
||||
}
|
||||
|
||||
public async cacheDBMissesAsync<N extends keyof TFields>(
|
||||
fieldName: N,
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
): Promise<void> {
|
||||
await this.genericLocalMemoryCacher.cacheDBMissesAsync(
|
||||
fieldValues.map((fieldValue) => this.makeCacheKey(fieldName, fieldValue))
|
||||
);
|
||||
}
|
||||
|
||||
public async invalidateManyAsync<N extends keyof TFields>(
|
||||
fieldName: N,
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
): Promise<void> {
|
||||
await this.genericLocalMemoryCacher.invalidateManyAsync(
|
||||
fieldValues.map((fieldValue) => this.makeCacheKey(fieldName, fieldValue))
|
||||
);
|
||||
}
|
||||
|
||||
private makeCacheKey<N extends keyof TFields>(
|
||||
fieldName: N,
|
||||
fieldValue: NonNullable<TFields[N]>
|
||||
): string {
|
||||
const columnName = this.entityConfiguration.entityToDBFieldsKeyMapping.get(fieldName);
|
||||
invariant(columnName, `database field mapping missing for ${fieldName}`);
|
||||
const parts = [
|
||||
this.entityConfiguration.tableName,
|
||||
`${this.entityConfiguration.cacheKeyVersion}`,
|
||||
columnName,
|
||||
String(fieldValue),
|
||||
];
|
||||
return this.genericLocalMemoryCacher.makeCacheKey(parts);
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,45 @@
|
||||
import type {
|
||||
import {
|
||||
computeIfAbsent,
|
||||
EntityCacheAdapter,
|
||||
EntityConfiguration,
|
||||
IEntityCacheAdapter,
|
||||
IEntityCacheAdapterProvider,
|
||||
} from '@expo/entity';
|
||||
import { computeIfAbsent, GenericEntityCacheAdapter } from '@expo/entity';
|
||||
|
||||
import type { ILocalMemoryCache } from './GenericLocalMemoryCacher.ts';
|
||||
import { GenericLocalMemoryCacher } from './GenericLocalMemoryCacher.ts';
|
||||
import GenericLocalMemoryCacher, { LocalMemoryCache } from './GenericLocalMemoryCacher';
|
||||
import LocalMemoryCacheAdapter from './LocalMemoryCacheAdapter';
|
||||
|
||||
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>(),
|
||||
export default class LocalMemoryCacheAdapterProvider implements IEntityCacheAdapterProvider {
|
||||
// local memory cache adapters should be shared/reused across requests
|
||||
static localMemoryCacheAdapterMap = new Map<string, LocalMemoryCacheAdapter<any>>();
|
||||
|
||||
static getNoOpProvider(): IEntityCacheAdapterProvider {
|
||||
return new LocalMemoryCacheAdapterProvider(<TFields>() =>
|
||||
GenericLocalMemoryCacher.createNoOpCache<TFields>()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a local memory cache adapter provider configured with the supplied local memory cache creator function.
|
||||
*/
|
||||
static createProviderWithCacheCreator(
|
||||
cacheCreator: LocalMemoryCacheCreator,
|
||||
static getProvider(
|
||||
options: { maxSize?: number; ttlSeconds?: number } = {}
|
||||
): IEntityCacheAdapterProvider {
|
||||
return new LocalMemoryCacheAdapterProvider(cacheCreator);
|
||||
return new LocalMemoryCacheAdapterProvider(<TFields>() =>
|
||||
GenericLocalMemoryCacher.createLRUCache<TFields>(options)
|
||||
);
|
||||
}
|
||||
|
||||
private readonly localMemoryCacheAdapterMap = new Map<
|
||||
string,
|
||||
GenericEntityCacheAdapter<any, any>
|
||||
>();
|
||||
private constructor(
|
||||
private readonly localMemoryCacheCreator: <TFields>() => LocalMemoryCache<TFields>
|
||||
) {}
|
||||
|
||||
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),
|
||||
);
|
||||
});
|
||||
public getCacheAdapter<TFields>(
|
||||
entityConfiguration: EntityConfiguration<TFields>
|
||||
): EntityCacheAdapter<TFields> {
|
||||
return computeIfAbsent(
|
||||
LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap,
|
||||
entityConfiguration.tableName,
|
||||
() => {
|
||||
const localMemoryCache = this.localMemoryCacheCreator<TFields>();
|
||||
return new LocalMemoryCacheAdapter(entityConfiguration, localMemoryCache);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
import { CacheStatus, ViewerContext } from '@expo/entity';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import GenericLocalMemoryCacher from '../GenericLocalMemoryCacher';
|
||||
import LocalMemoryCacheAdapter from '../LocalMemoryCacheAdapter';
|
||||
import { LocalMemoryCacheAdapterProvider } from '../LocalMemoryCacheAdapterProvider';
|
||||
import LocalMemoryTestEntity from '../testfixtures/LocalMemoryTestEntity';
|
||||
import {
|
||||
createLocalMemoryIntegrationTestEntityCompanionProvider,
|
||||
createNoopLocalMemoryIntegrationTestEntityCompanionProvider,
|
||||
} from '../testfixtures/createLocalMemoryIntegrationTestEntityCompanionProvider';
|
||||
|
||||
class TestViewerContext extends ViewerContext {}
|
||||
|
||||
describe(LocalMemoryCacheAdapter, () => {
|
||||
beforeEach(async () => {
|
||||
LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap.clear();
|
||||
});
|
||||
|
||||
it('has correct caching behavior', async () => {
|
||||
const viewerContext = new TestViewerContext(
|
||||
createLocalMemoryIntegrationTestEntityCompanionProvider()
|
||||
);
|
||||
const cacheAdapter = viewerContext.entityCompanionProvider.getCompanionForEntity(
|
||||
LocalMemoryTestEntity,
|
||||
LocalMemoryTestEntity.getCompanionDefinition()
|
||||
)['tableDataCoordinator']['cacheAdapter'];
|
||||
const cacheKeyMaker = cacheAdapter['makeCacheKey'].bind(cacheAdapter);
|
||||
|
||||
const date = new Date();
|
||||
const entity1Created = await LocalMemoryTestEntity.creator(viewerContext)
|
||||
.setField('name', 'blah')
|
||||
.setField('dateField', date)
|
||||
.enforceCreateAsync();
|
||||
|
||||
// loading an entity should put it in cache
|
||||
const entity1 = await LocalMemoryTestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByIDAsync(entity1Created.getID());
|
||||
|
||||
const entitySpecificGenericCacher =
|
||||
LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap.get(
|
||||
LocalMemoryTestEntity.getCompanionDefinition().entityConfiguration.tableName
|
||||
)!['genericLocalMemoryCacher'];
|
||||
const cachedResult = await entitySpecificGenericCacher.loadManyAsync([
|
||||
cacheKeyMaker('id', entity1.getID()),
|
||||
]);
|
||||
const cachedValue = cachedResult.get(cacheKeyMaker('id', 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.loader(viewerContext).loadByIDAsync(
|
||||
nonExistentId
|
||||
);
|
||||
expect(entityNonExistentResult.ok).toBe(false);
|
||||
|
||||
const nonExistentCachedResult = await entitySpecificGenericCacher.loadManyAsync([
|
||||
cacheKeyMaker('id', nonExistentId),
|
||||
]);
|
||||
expect(nonExistentCachedResult.get(cacheKeyMaker('id', nonExistentId))).toMatchObject({
|
||||
status: CacheStatus.NEGATIVE,
|
||||
});
|
||||
|
||||
// load again through entities framework to ensure it reads negative result
|
||||
const entityNonExistentResult2 = await LocalMemoryTestEntity.loader(
|
||||
viewerContext
|
||||
).loadByIDAsync(nonExistentId);
|
||||
expect(entityNonExistentResult2.ok).toBe(false);
|
||||
|
||||
// invalidate from cache to ensure it invalidates correctly
|
||||
await LocalMemoryTestEntity.loader(viewerContext).invalidateFieldsAsync(entity1.getAllFields());
|
||||
const cachedResultMiss = await entitySpecificGenericCacher.loadManyAsync([
|
||||
cacheKeyMaker('id', entity1.getID()),
|
||||
]);
|
||||
const cachedValueMiss = cachedResultMiss.get(cacheKeyMaker('id', entity1.getID()));
|
||||
expect(cachedValueMiss).toMatchObject({ status: CacheStatus.MISS });
|
||||
});
|
||||
|
||||
it('shares the cache between different requests', async () => {
|
||||
const genericLocalMemoryCacherLoadManySpy = jest.spyOn(
|
||||
GenericLocalMemoryCacher.prototype as unknown as any,
|
||||
'loadManyAsync'
|
||||
);
|
||||
const viewerContext = new TestViewerContext(
|
||||
createLocalMemoryIntegrationTestEntityCompanionProvider()
|
||||
);
|
||||
|
||||
const date = new Date();
|
||||
const entity1Created = await LocalMemoryTestEntity.creator(viewerContext)
|
||||
.setField('name', 'blah')
|
||||
.setField('dateField', date)
|
||||
.enforceCreateAsync();
|
||||
|
||||
// loading an entity should put it in cache
|
||||
await LocalMemoryTestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByIDAsync(entity1Created.getID());
|
||||
|
||||
// load entity with a different request
|
||||
const viewerContext2 = new TestViewerContext(
|
||||
createLocalMemoryIntegrationTestEntityCompanionProvider()
|
||||
);
|
||||
const entity1WithVc2 = await LocalMemoryTestEntity.loader(viewerContext2)
|
||||
.enforcing()
|
||||
.loadByIDAsync(entity1Created.getID());
|
||||
|
||||
const cacheAdapter = viewerContext.entityCompanionProvider.getCompanionForEntity(
|
||||
LocalMemoryTestEntity,
|
||||
LocalMemoryTestEntity.getCompanionDefinition()
|
||||
)['tableDataCoordinator']['cacheAdapter'];
|
||||
const cacheKeyMaker = cacheAdapter['makeCacheKey'].bind(cacheAdapter);
|
||||
expect(entity1WithVc2.getAllFields()).toMatchObject({
|
||||
id: entity1WithVc2.getID(),
|
||||
name: 'blah',
|
||||
dateField: date,
|
||||
});
|
||||
expect(genericLocalMemoryCacherLoadManySpy).toBeCalledWith([
|
||||
cacheKeyMaker('id', entity1WithVc2.getID()),
|
||||
]);
|
||||
expect(genericLocalMemoryCacherLoadManySpy).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('respects the parameters of a noop cache', async () => {
|
||||
const viewerContext = new TestViewerContext(
|
||||
createNoopLocalMemoryIntegrationTestEntityCompanionProvider()
|
||||
);
|
||||
const cacheAdapter = viewerContext.entityCompanionProvider.getCompanionForEntity(
|
||||
LocalMemoryTestEntity,
|
||||
LocalMemoryTestEntity.getCompanionDefinition()
|
||||
)['tableDataCoordinator']['cacheAdapter'];
|
||||
const cacheKeyMaker = cacheAdapter['makeCacheKey'].bind(cacheAdapter);
|
||||
|
||||
const date = new Date();
|
||||
const entity1Created = await LocalMemoryTestEntity.creator(viewerContext)
|
||||
.setField('name', 'blah')
|
||||
.setField('dateField', date)
|
||||
.enforceCreateAsync();
|
||||
|
||||
// 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)
|
||||
.enforcing()
|
||||
.loadByIDAsync(entity1Created.getID());
|
||||
|
||||
const entitySpecificGenericCacher =
|
||||
LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap.get(
|
||||
LocalMemoryTestEntity.getCompanionDefinition().entityConfiguration.tableName
|
||||
)!['genericLocalMemoryCacher'];
|
||||
const cachedResult = await entitySpecificGenericCacher.loadManyAsync([
|
||||
cacheKeyMaker('id', entity1.getID()),
|
||||
]);
|
||||
const cachedValue = cachedResult.get(cacheKeyMaker('id', 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.loader(viewerContext).loadByIDAsync(
|
||||
nonExistentId
|
||||
);
|
||||
expect(entityNonExistentResult.ok).toBe(false);
|
||||
|
||||
const nonExistentCachedResult = await entitySpecificGenericCacher.loadManyAsync([
|
||||
cacheKeyMaker('id', nonExistentId),
|
||||
]);
|
||||
expect(nonExistentCachedResult.get(cacheKeyMaker('id', nonExistentId))).toMatchObject({
|
||||
status: CacheStatus.MISS,
|
||||
});
|
||||
});
|
||||
});
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
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,
|
||||
);
|
||||
};
|
||||
-178
@@ -1,178 +0,0 @@
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
-170
@@ -1,170 +0,0 @@
|
||||
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'>[],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { CacheStatus, UUIDField, EntityConfiguration } from '@expo/entity';
|
||||
|
||||
import GenericLocalMemoryCacher, {
|
||||
DOES_NOT_EXIST_LOCAL_MEMORY_CACHE,
|
||||
} from '../GenericLocalMemoryCacher';
|
||||
import LocalMemoryCacheAdapter from '../LocalMemoryCacheAdapter';
|
||||
|
||||
type BlahFields = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const entityConfiguration = new EntityConfiguration<BlahFields>({
|
||||
idField: 'id',
|
||||
tableName: 'blah',
|
||||
schema: {
|
||||
id: new UUIDField({ columnName: 'id', cache: true }),
|
||||
},
|
||||
databaseAdapterFlavor: 'postgres',
|
||||
cacheAdapterFlavor: 'local-memory',
|
||||
});
|
||||
|
||||
describe(LocalMemoryCacheAdapter, () => {
|
||||
describe('loadManyAsync', () => {
|
||||
it('returns appropriate cache results', async () => {
|
||||
const cacheAdapter = new LocalMemoryCacheAdapter(
|
||||
entityConfiguration,
|
||||
GenericLocalMemoryCacher.createLRUCache({
|
||||
maxSize: Number.MAX_SAFE_INTEGER,
|
||||
ttlSeconds: Number.MAX_SAFE_INTEGER,
|
||||
})
|
||||
);
|
||||
|
||||
const cacheHits = new Map<string, Readonly<BlahFields>>([['test-id-1', { id: 'test-id-1' }]]);
|
||||
await cacheAdapter.cacheManyAsync('id', cacheHits);
|
||||
await cacheAdapter.cacheDBMissesAsync('id', ['test-id-2']);
|
||||
|
||||
const results = await cacheAdapter.loadManyAsync('id', [
|
||||
'test-id-1',
|
||||
'test-id-2',
|
||||
'test-id-3',
|
||||
]);
|
||||
|
||||
expect(results.get('test-id-1')).toMatchObject({
|
||||
status: CacheStatus.HIT,
|
||||
item: { id: 'test-id-1' },
|
||||
});
|
||||
expect(results.get('test-id-2')).toMatchObject({ status: CacheStatus.NEGATIVE });
|
||||
expect(results.get('test-id-3')).toMatchObject({ status: CacheStatus.MISS });
|
||||
expect(results.size).toBe(3);
|
||||
});
|
||||
|
||||
it('returns empty map when passed empty array of fieldValues', async () => {
|
||||
const cacheAdapter = new LocalMemoryCacheAdapter(
|
||||
entityConfiguration,
|
||||
GenericLocalMemoryCacher.createLRUCache({
|
||||
maxSize: Number.MAX_SAFE_INTEGER,
|
||||
ttlSeconds: Number.MAX_SAFE_INTEGER,
|
||||
})
|
||||
);
|
||||
const results = await cacheAdapter.loadManyAsync('id', []);
|
||||
expect(results).toEqual(new Map());
|
||||
});
|
||||
});
|
||||
|
||||
describe('cacheManyAsync', () => {
|
||||
it('correctly caches all objects', async () => {
|
||||
const localMemoryCache = GenericLocalMemoryCacher.createLRUCache<BlahFields>({
|
||||
maxSize: Number.MAX_SAFE_INTEGER,
|
||||
ttlSeconds: Number.MAX_SAFE_INTEGER,
|
||||
});
|
||||
|
||||
const cacheAdapter = new LocalMemoryCacheAdapter(entityConfiguration, localMemoryCache);
|
||||
await cacheAdapter.cacheManyAsync('id', new Map([['test-id-1', { id: 'test-id-1' }]]));
|
||||
|
||||
const cacheKey = cacheAdapter['makeCacheKey']('id', 'test-id-1');
|
||||
expect(localMemoryCache.get(cacheKey)).toMatchObject({
|
||||
id: 'test-id-1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cacheDBMissesAsync', () => {
|
||||
it('correctly caches misses', async () => {
|
||||
const localMemoryCache = GenericLocalMemoryCacher.createLRUCache<BlahFields>({
|
||||
maxSize: Number.MAX_SAFE_INTEGER,
|
||||
ttlSeconds: Number.MAX_SAFE_INTEGER,
|
||||
});
|
||||
|
||||
const cacheAdapter = new LocalMemoryCacheAdapter(entityConfiguration, localMemoryCache);
|
||||
await cacheAdapter.cacheDBMissesAsync('id', ['test-id-1']);
|
||||
|
||||
const cacheKey = cacheAdapter['makeCacheKey']('id', 'test-id-1');
|
||||
expect(localMemoryCache.get(cacheKey)).toEqual(DOES_NOT_EXIST_LOCAL_MEMORY_CACHE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalidateManyAsync', () => {
|
||||
it('invalidates correctly', async () => {
|
||||
const localMemoryCache = GenericLocalMemoryCacher.createLRUCache<BlahFields>({
|
||||
maxSize: Number.MAX_SAFE_INTEGER,
|
||||
ttlSeconds: Number.MAX_SAFE_INTEGER,
|
||||
});
|
||||
|
||||
const cacheAdapter = new LocalMemoryCacheAdapter(entityConfiguration, localMemoryCache);
|
||||
await cacheAdapter.cacheManyAsync('id', new Map([['test-id-1', { id: 'test-id-1' }]]));
|
||||
await cacheAdapter.cacheDBMissesAsync('id', ['test-id-2']);
|
||||
await cacheAdapter.invalidateManyAsync('id', ['test-id-1', 'test-id-2']);
|
||||
|
||||
const results = await cacheAdapter.loadManyAsync('id', ['test-id-1', 'test-id-2']);
|
||||
expect(results.get('test-id-1')).toMatchObject({ status: CacheStatus.MISS });
|
||||
expect(results.get('test-id-2')).toMatchObject({ status: CacheStatus.MISS });
|
||||
});
|
||||
|
||||
it('returns when passed empty array of fieldValues', async () => {
|
||||
const cacheAdapter = new LocalMemoryCacheAdapter(
|
||||
entityConfiguration,
|
||||
GenericLocalMemoryCacher.createLRUCache<BlahFields>({
|
||||
maxSize: Number.MAX_SAFE_INTEGER,
|
||||
ttlSeconds: Number.MAX_SAFE_INTEGER,
|
||||
})
|
||||
);
|
||||
await cacheAdapter.invalidateManyAsync('id', []);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,5 +4,7 @@
|
||||
* @module @expo/entity-cache-adapter-local-memory
|
||||
*/
|
||||
|
||||
export * from './GenericLocalMemoryCacher.ts';
|
||||
export * from './LocalMemoryCacheAdapterProvider.ts';
|
||||
export { default as LocalMemoryCacheAdapter } from './LocalMemoryCacheAdapter';
|
||||
export { default as LocalMemoryCacheAdapterProvider } from './LocalMemoryCacheAdapterProvider';
|
||||
export { default as GenericLocalMemoryCacher } from './GenericLocalMemoryCacher';
|
||||
export * from './GenericLocalMemoryCacher';
|
||||
+41
-40
@@ -1,12 +1,13 @@
|
||||
import type { EntityCompanionDefinition, ViewerContext } from '@expo/entity';
|
||||
import {
|
||||
AlwaysAllowPrivacyPolicyRule,
|
||||
DateField,
|
||||
Entity,
|
||||
EntityConfiguration,
|
||||
EntityPrivacyPolicy,
|
||||
StringField,
|
||||
ViewerContext,
|
||||
UUIDField,
|
||||
DateField,
|
||||
StringField,
|
||||
EntityConfiguration,
|
||||
EntityCompanionDefinition,
|
||||
Entity,
|
||||
} from '@expo/entity';
|
||||
|
||||
export type LocalMemoryTestEntityFields = {
|
||||
@@ -15,36 +16,32 @@ export type LocalMemoryTestEntityFields = {
|
||||
dateField: Date | null;
|
||||
};
|
||||
|
||||
export class LocalMemoryTestEntity extends Entity<
|
||||
export default class LocalMemoryTestEntity extends Entity<
|
||||
LocalMemoryTestEntityFields,
|
||||
'id',
|
||||
string,
|
||||
ViewerContext
|
||||
> {
|
||||
static defineCompanionDefinition(): EntityCompanionDefinition<
|
||||
static getCompanionDefinition(): EntityCompanionDefinition<
|
||||
LocalMemoryTestEntityFields,
|
||||
'id',
|
||||
string,
|
||||
ViewerContext,
|
||||
LocalMemoryTestEntity,
|
||||
LocalMemoryTestEntityPrivacyPolicy
|
||||
> {
|
||||
return {
|
||||
entityClass: LocalMemoryTestEntity,
|
||||
entityConfiguration: localMemoryTestEntityConfiguration,
|
||||
privacyPolicyClass: LocalMemoryTestEntityPrivacyPolicy,
|
||||
};
|
||||
return localMemoryTestEntityCompanionDefinition;
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalMemoryTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
LocalMemoryTestEntityFields,
|
||||
'id',
|
||||
string,
|
||||
ViewerContext,
|
||||
LocalMemoryTestEntity
|
||||
> {
|
||||
protected override readonly createRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<
|
||||
LocalMemoryTestEntityFields,
|
||||
'id',
|
||||
string,
|
||||
ViewerContext,
|
||||
LocalMemoryTestEntity
|
||||
>(),
|
||||
@@ -52,7 +49,7 @@ export class LocalMemoryTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
protected override readonly readRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<
|
||||
LocalMemoryTestEntityFields,
|
||||
'id',
|
||||
string,
|
||||
ViewerContext,
|
||||
LocalMemoryTestEntity
|
||||
>(),
|
||||
@@ -60,7 +57,7 @@ export class LocalMemoryTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
protected override readonly updateRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<
|
||||
LocalMemoryTestEntityFields,
|
||||
'id',
|
||||
string,
|
||||
ViewerContext,
|
||||
LocalMemoryTestEntity
|
||||
>(),
|
||||
@@ -68,32 +65,36 @@ export class LocalMemoryTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
protected override readonly deleteRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<
|
||||
LocalMemoryTestEntityFields,
|
||||
'id',
|
||||
string,
|
||||
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',
|
||||
export const localMemoryTestEntityConfiguration =
|
||||
new EntityConfiguration<LocalMemoryTestEntityFields>({
|
||||
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',
|
||||
});
|
||||
|
||||
const localMemoryTestEntityCompanionDefinition = new EntityCompanionDefinition({
|
||||
entityClass: LocalMemoryTestEntity,
|
||||
entityConfiguration: localMemoryTestEntityConfiguration,
|
||||
privacyPolicyClass: LocalMemoryTestEntityPrivacyPolicy,
|
||||
});
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
NoOpEntityMetricsAdapter,
|
||||
IEntityMetricsAdapter,
|
||||
EntityCompanionProvider,
|
||||
StubQueryContextProvider,
|
||||
StubDatabaseAdapterProvider,
|
||||
} from '@expo/entity';
|
||||
|
||||
import { LocalMemoryCacheAdapterProvider } from '../LocalMemoryCacheAdapterProvider';
|
||||
|
||||
export const createLocalMemoryIntegrationTestEntityCompanionProvider = (
|
||||
localMemoryOptions: { maxSize?: number; ttlSeconds?: number } = {},
|
||||
metricsAdapter: IEntityMetricsAdapter = new NoOpEntityMetricsAdapter()
|
||||
): EntityCompanionProvider => {
|
||||
const localMemoryCacheAdapterProvider =
|
||||
localMemoryOptions.maxSize === 0 && localMemoryOptions.ttlSeconds === 0
|
||||
? LocalMemoryCacheAdapterProvider.getNoOpProvider()
|
||||
: LocalMemoryCacheAdapterProvider.getProvider(localMemoryOptions);
|
||||
return new EntityCompanionProvider(
|
||||
metricsAdapter,
|
||||
new Map([
|
||||
[
|
||||
'postgres',
|
||||
{
|
||||
adapterProvider: new StubDatabaseAdapterProvider(),
|
||||
queryContextProvider: StubQueryContextProvider,
|
||||
},
|
||||
],
|
||||
]),
|
||||
new Map([
|
||||
[
|
||||
'local-memory',
|
||||
{
|
||||
cacheAdapterProvider: localMemoryCacheAdapterProvider,
|
||||
},
|
||||
],
|
||||
])
|
||||
);
|
||||
};
|
||||
|
||||
export const createNoopLocalMemoryIntegrationTestEntityCompanionProvider = (
|
||||
metricsAdapter: IEntityMetricsAdapter = new NoOpEntityMetricsAdapter()
|
||||
): EntityCompanionProvider => {
|
||||
return createLocalMemoryIntegrationTestEntityCompanionProvider(
|
||||
{ maxSize: 0, ttlSeconds: 0 },
|
||||
metricsAdapter
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["src/**/*"],
|
||||
"references": [{ "path": "../entity" }, { "path": "../entity-testing-utils" }]
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "build",
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -3,337 +3,234 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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 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))
|
||||
* 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))
|
||||
* support entity fields subset of db fields ([#49](https://github.com/expo/entity/issues/49)) ([4e40b2e](https://github.com/expo/entity/commit/4e40b2e521407e521d236978ec3b3b56db3990be))
|
||||
@@ -11,10 +11,17 @@ During `EntityCompanionProvider` instantiation:
|
||||
```typescript
|
||||
import Redis from 'ioredis';
|
||||
|
||||
const genericRedisCacherContext = {
|
||||
const redisCacheAdapterContext = {
|
||||
redisClient: new Redis(new URL(process.env['REDIS_URL']!).toString()),
|
||||
cacheKeyDelimiter: ':',
|
||||
makeKeyFn(...parts: string[]): string {
|
||||
const delimiter = ':';
|
||||
const escapedParts = parts.map((part) =>
|
||||
part.replace('\\', '\\\\').replace(delimiter, `\\${delimiter}`)
|
||||
);
|
||||
return escapedParts.join(delimiter);
|
||||
},
|
||||
cacheKeyPrefix: 'ent-',
|
||||
cacheKeyVersion: 1,
|
||||
ttlSecondsPositive: 86400, // 1 day
|
||||
ttlSecondsNegative: 600, // 10 minutes
|
||||
};
|
||||
@@ -29,9 +36,9 @@ export const createDefaultEntityCompanionProvider = (
|
||||
},
|
||||
{
|
||||
['redis']: {
|
||||
cacheAdapterProvider: new RedisCacheAdapterProvider(genericRedisCacheContext),
|
||||
cacheAdapterProvider: new RedisCacheAdapterProvider(redisCacheAdapterContext),
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
```
|
||||
```
|
||||
@@ -1,45 +1,38 @@
|
||||
{
|
||||
"name": "@expo/entity-cache-adapter-redis",
|
||||
"version": "0.64.0",
|
||||
"version": "0.24.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",
|
||||
"lint-fix": "eslint src --fix",
|
||||
"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": ">=12"
|
||||
},
|
||||
"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)"
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@expo/entity": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/entity": "workspace:^"
|
||||
"ioredis": "^4.27.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/batcher": "1.0.0",
|
||||
"@expo/entity-testing-utils": "workspace:^",
|
||||
"@jest/globals": "30.3.0",
|
||||
"ioredis": "5.10.1",
|
||||
"ts-mockito": "2.6.1",
|
||||
"typescript": "6.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ioredis": ">=5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"@expo/entity": "^0.24.0"
|
||||
}
|
||||
}
|
||||
@@ -1,127 +1,25 @@
|
||||
import type {
|
||||
CacheLoadResult,
|
||||
EntityConfiguration,
|
||||
IEntityGenericCacher,
|
||||
IEntityLoadKey,
|
||||
IEntityLoadValue,
|
||||
} from '@expo/entity';
|
||||
import {
|
||||
CacheLoadResult,
|
||||
CacheStatus,
|
||||
EntityConfiguration,
|
||||
transformCacheObjectToFields,
|
||||
transformFieldsToCacheObject,
|
||||
IEntityGenericCacher,
|
||||
} from '@expo/entity';
|
||||
import { Redis } from 'ioredis';
|
||||
|
||||
import { redisTransformerMap } from './RedisCommon.ts';
|
||||
import { wrapNativeRedisCallAsync } from './errors/wrapNativeRedisCallAsync.ts';
|
||||
import { getSurroundingCacheKeyVersionsForInvalidation } from './utils/getSurroundingCacheKeyVersionsForInvalidation.ts';
|
||||
import { redisTransformerMap } from './RedisCommon';
|
||||
import wrapNativeRedisCallAsync from './errors/wrapNativeRedisCallAsync';
|
||||
|
||||
// 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;
|
||||
redisClient: Redis;
|
||||
|
||||
/**
|
||||
* TTL for caching database hits. Successive entity loads within this TTL
|
||||
@@ -134,33 +32,23 @@ export interface GenericRedisCacheContext {
|
||||
* 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> {
|
||||
export default class GenericRedisCacher<TFields> implements IEntityGenericCacher<TFields> {
|
||||
constructor(
|
||||
private readonly context: GenericRedisCacheContext,
|
||||
private readonly entityConfiguration: EntityConfiguration<TFields, TIDField>,
|
||||
) {
|
||||
validateGenericRedisCacheContext(context);
|
||||
}
|
||||
private readonly entityConfiguration: EntityConfiguration<TFields>
|
||||
) {}
|
||||
|
||||
public async loadManyAsync(
|
||||
keys: readonly string[],
|
||||
keys: readonly string[]
|
||||
): Promise<ReadonlyMap<string, CacheLoadResult<TFields>>> {
|
||||
if (keys.length === 0) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const redisResults = await wrapNativeRedisCallAsync(() =>
|
||||
this.context.redisClient.mget(...keys),
|
||||
this.context.redisClient.mget(...keys)
|
||||
);
|
||||
|
||||
const results = new Map<string, CacheLoadResult<TFields>>();
|
||||
@@ -178,7 +66,7 @@ export class GenericRedisCacher<
|
||||
item: transformCacheObjectToFields(
|
||||
this.entityConfiguration,
|
||||
redisTransformerMap,
|
||||
JSON.parse(redisResult),
|
||||
JSON.parse(redisResult)
|
||||
),
|
||||
});
|
||||
} else {
|
||||
@@ -200,10 +88,10 @@ export class GenericRedisCacher<
|
||||
redisTransaction = redisTransaction.set(
|
||||
key,
|
||||
JSON.stringify(
|
||||
transformFieldsToCacheObject(this.entityConfiguration, redisTransformerMap, object),
|
||||
transformFieldsToCacheObject(this.entityConfiguration, redisTransformerMap, object)
|
||||
),
|
||||
'EX',
|
||||
this.context.ttlSecondsPositive,
|
||||
this.context.ttlSecondsPositive
|
||||
);
|
||||
});
|
||||
await wrapNativeRedisCallAsync(() => redisTransaction.exec());
|
||||
@@ -220,7 +108,7 @@ export class GenericRedisCacher<
|
||||
key,
|
||||
DOES_NOT_EXIST_REDIS,
|
||||
'EX',
|
||||
this.context.ttlSecondsNegative,
|
||||
this.context.ttlSecondsNegative
|
||||
);
|
||||
});
|
||||
await wrapNativeRedisCallAsync(() => redisTransaction.exec());
|
||||
@@ -233,68 +121,4 @@ export class GenericRedisCacher<
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { EntityCacheAdapter, EntityConfiguration, CacheLoadResult, mapKeys } from '@expo/entity';
|
||||
import invariant from 'invariant';
|
||||
import { Redis } from 'ioredis';
|
||||
|
||||
import GenericRedisCacher from './GenericRedisCacher';
|
||||
|
||||
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> {
|
||||
private readonly genericRedisCacher: GenericRedisCacher<TFields>;
|
||||
constructor(
|
||||
entityConfiguration: EntityConfiguration<TFields>,
|
||||
private readonly context: RedisCacheAdapterContext
|
||||
) {
|
||||
super(entityConfiguration);
|
||||
|
||||
this.genericRedisCacher = new GenericRedisCacher(
|
||||
{
|
||||
redisClient: context.redisClient,
|
||||
ttlSecondsNegative: context.ttlSecondsNegative,
|
||||
ttlSecondsPositive: context.ttlSecondsPositive,
|
||||
},
|
||||
entityConfiguration
|
||||
);
|
||||
}
|
||||
|
||||
public async loadManyAsync<N extends keyof TFields>(
|
||||
fieldName: N,
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult<TFields>>> {
|
||||
const redisCacheKeyToFieldValueMapping = new Map(
|
||||
fieldValues.map((fieldValue) => [this.makeCacheKey(fieldName, fieldValue), fieldValue])
|
||||
);
|
||||
const cacheResults = await this.genericRedisCacher.loadManyAsync(
|
||||
Array.from(redisCacheKeyToFieldValueMapping.keys())
|
||||
);
|
||||
|
||||
return mapKeys(cacheResults, (redisCacheKey) => {
|
||||
const fieldValue = redisCacheKeyToFieldValueMapping.get(redisCacheKey);
|
||||
invariant(
|
||||
fieldValue !== undefined,
|
||||
'Unspecified cache key %s returned from generic Redis cacher',
|
||||
redisCacheKey
|
||||
);
|
||||
return fieldValue;
|
||||
});
|
||||
}
|
||||
|
||||
public async cacheManyAsync<N extends keyof TFields>(
|
||||
fieldName: N,
|
||||
objectMap: ReadonlyMap<NonNullable<TFields[N]>, Readonly<TFields>>
|
||||
): Promise<void> {
|
||||
await this.genericRedisCacher.cacheManyAsync(
|
||||
mapKeys(objectMap, (fieldValue) => this.makeCacheKey(fieldName, fieldValue))
|
||||
);
|
||||
}
|
||||
|
||||
public async cacheDBMissesAsync<N extends keyof TFields>(
|
||||
fieldName: N,
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
): Promise<void> {
|
||||
await this.genericRedisCacher.cacheDBMissesAsync(
|
||||
fieldValues.map((fieldValue) => this.makeCacheKey(fieldName, fieldValue))
|
||||
);
|
||||
}
|
||||
|
||||
public async invalidateManyAsync<N extends keyof TFields>(
|
||||
fieldName: N,
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
): Promise<void> {
|
||||
await this.genericRedisCacher.invalidateManyAsync(
|
||||
fieldValues.map((fieldValue) => this.makeCacheKey(fieldName, fieldValue))
|
||||
);
|
||||
}
|
||||
|
||||
private makeCacheKey<N extends keyof TFields>(
|
||||
fieldName: N,
|
||||
fieldValue: NonNullable<TFields[N]>
|
||||
): string {
|
||||
const columnName = this.entityConfiguration.entityToDBFieldsKeyMapping.get(fieldName);
|
||||
invariant(columnName, `database field mapping missing for ${fieldName}`);
|
||||
return this.context.makeKeyFn(
|
||||
this.context.cacheKeyPrefix,
|
||||
this.entityConfiguration.tableName,
|
||||
`v2.${this.entityConfiguration.cacheKeyVersion}`,
|
||||
columnName,
|
||||
String(fieldValue)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,13 @@
|
||||
import type {
|
||||
EntityConfiguration,
|
||||
IEntityCacheAdapter,
|
||||
IEntityCacheAdapterProvider,
|
||||
} from '@expo/entity';
|
||||
import { GenericEntityCacheAdapter } from '@expo/entity';
|
||||
import { IEntityCacheAdapterProvider, EntityConfiguration, EntityCacheAdapter } from '@expo/entity';
|
||||
|
||||
import type { GenericRedisCacheContext } from './GenericRedisCacher.ts';
|
||||
import { GenericRedisCacher } from './GenericRedisCacher.ts';
|
||||
import RedisCacheAdapter, { RedisCacheAdapterContext } from './RedisCacheAdapter';
|
||||
|
||||
export class RedisCacheAdapterProvider implements IEntityCacheAdapterProvider {
|
||||
constructor(private readonly context: GenericRedisCacheContext) {}
|
||||
export default class RedisCacheAdapterProvider implements IEntityCacheAdapterProvider {
|
||||
constructor(private readonly context: RedisCacheAdapterContext) {}
|
||||
|
||||
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));
|
||||
getCacheAdapter<TFields>(
|
||||
entityConfiguration: EntityConfiguration<TFields>
|
||||
): EntityCacheAdapter<TFields> {
|
||||
return new RedisCacheAdapter(entityConfiguration, this.context);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BufferField, DateField } from '@expo/entity';
|
||||
import { DateField } from '@expo/entity';
|
||||
|
||||
export const redisTransformerMap = new Map([
|
||||
[
|
||||
@@ -22,25 +22,4 @@ 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),
|
||||
},
|
||||
],
|
||||
]);
|
||||
-180
@@ -1,180 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
-228
@@ -1,228 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
+42
-34
@@ -1,64 +1,68 @@
|
||||
import { CacheStatus, ViewerContext } from '@expo/entity';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from '@jest/globals';
|
||||
import { Redis } from 'ioredis';
|
||||
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,
|
||||
import GenericRedisCacher from '../GenericRedisCacher';
|
||||
import { RedisCacheAdapterContext } from '../RedisCacheAdapter';
|
||||
import RedisTestEntity, {
|
||||
redisTestEntityConfiguration,
|
||||
} from '../__testfixtures__/RedisTestEntity.ts';
|
||||
import { createRedisIntegrationTestEntityCompanionProvider } from '../__testfixtures__/createRedisIntegrationTestEntityCompanionProvider.ts';
|
||||
RedisTestEntityFields,
|
||||
} from '../testfixtures/RedisTestEntity';
|
||||
import { createRedisIntegrationTestEntityCompanionProvider } from '../testfixtures/createRedisIntegrationTestEntityCompanionProvider';
|
||||
|
||||
class TestViewerContext extends ViewerContext {}
|
||||
|
||||
describe(GenericRedisCacher, () => {
|
||||
let genericRedisCacheContext: GenericRedisCacheContext;
|
||||
let redisCacheAdapterContext: RedisCacheAdapterContext;
|
||||
|
||||
beforeAll(() => {
|
||||
genericRedisCacheContext = {
|
||||
redisCacheAdapterContext = {
|
||||
redisClient: new Redis(new URL(process.env['REDIS_URL']!).toString()),
|
||||
cacheKeyDelimiter: ':',
|
||||
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
|
||||
invalidationConfig: {
|
||||
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await (genericRedisCacheContext.redisClient as Redis).flushdb();
|
||||
await redisCacheAdapterContext.redisClient.flushdb();
|
||||
});
|
||||
afterAll(async () => {
|
||||
(genericRedisCacheContext.redisClient as Redis).disconnect();
|
||||
redisCacheAdapterContext.redisClient.disconnect();
|
||||
});
|
||||
|
||||
it('has correct caching and loading behavior', async () => {
|
||||
const viewerContext = new TestViewerContext(
|
||||
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
|
||||
createRedisIntegrationTestEntityCompanionProvider(redisCacheAdapterContext)
|
||||
);
|
||||
const genericRedisCacher = new GenericRedisCacher(
|
||||
genericRedisCacheContext,
|
||||
redisTestEntityConfiguration,
|
||||
{
|
||||
redisClient: redisCacheAdapterContext.redisClient,
|
||||
ttlSecondsNegative: redisCacheAdapterContext.ttlSecondsNegative,
|
||||
ttlSecondsPositive: redisCacheAdapterContext.ttlSecondsPositive,
|
||||
},
|
||||
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();
|
||||
.enforceCreateAsync();
|
||||
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 cachedJSON = await redisCacheAdapterContext.redisClient.get(testKey);
|
||||
const cachedValue = JSON.parse(cachedJSON!);
|
||||
expect(cachedValue).toMatchObject({
|
||||
id: entity1Created.getID(),
|
||||
@@ -73,11 +77,14 @@ describe(GenericRedisCacher, () => {
|
||||
});
|
||||
expect(loadedObjectMap.size).toBe(1);
|
||||
});
|
||||
|
||||
it('has correct negative caching behaviour', async () => {
|
||||
const genericRedisCacher = new GenericRedisCacher(
|
||||
genericRedisCacheContext,
|
||||
redisTestEntityConfiguration,
|
||||
{
|
||||
redisClient: redisCacheAdapterContext.redisClient,
|
||||
ttlSecondsNegative: redisCacheAdapterContext.ttlSecondsNegative,
|
||||
ttlSecondsPositive: redisCacheAdapterContext.ttlSecondsPositive,
|
||||
},
|
||||
redisTestEntityConfiguration
|
||||
);
|
||||
|
||||
const testKey = `test-id-key-non-existent-id`;
|
||||
@@ -86,22 +93,23 @@ describe(GenericRedisCacher, () => {
|
||||
const cacheLoadResult = loadedObjectMap.get(testKey)!;
|
||||
expect(cacheLoadResult.status).toBe(CacheStatus.NEGATIVE);
|
||||
});
|
||||
|
||||
it('has correct invalidation behaviour', async () => {
|
||||
const viewerContext = new TestViewerContext(
|
||||
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
|
||||
createRedisIntegrationTestEntityCompanionProvider(redisCacheAdapterContext)
|
||||
);
|
||||
const genericRedisCacher = new GenericRedisCacher(
|
||||
genericRedisCacheContext,
|
||||
redisTestEntityConfiguration,
|
||||
{
|
||||
redisClient: redisCacheAdapterContext.redisClient,
|
||||
ttlSecondsNegative: redisCacheAdapterContext.ttlSecondsNegative,
|
||||
ttlSecondsPositive: redisCacheAdapterContext.ttlSecondsPositive,
|
||||
},
|
||||
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();
|
||||
.enforceCreateAsync();
|
||||
const testKey = `test-id-key-${entity1Created.getID()}`;
|
||||
const objectMap = new Map<string, Readonly<RedisTestEntityFields>>([
|
||||
[testKey, entity1Created.getAllFields()],
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
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 viewerContext = new TestViewerContext(
|
||||
createRedisIntegrationTestEntityCompanionProvider(redisCacheAdapterContext)
|
||||
);
|
||||
const cacheAdapter = viewerContext.entityCompanionProvider.getCompanionForEntity(
|
||||
RedisTestEntity,
|
||||
RedisTestEntity.getCompanionDefinition()
|
||||
)['tableDataCoordinator']['cacheAdapter'];
|
||||
const cacheKeyMaker = cacheAdapter['makeCacheKey'].bind(cacheAdapter);
|
||||
|
||||
const entity1Created = await RedisTestEntity.creator(viewerContext)
|
||||
.setField('name', 'blah')
|
||||
.enforceCreateAsync();
|
||||
|
||||
// loading an entity should put it in cache
|
||||
const entity1 = await RedisTestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByIDAsync(entity1Created.getID());
|
||||
|
||||
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(viewerContext).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(viewerContext).loadByIDAsync(
|
||||
nonExistentId
|
||||
);
|
||||
expect(entityNonExistentResult2.ok).toBe(false);
|
||||
|
||||
// invalidate from cache to ensure it invalidates correctly
|
||||
await RedisTestEntity.loader(viewerContext).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 viewerContext = new TestViewerContext(
|
||||
createRedisIntegrationTestEntityCompanionProvider(redisCacheAdapterContext)
|
||||
);
|
||||
const date = new Date();
|
||||
const entity1 = await enforceAsyncResult(
|
||||
RedisTestEntity.creator(viewerContext).setField('dateField', date).createAsync()
|
||||
);
|
||||
expect(entity1.getField('dateField')).toEqual(date);
|
||||
|
||||
const entity2 = await RedisTestEntity.loader(viewerContext)
|
||||
.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);
|
||||
});
|
||||
|
||||
it('caches and restores empty string field keys', async () => {
|
||||
const viewerContext = new TestViewerContext(
|
||||
createRedisIntegrationTestEntityCompanionProvider(redisCacheAdapterContext)
|
||||
);
|
||||
const entity1 = await enforceAsyncResult(
|
||||
RedisTestEntity.creator(viewerContext).setField('name', '').createAsync()
|
||||
);
|
||||
const entity2 = await RedisTestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('name', '');
|
||||
expect(entity2?.getID()).toEqual(entity1.getID());
|
||||
|
||||
// simulate new request
|
||||
const vc2 = new TestViewerContext(
|
||||
createRedisIntegrationTestEntityCompanionProvider(redisCacheAdapterContext)
|
||||
);
|
||||
const entity3 = await RedisTestEntity.loader(vc2)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('name', '');
|
||||
expect(entity3?.getID()).toEqual(entity1.getID());
|
||||
});
|
||||
});
|
||||
@@ -1,45 +1,46 @@
|
||||
import { EntityCacheAdapterTransientError, ViewerContext } from '@expo/entity';
|
||||
import { beforeAll, beforeEach, describe, expect, it } from '@jest/globals';
|
||||
import { Redis } from 'ioredis';
|
||||
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';
|
||||
import RedisCacheAdapter, { RedisCacheAdapterContext } from '../RedisCacheAdapter';
|
||||
import RedisTestEntity from '../testfixtures/RedisTestEntity';
|
||||
import { createRedisIntegrationTestEntityCompanionProvider } from '../testfixtures/createRedisIntegrationTestEntityCompanionProvider';
|
||||
|
||||
class TestViewerContext extends ViewerContext {}
|
||||
|
||||
describe(GenericRedisCacher, () => {
|
||||
const redisClient = new Redis(new URL(process.env['REDIS_URL']!).toString());
|
||||
let genericRedisCacheContext: GenericRedisCacheContext;
|
||||
describe(RedisCacheAdapter, () => {
|
||||
let redisCacheAdapterContext: RedisCacheAdapterContext;
|
||||
|
||||
beforeAll(() => {
|
||||
genericRedisCacheContext = {
|
||||
redisClient,
|
||||
cacheKeyDelimiter: ':',
|
||||
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
|
||||
invalidationConfig: {
|
||||
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await redisClient.flushdb();
|
||||
await redisCacheAdapterContext.redisClient.flushdb();
|
||||
});
|
||||
|
||||
it('throws when redis is disconnected', async () => {
|
||||
redisClient.disconnect();
|
||||
redisCacheAdapterContext.redisClient.disconnect();
|
||||
|
||||
const vc1 = new TestViewerContext(
|
||||
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
|
||||
createRedisIntegrationTestEntityCompanionProvider(redisCacheAdapterContext)
|
||||
);
|
||||
|
||||
await expect(
|
||||
RedisTestEntity.creator(vc1).setField('name', 'blah').createAsync(),
|
||||
RedisTestEntity.creator(vc1).setField('name', 'blah').enforceCreateAsync()
|
||||
).rejects.toThrow(EntityCacheAdapterTransientError);
|
||||
});
|
||||
});
|
||||
@@ -1,83 +0,0 @@
|
||||
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 },
|
||||
],
|
||||
});
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
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),
|
||||
},
|
||||
],
|
||||
]),
|
||||
);
|
||||
};
|
||||
@@ -1,428 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
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 }),
|
||||
},
|
||||
databaseAdapterFlavor: 'postgres',
|
||||
cacheAdapterFlavor: 'redis',
|
||||
});
|
||||
|
||||
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', []);
|
||||
});
|
||||
});
|
||||
});
|
||||
+2
-3
@@ -1,12 +1,11 @@
|
||||
import { EntityCacheAdapterTransientError } from '@expo/entity';
|
||||
import { describe, expect, it } from '@jest/globals';
|
||||
|
||||
import { wrapNativeRedisCallAsync } from '../wrapNativeRedisCallAsync.ts';
|
||||
import wrapNativeRedisCallAsync from '../wrapNativeRedisCallAsync';
|
||||
|
||||
describe(wrapNativeRedisCallAsync, () => {
|
||||
it('rethrows literals', async () => {
|
||||
const throwingFn = async (): Promise<void> => {
|
||||
// eslint-disable-next-line no-throw-literal,@typescript-eslint/only-throw-error
|
||||
// eslint-disable-next-line no-throw-literal,@typescript-eslint/no-throw-literal
|
||||
throw 'hello';
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EntityCacheAdapterTransientError } from '@expo/entity';
|
||||
|
||||
export async function wrapNativeRedisCallAsync<T>(fn: () => Promise<T>): Promise<T> {
|
||||
export default async function wrapNativeRedisCallAsync<T>(fn: () => Promise<T>): Promise<T> {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
* @module @expo/entity-cache-adapter-redis
|
||||
*/
|
||||
|
||||
export * from './GenericRedisCacher.ts';
|
||||
export * from './RedisCacheAdapterProvider.ts';
|
||||
export * from './RedisCommon.ts';
|
||||
export * from './errors/wrapNativeRedisCallAsync.ts';
|
||||
export * from './utils/getSurroundingCacheKeyVersionsForInvalidation.ts';
|
||||
export { default as RedisCacheAdapter } from './RedisCacheAdapter';
|
||||
export * from './RedisCacheAdapter';
|
||||
export { default as RedisCacheAdapterProvider } from './RedisCacheAdapterProvider';
|
||||
export * from './RedisCommon';
|
||||
export { default as GenericRedisCacher } from './GenericRedisCacher';
|
||||
export * from './GenericRedisCacher';
|
||||
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
AlwaysAllowPrivacyPolicyRule,
|
||||
EntityPrivacyPolicy,
|
||||
ViewerContext,
|
||||
UUIDField,
|
||||
DateField,
|
||||
StringField,
|
||||
EntityConfiguration,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export class RedisTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
RedisTestEntityFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
RedisTestEntity
|
||||
> {
|
||||
protected override readonly createRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<
|
||||
RedisTestEntityFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
RedisTestEntity
|
||||
>(),
|
||||
];
|
||||
protected override readonly readRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<
|
||||
RedisTestEntityFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
RedisTestEntity
|
||||
>(),
|
||||
];
|
||||
protected override readonly updateRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<
|
||||
RedisTestEntityFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
RedisTestEntity
|
||||
>(),
|
||||
];
|
||||
protected override 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',
|
||||
cache: true,
|
||||
}),
|
||||
dateField: new DateField({
|
||||
columnName: 'date_field',
|
||||
}),
|
||||
},
|
||||
databaseAdapterFlavor: 'postgres',
|
||||
cacheAdapterFlavor: 'redis',
|
||||
});
|
||||
|
||||
const redisTestEntityCompanionDefinition = new EntityCompanionDefinition({
|
||||
entityClass: RedisTestEntity,
|
||||
entityConfiguration: redisTestEntityConfiguration,
|
||||
privacyPolicyClass: RedisTestEntityPrivacyPolicy,
|
||||
});
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
NoOpEntityMetricsAdapter,
|
||||
IEntityMetricsAdapter,
|
||||
EntityCompanionProvider,
|
||||
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,
|
||||
new Map([
|
||||
[
|
||||
'postgres',
|
||||
{
|
||||
adapterProvider: new StubDatabaseAdapterProvider(),
|
||||
queryContextProvider: StubQueryContextProvider,
|
||||
},
|
||||
],
|
||||
]),
|
||||
new Map([
|
||||
[
|
||||
'redis',
|
||||
{
|
||||
cacheAdapterProvider: new RedisCacheAdapterProvider(redisCacheAdapterContext),
|
||||
},
|
||||
],
|
||||
])
|
||||
);
|
||||
};
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
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]);
|
||||
});
|
||||
});
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
export function getSurroundingCacheKeyVersionsForInvalidation(
|
||||
cacheKeyVersion: number,
|
||||
): readonly number[] {
|
||||
return [
|
||||
...(cacheKeyVersion === 0 ? [] : [cacheKeyVersion - 1]),
|
||||
cacheKeyVersion,
|
||||
cacheKeyVersion + 1,
|
||||
];
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["src/**/*"],
|
||||
"references": [{ "path": "../entity" }, { "path": "../entity-testing-utils" }]
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "build",
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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))
|
||||
@@ -1,27 +0,0 @@
|
||||
# @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
|
||||
```
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"name": "@expo/entity-codemod",
|
||||
"version": "0.64.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.3.0",
|
||||
"@types/jscodeshift": "17.3.0",
|
||||
"@types/node": "24.12.4",
|
||||
"jscodeshift": "17.3.0",
|
||||
"typescript": "6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
-79
@@ -1,79 +0,0 @@
|
||||
/* 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
|
||||
)
|
||||
}
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
/* 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
|
||||
)
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
/* eslint-disable */
|
||||
|
||||
async function testAsync(viewerContext: ViewerContext): Promise<void> {
|
||||
const testTrigger = await TestTriggerEntity.createAsync(actorViewerContext, undefined, {
|
||||
appId,
|
||||
});
|
||||
|
||||
await TestTriggerEntity.enforceDeleteAsync(testTrigger);
|
||||
return testTrigger;
|
||||
};
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
/* 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;
|
||||
};
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
/* eslint-disable */
|
||||
|
||||
async function anotherCase(updater: any): Promise<any> {
|
||||
return await updater.enforceUpdateAsync();
|
||||
}
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
/* eslint-disable */
|
||||
|
||||
async function anotherCase(updater: any): Promise<any> {
|
||||
return await updater.enforceUpdateAsync();
|
||||
}
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
/* 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
|
||||
)
|
||||
}
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
/* 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
|
||||
)
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
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;
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
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;
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
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');
|
||||
};
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
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');
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
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`),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
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();
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
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();
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
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();
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["**/__testfixtures__/**"]
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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))
|
||||
@@ -1,3 +0,0 @@
|
||||
# @expo/entity-database-adapter-knex-testing-utils
|
||||
|
||||
Testing utilities for applications using Entity with Knex database adapter.
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"name": "@expo/entity-database-adapter-knex-testing-utils",
|
||||
"version": "0.64.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.3.0",
|
||||
"@types/invariant": "2.2.37",
|
||||
"@types/node": "24.12.4",
|
||||
"typescript": "6.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@jest/globals": "*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
-316
@@ -1,316 +0,0 @@
|
||||
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 },
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
-130
@@ -1,130 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
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]);
|
||||
});
|
||||
-1035
File diff suppressed because it is too large.
Load diff
-16
@@ -1,16 +0,0 @@
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
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(),
|
||||
},
|
||||
],
|
||||
]),
|
||||
);
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
/* 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';
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["src/**/*"],
|
||||
"references": [{ "path": "../entity" }, { "path": "../entity-database-adapter-knex" }]
|
||||
}
|
||||
@@ -3,398 +3,270 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* support entity fields subset of db fields ([#49](https://github.com/expo/entity/issues/49)) ([4e40b2e](https://github.com/expo/entity/commit/4e40b2e521407e521d236978ec3b3b56db3990be))
|
||||
Loaded 100 of 406 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user