mirror of
https://github.com/expo/entity.git
synced 2026-11-14 09:58:09 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2f09aff71 |
No files matched your search
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"directory": "./src",
|
||||
"location": "top",
|
||||
"exclude": ["tests__", "testfixtures"],
|
||||
"singleQuotes": true,
|
||||
"exportDefault": true,
|
||||
"delete": true
|
||||
}
|
||||
@@ -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,47 @@
|
||||
module.exports = {
|
||||
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'],
|
||||
},
|
||||
// async functions must have names ending with 'Async'
|
||||
{
|
||||
selector: ['function', 'classMethod', 'typeMethod'],
|
||||
format: ['camelCase'],
|
||||
modifiers: ['async'],
|
||||
suffix: ['Async'],
|
||||
},
|
||||
],
|
||||
'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,23 @@
|
||||
name: docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: yarn
|
||||
node-version-file: package.json
|
||||
- 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 }}
|
||||
+25
-45
@@ -2,53 +2,33 @@ name: tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
# renovate: datasource=github-releases depName=jdx/mise
|
||||
MISE_VERSION: v2026.10.19
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: ${{ env.MISE_VERSION }}
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
cache: yarn
|
||||
- run: mise run ci
|
||||
- uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
directory: ./coverage
|
||||
flags: unittest
|
||||
fail_ci_if_error: true
|
||||
- uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
directory: ./coverage-integration
|
||||
flags: integration
|
||||
fail_ci_if_error: true
|
||||
docs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: ${{ env.MISE_VERSION }}
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
cache: yarn
|
||||
- run: mise run typedoc
|
||||
- name: Deploy
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./doc
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: yarn
|
||||
node-version-file: package.json
|
||||
- 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
|
||||
@@ -30,17 +30,12 @@ node_modules/
|
||||
|
||||
# [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
|
||||
+324
-389
@@ -3,655 +3,590 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.72.0](https://github.com/expo/entity/compare/v0.71.0...v0.72.0) (2026-10-29)
|
||||
|
||||
### Features
|
||||
|
||||
- add notAuthorizedEntity to getUserFacingDenialReason ([#732](https://github.com/expo/entity/issues/732)) ([24109be](https://github.com/expo/entity/commit/24109bec32c03b228bd35cef8bd6a379dcf7277b))
|
||||
|
||||
# [0.71.0](https://github.com/expo/entity/compare/v0.70.0...v0.71.0) (2026-10-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- declare runtime dependencies imported by published packages ([#717](https://github.com/expo/entity/issues/717)) ([3c96f3b](https://github.com/expo/entity/commit/3c96f3b9ea2b097c979447354f0abe35ed5e8d57))
|
||||
|
||||
# [0.70.0](https://github.com/expo/entity/compare/v0.69.0...v0.70.0) (2026-10-05)
|
||||
|
||||
### Features
|
||||
|
||||
- add transient error for PostgreSQL lock timeouts ([#716](https://github.com/expo/entity/issues/716)) ([13ce74e](https://github.com/expo/entity/commit/13ce74e93aec0a0b0b84edd1e1e89d785a3dbf46)), closes [#714](https://github.com/expo/entity/issues/714) [#715](https://github.com/expo/entity/issues/715)
|
||||
|
||||
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
|
||||
|
||||
### Features
|
||||
|
||||
- add ID-based and field-based knex database loaders for use with locking modifiers ([#706](https://github.com/expo/entity/issues/706)) ([e87e2a3](https://github.com/expo/entity/commit/e87e2a3a28f4279f5911bfd0189964b94a2ced7f))
|
||||
|
||||
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
|
||||
|
||||
### Features
|
||||
|
||||
- add forShare and skipLocked selection modifiers to knex loader ([#705](https://github.com/expo/entity/issues/705)) ([243a038](https://github.com/expo/entity/commit/243a0386fb9755f3926ade8dcc1b92adf38cd923))
|
||||
- add forUpdate to knex loader ([#704](https://github.com/expo/entity/issues/704)) ([727f166](https://github.com/expo/entity/commit/727f16662d8baa3757bb8e94f51d837f17fd333d))
|
||||
|
||||
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
|
||||
|
||||
**Note:** Version bump only for package root
|
||||
|
||||
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
|
||||
|
||||
**Note:** Version bump only for package root
|
||||
|
||||
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** update dependency koa to v3.2.1 ([#634](https://github.com/expo/entity/issues/634)) ([1c17dc0](https://github.com/expo/entity/commit/1c17dc0f37ec68fb73aae1f92b3ce2c79062b93e))
|
||||
|
||||
### Features
|
||||
|
||||
- accept array of parent entity classes in cascade deletion rule ([#650](https://github.com/expo/entity/issues/650)) ([df75910](https://github.com/expo/entity/commit/df75910cbdc89df60ed2f23dbedf8862ed5c9077))
|
||||
|
||||
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- only throw EntityDatabaseAdapterPaginationCursorInvalidError from cursor decoding logic ([#537](https://github.com/expo/entity/issues/537)) ([e3cee07](https://github.com/expo/entity/commit/e3cee07091af8d1e6c5bb2d82f13e837c7658ea9))
|
||||
- remove unused arg from EntityInvalidationUtils ([#536](https://github.com/expo/entity/issues/536)) ([0a1d1b5](https://github.com/expo/entity/commit/0a1d1b5972c15c932f5c31015f2519163f882f83))
|
||||
|
||||
# [0.61.0](https://github.com/expo/entity/compare/v0.60.0...v0.61.0) (2026-03-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** update dependency graphql to v16.13.1 ([#510](https://github.com/expo/entity/issues/510)) ([1a8535f](https://github.com/expo/entity/commit/1a8535f983f38aaa462a8d587e2f62f5fab1043c))
|
||||
|
||||
### Features
|
||||
|
||||
- update the SQLExpression and SQLFragmentHelpers APIs ([#516](https://github.com/expo/entity/issues/516)) ([112d4dc](https://github.com/expo/entity/commit/112d4dc9b379da5317ff6acaf1d2e9bd597128a6))
|
||||
|
||||
# [0.60.0](https://github.com/expo/entity/compare/v0.59.0...v0.60.0) (2026-03-06)
|
||||
|
||||
### Features
|
||||
|
||||
- Add SQLExpression class that supports fluent comparison method API ([#506](https://github.com/expo/entity/issues/506)) ([535b681](https://github.com/expo/entity/commit/535b6811e08578aba1aadb095f3e401cfe7eaa26))
|
||||
|
||||
# [0.59.0](https://github.com/expo/entity/compare/v0.58.0...v0.59.0) (2026-03-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** update dependency @koa/router to v15.3.1 ([#497](https://github.com/expo/entity/issues/497)) ([72b3c80](https://github.com/expo/entity/commit/72b3c80d12a1afa3c5593bcf6d7ad233c3524341))
|
||||
|
||||
### Features
|
||||
|
||||
- add support to SQLFragment for `= ANY(?)` ([#498](https://github.com/expo/entity/issues/498)) ([b931daa](https://github.com/expo/entity/commit/b931daa9a082cdb33f4559d6d32e1655f0459b79))
|
||||
|
||||
# [0.58.0](https://github.com/expo/entity/compare/v0.57.0...v0.58.0) (2026-03-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- escape all matching portions of parts of cacher cache keys ([#486](https://github.com/expo/entity/issues/486)) ([21f2f6f](https://github.com/expo/entity/commit/21f2f6f461ee771e8b1c1b22dbaf9b275094c0db))
|
||||
- fix some security review suggestions ([#485](https://github.com/expo/entity/issues/485)) ([e263bd2](https://github.com/expo/entity/commit/e263bd218cda579365766d0918c4961304bd1d9f))
|
||||
|
||||
# [0.57.0](https://github.com/expo/entity/compare/v0.56.0...v0.57.0) (2026-02-28)
|
||||
|
||||
**Note:** Version bump only for package root
|
||||
|
||||
# [0.56.0](https://github.com/expo/entity/compare/v0.55.0...v0.56.0) (2026-02-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Add default for TSelectedFields for application-instantiated types ([#438](https://github.com/expo/entity/issues/438)) ([3d4aa0e](https://github.com/expo/entity/commit/3d4aa0e9efa519a36f3db69bd8f117a993c100fa))
|
||||
- Apply same viewer context subclass type mechanism to knexLoader and related methods ([#472](https://github.com/expo/entity/issues/472)) ([018a40b](https://github.com/expo/entity/commit/018a40bc01d579ffbf88f308e641921e6fc39097))
|
||||
- correct discriminated union type for forward/backward pagination ([#474](https://github.com/expo/entity/issues/474)) ([90ae6e2](https://github.com/expo/entity/commit/90ae6e299bda26ac9a80d83e83334d4152c022dd))
|
||||
- correct pagination behavior for standard pagination with descending ordering clauses ([#477](https://github.com/expo/entity/issues/477)) ([e32c4c2](https://github.com/expo/entity/commit/e32c4c2bcc05f225a556f95c90f1ee0f4e24de74))
|
||||
- correct precedence parentheses for SQLFragment joins ([#476](https://github.com/expo/entity/issues/476)) ([2c4c649](https://github.com/expo/entity/commit/2c4c649a4bca5bc9ffdc41eb3135b2dfe4db5e57)), closes [/github.com/expo/entity/blob/main/packages/entity-database-adapter-knex/src/internal/EntityKnexDataManager.ts#L303](https://github.com//github.com/expo/entity/blob/main/packages/entity-database-adapter-knex/src/internal/EntityKnexDataManager.ts/issues/L303)
|
||||
- **deps:** update dependency @apollo/server to v5.4.0 [security] ([#420](https://github.com/expo/entity/issues/420)) ([cd81c5a](https://github.com/expo/entity/commit/cd81c5a5b6be43ef60248eeea4031a883dd1eae8))
|
||||
- **deps:** update dependency @koa/bodyparser to v6.1.0 ([#405](https://github.com/expo/entity/issues/405)) ([2dda22d](https://github.com/expo/entity/commit/2dda22d761f87851e3fcccc8443597daf8ac42e8))
|
||||
- **deps:** update dependency @koa/router to v15.3.0 ([#406](https://github.com/expo/entity/issues/406)) ([9e88cf6](https://github.com/expo/entity/commit/9e88cf6aab76902c65994d28de8dab73f1267021))
|
||||
- **deps:** update dependency graphql to v16.13.0 ([#479](https://github.com/expo/entity/issues/479)) ([976e2c3](https://github.com/expo/entity/commit/976e2c33867da71a2920d050c4086e2af8f6d54d))
|
||||
- **deps:** update dependency koa to v3.1.2 [security] ([#484](https://github.com/expo/entity/issues/484)) ([b258dad](https://github.com/expo/entity/commit/b258dad2ed69c473f04d57d26910eab2032d142c))
|
||||
- eliminate some unnecessary any casts ([#448](https://github.com/expo/entity/issues/448)) ([cc78029](https://github.com/expo/entity/commit/cc78029117c14510b0934b5e0d19876a45570f8d))
|
||||
- explicitly document behavior when cursor entity no longer exists ([#453](https://github.com/expo/entity/issues/453)) ([4dc156c](https://github.com/expo/entity/commit/4dc156c7a8dc50bc9e2802be133b9e258b289f54)), closes [#422](https://github.com/expo/entity/issues/422) [#431](https://github.com/expo/entity/issues/431)
|
||||
- move EntityPrivacyUtils back into core package ([#429](https://github.com/expo/entity/issues/429)) ([56ec27d](https://github.com/expo/entity/commit/56ec27d5f201d83930a2cc05dfc380a054bff1e1))
|
||||
- pass in augmentable classes to installation ([#437](https://github.com/expo/entity/issues/437)) ([f5c728b](https://github.com/expo/entity/commit/f5c728be565e01c0a8ace9ef51f6de8f49184dab)), closes [/github.com/expo/entity/pull/410#pullrequestreview-3765893898](https://github.com//github.com/expo/entity/pull/410/issues/pullrequestreview-3765893898)
|
||||
- refactor install method to instead be free functions ([#441](https://github.com/expo/entity/issues/441)) ([51d632d](https://github.com/expo/entity/commit/51d632da1619dc039d5c41ce6533f63254ec4337)), closes [#410](https://github.com/expo/entity/issues/410)
|
||||
- Reorganize constructionUtils to make non-public ([#454](https://github.com/expo/entity/issues/454)) ([d9377a2](https://github.com/expo/entity/commit/d9377a290b7e0c6803f9e83537eddb7204bc9320)), closes [#410](https://github.com/expo/entity/issues/410)
|
||||
- revert change to codemod now that Entity.knexLoader is supported again via subclass ([#443](https://github.com/expo/entity/issues/443)) ([ed6b2e0](https://github.com/expo/entity/commit/ed6b2e05539c77ee2abd2c6da74e2a451967d06e)), closes [#441](https://github.com/expo/entity/issues/441)
|
||||
- Type idField in EntityConfiguration as TIDField ([#425](https://github.com/expo/entity/issues/425)) ([be40bcf](https://github.com/expo/entity/commit/be40bcffa6e7db9b7ad75e1a80bbb48ff417c6c5))
|
||||
- update integration test to use BlahEntity.knexLoader syntax ([#444](https://github.com/expo/entity/issues/444)) ([457f58a](https://github.com/expo/entity/commit/457f58ad2c4e1cf09f3132f3b9af5008ba0290c7)), closes [#441](https://github.com/expo/entity/issues/441)
|
||||
- Use TSelectedFields for knex loader order by method ([#424](https://github.com/expo/entity/issues/424)) ([a9d09f4](https://github.com/expo/entity/commit/a9d09f42b67378d06133bcfb5b99c9afb17aac0d))
|
||||
|
||||
### Features
|
||||
|
||||
- Add authorization action to EntityPrivacyPolicyRuleEvaluationContext ([#464](https://github.com/expo/entity/issues/464)) ([e71ae5f](https://github.com/expo/entity/commit/e71ae5fcbae95096e0d6a42a8c960896ab83ccfb))
|
||||
- add codemod for upcoming v0.55.0-v0.56.0 ([#413](https://github.com/expo/entity/issues/413)) ([aba0ce5](https://github.com/expo/entity/commit/aba0ce5b5f38cb7b01956cde14a4714e2a03abb4)), closes [#407](https://github.com/expo/entity/issues/407) [-#410](https://github.com/-/issues/410)
|
||||
- add entity-database-adapter-knex-testing-utils containing StubPostgresDatabaseAdapter ([#412](https://github.com/expo/entity/issues/412)) ([eeccf59](https://github.com/expo/entity/commit/eeccf594c11bf090aa83bbf71fb4ead8668aec7d))
|
||||
- add entityField SQL helper ([#481](https://github.com/expo/entity/issues/481)) ([459a5ff](https://github.com/expo/entity/commit/459a5ff85014a8c7396c0856dffacb1491ccb963))
|
||||
- Add ilike and trigram similarity search to pagination ([#431](https://github.com/expo/entity/issues/431)) ([f72cbca](https://github.com/expo/entity/commit/f72cbcaf5a722e6bb8299644ec32b07f1d3362a9)), closes [#422](https://github.com/expo/entity/issues/422)
|
||||
- add method to get pagination cursor for single entity ([#475](https://github.com/expo/entity/issues/475)) ([898f71c](https://github.com/expo/entity/commit/898f71ce082c2593585575b60bd6ef2546717124))
|
||||
- Add paginated loader to entity-database-adapter-knex ([#422](https://github.com/expo/entity/issues/422)) ([58deffd](https://github.com/expo/entity/commit/58deffd426b0214073ede608cd47c04db2596d60))
|
||||
- Add pagination max page size configuration ([#436](https://github.com/expo/entity/issues/436)) ([3ec4b5f](https://github.com/expo/entity/commit/3ec4b5f0cd53bf18d6bcfa27dd29b70d3f4d5f19))
|
||||
- add PostgresEntity/ReadonlyPostgresEntity classes that expose knexLoader methods ([#442](https://github.com/expo/entity/issues/442)) ([40b409f](https://github.com/expo/entity/commit/40b409f6f889febf8b90b8837caecac26ff64174)), closes [#441](https://github.com/expo/entity/issues/441)
|
||||
- Add raw sqlfragment orderBy capability for pagination ([#457](https://github.com/expo/entity/issues/457)) ([0615ffa](https://github.com/expo/entity/commit/0615ffacffbe9e903a9bf03b171c102f0554b992))
|
||||
- add support for nulls ordering in order by clauses ([#468](https://github.com/expo/entity/issues/468)) ([6c0838d](https://github.com/expo/entity/commit/6c0838dc577a3b6190e3551ee834925d7bd53bc6))
|
||||
- support nullable search fields with postgres transforms ([#470](https://github.com/expo/entity/issues/470)) ([53b0604](https://github.com/expo/entity/commit/53b0604152b8ca5ad9fc616d18478f93473c7eb0))
|
||||
|
||||
# [0.55.0](https://github.com/expo/entity/compare/v0.54.0...v0.55.0) (2026-01-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** update dependency @apollo/server to v5.3.0 ([#403](https://github.com/expo/entity/issues/403)) ([b758c4d](https://github.com/expo/entity/commit/b758c4d344294797430de0afb88e47f558b6fa8d))
|
||||
|
||||
### Features
|
||||
|
||||
- add AllowIf[Any|All]SubRulesAllowPrivacyPolicyRule rules ([#401](https://github.com/expo/entity/issues/401)) ([5db8977](https://github.com/expo/entity/commit/5db89777c605eda9a8393227ceeb641e460dc463))
|
||||
- Add AllowIfInParentCascadeDeletionPrivacyPolicyRule ([#399](https://github.com/expo/entity/issues/399)) ([56ed357](https://github.com/expo/entity/commit/56ed357b923a8374a3c120e3eef5d487d5fb2f9c))
|
||||
- add EvaluateIfEntityFieldPredicatePrivacyPolicyRule ([#400](https://github.com/expo/entity/issues/400)) ([f3b575c](https://github.com/expo/entity/commit/f3b575cd00d47c747cf45e96c6db7857592b0f93))
|
||||
|
||||
# [0.54.0](https://github.com/expo/entity/compare/v0.53.0...v0.54.0) (2026-01-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** pin dependencies ([#328](https://github.com/expo/entity/issues/328)) ([5679f27](https://github.com/expo/entity/commit/5679f27209f1515ca5e626d858c4aa054b414625))
|
||||
- **deps:** pin dependency @apollo/server to 4.12.2 ([#330](https://github.com/expo/entity/issues/330)) ([7038912](https://github.com/expo/entity/commit/7038912ca5e5b203ebbca4b46bda5612e7660eac))
|
||||
- **deps:** pin dependency @koa/bodyparser to 6.0.0 ([#339](https://github.com/expo/entity/issues/339)) ([9f93926](https://github.com/expo/entity/commit/9f939269cf843059ec0ac729d470db074730348e))
|
||||
- **deps:** pin dependency koa to 2.16.3 ([#331](https://github.com/expo/entity/issues/331)) ([ce55922](https://github.com/expo/entity/commit/ce55922e3ec6692a9ea4847427dc475902174830))
|
||||
- Inline map().filter() loops in EntityLoaderUtils ([#369](https://github.com/expo/entity/issues/369)) ([ea6c128](https://github.com/expo/entity/commit/ea6c128d9c8fc21484645796f79e1aa8839dd395))
|
||||
- run tsc during tests CI job ([#373](https://github.com/expo/entity/issues/373)) ([1cc57ed](https://github.com/expo/entity/commit/1cc57eddce72663cff17a5102afa6c3e8248aee4))
|
||||
|
||||
### Features
|
||||
|
||||
- synthetically set cascading set-null fields to null during canViewerDeleteAsync evaluation ([#376](https://github.com/expo/entity/issues/376)) ([544bb7c](https://github.com/expo/entity/commit/544bb7c3f2f41419cb3b779f7e3d968e09455fa0))
|
||||
|
||||
# [0.53.0](https://github.com/expo/entity/compare/v0.52.0...v0.53.0) (2025-12-19)
|
||||
|
||||
**Note:** Version bump only for package root
|
||||
|
||||
# [0.52.0](https://github.com/expo/entity/compare/v0.51.0...v0.52.0) (2025-12-19)
|
||||
|
||||
### Features
|
||||
|
||||
- switch local cache to an interface ([#322](https://github.com/expo/entity/issues/322)) ([c453726](https://github.com/expo/entity/commit/c4537262d810ea01e519f9aec6ae5b17a54ce936))
|
||||
|
||||
# [0.51.0](https://github.com/expo/entity/compare/v0.50.0...v0.51.0) (2025-11-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add default for TSelectedFields in PrivacyPolicyTestUtils Case ([#316](https://github.com/expo/entity/issues/316)) ([2f9d487](https://github.com/expo/entity/commit/2f9d4873aaefe891920c25a41088f7c467da6113))
|
||||
|
||||
# [0.50.0](https://github.com/expo/entity/compare/v0.49.0...v0.50.0) (2025-10-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- make in memory and stub database adapters behave more similarly to sql ([#315](https://github.com/expo/entity/issues/315)) ([0f53c24](https://github.com/expo/entity/commit/0f53c24faff8a994906e630d7f17d67d89ff5b0f)), closes [#311](https://github.com/expo/entity/issues/311)
|
||||
|
||||
# [0.49.0](https://github.com/expo/entity/compare/v0.48.0...v0.49.0) (2025-10-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add readonly modifier to partitionArray utility method ([#307](https://github.com/expo/entity/issues/307)) ([077d53c](https://github.com/expo/entity/commit/077d53cd86d4c54132ea2ebcc59d1d75e2b507d0))
|
||||
- validate that UUID field is lower-case ([#310](https://github.com/expo/entity/issues/310)) ([949f00d](https://github.com/expo/entity/commit/949f00d93b52e405188b9b23e23ae59ce1c546de)), closes [/github.com/expo/entity/blob/077d53cd86d4c54132ea2ebcc59d1d75e2b507d0/packages/entity/src/EntityDatabaseAdapter.ts#L182](https://github.com//github.com/expo/entity/blob/077d53cd86d4c54132ea2ebcc59d1d75e2b507d0/packages/entity/src/EntityDatabaseAdapter.ts/issues/L182) [#309](https://github.com/expo/entity/issues/309)
|
||||
|
||||
### Features
|
||||
|
||||
- add more documentation to entity errors ([#312](https://github.com/expo/entity/issues/312)) ([4c3fd17](https://github.com/expo/entity/commit/4c3fd175a91cf3107b1ac413e39e52e051a9dc4b))
|
||||
- throw specific errors for database adapter issues ([#311](https://github.com/expo/entity/issues/311)) ([f6639f9](https://github.com/expo/entity/commit/f6639f9def4ac20b628dd48fdca8d7cf61b6347b))
|
||||
|
||||
# [0.48.0](https://github.com/expo/entity/compare/v0.47.0...v0.48.0) (2025-09-19)
|
||||
|
||||
**Note:** Version bump only for package root
|
||||
|
||||
# [0.47.0](https://github.com/expo/entity/compare/v0.46.0...v0.47.0) (2025-09-19)
|
||||
|
||||
### Features
|
||||
|
||||
- add support for Buffer fields ([#305](https://github.com/expo/entity/issues/305)) ([31b52a5](https://github.com/expo/entity/commit/31b52a5f75842f4810f66c12ad03a10d9192f889))
|
||||
- allow manual specification of cascadingDeleteCause in delete mutator factory method ([#306](https://github.com/expo/entity/issues/306)) ([4a3eda2](https://github.com/expo/entity/commit/4a3eda209fa747ee093ca3053b3196bf9a52193d))
|
||||
|
||||
# [0.46.0](https://github.com/expo/entity/compare/v0.45.0...v0.46.0) (2025-06-26)
|
||||
|
||||
**Note:** Version bump only for package root
|
||||
|
||||
# [0.45.0](https://github.com/expo/entity/compare/v0.44.0...v0.45.0) (2025-06-06)
|
||||
|
||||
### Features
|
||||
|
||||
- add ability to define a custom cache key invalidation version supplier ([#290](https://github.com/expo/entity/issues/290)) ([a3ab61b](https://github.com/expo/entity/commit/a3ab61bfa8d2eab3bbd9c703c5e483184bb67f13))
|
||||
- add createOrGetExistingAsync and createWithUniqueConstraintRecoveryAsync utility methods ([#291](https://github.com/expo/entity/issues/291)) ([78bc264](https://github.com/expo/entity/commit/78bc264b3328692b4b6722388a57d298f02e65c5))
|
||||
|
||||
# [0.44.0](https://github.com/expo/entity/compare/v0.43.0...v0.44.0) (2025-05-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- introduce transactional dataloader mode and fix global transaction ID generation ([#289](https://github.com/expo/entity/issues/289)) ([d3e89bd](https://github.com/expo/entity/commit/d3e89bdd82e6df5c5b35522bc3574fd9e1cfddff))
|
||||
|
||||
### Features
|
||||
|
||||
- add transaction-scoped dataloaders ([#284](https://github.com/expo/entity/issues/284)) ([e054a0d](https://github.com/expo/entity/commit/e054a0d74b3a39158ca7d73d05f59d3633113400)), closes [#98](https://github.com/expo/entity/issues/98) [#194](https://github.com/expo/entity/issues/194)
|
||||
|
||||
# [0.43.0](https://github.com/expo/entity/compare/v0.42.0...v0.43.0) (2025-04-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix generated tsc definition sentinel type visibility ([#283](https://github.com/expo/entity/issues/283)) ([9758fd5](https://github.com/expo/entity/commit/9758fd5df8695a8121258cd988c0c79ce8a3e4b6))
|
||||
|
||||
# [0.42.0](https://github.com/expo/entity/compare/v0.41.0...v0.42.0) (2025-04-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix metrics for dataloader waterfall logging ([#267](https://github.com/expo/entity/issues/267)) ([1d95eba](https://github.com/expo/entity/commit/1d95eba1f9efb67164c3affc917c9b2514e1529f))
|
||||
- move source of truth of loader docblocks to enforcing loader since it is not the default loader ([#264](https://github.com/expo/entity/issues/264)) ([aa56b70](https://github.com/expo/entity/commit/aa56b705db115cf760125208eb2d3585a3794e58))
|
||||
|
||||
### Features
|
||||
|
||||
- add @expo/entity-testing-utils package ([#280](https://github.com/expo/entity/issues/280)) ([485894a](https://github.com/expo/entity/commit/485894af16e233d533a9480285bbda56812cbe0b))
|
||||
- add composite field loading and caching ([#272](https://github.com/expo/entity/issues/272)) ([f0aa0da](https://github.com/expo/entity/commit/f0aa0dafebdb56418cbd22dda437233d850db3e7))
|
||||
- add some utility classes and methods in preparation for composite keys ([#269](https://github.com/expo/entity/issues/269)) ([ad41af1](https://github.com/expo/entity/commit/ad41af1c826fb267f5e463771107c85802578a19)), closes [#201](https://github.com/expo/entity/issues/201)
|
||||
- change entity id field generic to field name and derive type where necessary ([#278](https://github.com/expo/entity/issues/278)) ([b7e524c](https://github.com/expo/entity/commit/b7e524c41892797608d0b884410b8d520a80a9ef))
|
||||
- convert batched/cached loader interface to holder pattern ([#271](https://github.com/expo/entity/issues/271)) ([06b3cb7](https://github.com/expo/entity/commit/06b3cb7f8076ef9e5b83cdf154da159801bd14cc)), closes [#201](https://github.com/expo/entity/issues/201)
|
||||
- enforce explicit id field cache property ([#276](https://github.com/expo/entity/issues/276)) ([1da5cc0](https://github.com/expo/entity/commit/1da5cc01e1affc3b3338a0ab2050504b5853ac6f))
|
||||
- invalidate n+/-1 cacheKeyVersion for entities for push safety ([#275](https://github.com/expo/entity/issues/275)) ([d9c1852](https://github.com/expo/entity/commit/d9c1852706f98f0761ff24ddd244e4ed2beca580))
|
||||
|
||||
# [0.41.0](https://github.com/expo/entity/compare/v0.40.0...v0.41.0) (2025-03-07)
|
||||
|
||||
- feat!: Default all loaders and mutators to enforcing (#256) ([a976e76](https://github.com/expo/entity/commit/a976e765098bca27049585e43a3bed5c71b9ac6b)), closes [#256](https://github.com/expo/entity/issues/256)
|
||||
|
||||
### Features
|
||||
|
||||
- Create codemod for v0.40.0 -> v0.41.0 ([#260](https://github.com/expo/entity/issues/260)) ([4a2b61f](https://github.com/expo/entity/commit/4a2b61f2bd8edfc19109ef4c8ac780eaf0f7795c))
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
- Default all loaders and mutators to enforcing
|
||||
|
||||
# [0.40.0](https://github.com/expo/entity/compare/v0.39.0...v0.40.0) (2025-02-27)
|
||||
|
||||
### Features
|
||||
|
||||
- Create codemod for v0.39.0 -> v0.40.0 ([#259](https://github.com/expo/entity/issues/259)) ([d63ddd6](https://github.com/expo/entity/commit/d63ddd65997a441a914a71b158a111339970aeab))
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "v0.40.0" ([fa78ce8](https://github.com/expo/entity/commit/fa78ce84e2f2eb8199e36fd51779fc68905001c0))
|
||||
|
||||
# [0.39.0](https://github.com/expo/entity/compare/v0.38.0...v0.39.0) (2024-11-21)
|
||||
|
||||
### Features
|
||||
|
||||
- add function to get authorization results for canViewerUpdateAsync/canViewerDeleteAsync ([#249](https://github.com/expo/entity/issues/249)) ([27a358d](https://github.com/expo/entity/commit/27a358d259503aa8d7d7413bfc1e52e0974dc626))
|
||||
|
||||
# [0.38.0](https://github.com/expo/entity/compare/v0.37.0...v0.38.0) (2024-06-27)
|
||||
|
||||
### Features
|
||||
|
||||
- add EntityEdgeDeletionAuthorizationInferenceBehavior for canViewerDeleteAsync ([#243](https://github.com/expo/entity/issues/243)) ([162b192](https://github.com/expo/entity/commit/162b19257ac720c94fb84d64808fb75748adcdc7))
|
||||
|
||||
# [0.37.0](https://github.com/expo/entity/compare/v0.36.0...v0.37.0) (2024-06-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update barrels ([#241](https://github.com/expo/entity/issues/241)) ([8c5f81b](https://github.com/expo/entity/commit/8c5f81bcca107e9b7bbea5f0ab41b0105057806e))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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
|
||||
@@ -10,17 +10,18 @@ Entity is a privacy-aware data layer for defining, caching, and authorizing acce
|
||||
|
||||
## 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,23 +78,11 @@ Entity is not limited in where it can or should be used, but was designed for us
|
||||
|
||||
Note: The entity framework instance should not be shared across multiple requests since it contains a unique memoized [Dataloader](https://github.com/graphql/dataloader#class-dataloader). A long-lived instance is prone to data synchronization issues, especially when the application is scaled horizontally and multiple shared caches would exist for the same data.
|
||||
|
||||
## Contributing
|
||||
|
||||
We control development tooling versions using [mise-en-place](https://mise.jdx.dev). If you have it installed, then:
|
||||
|
||||
- Running `mise run ci` will run all tests that our CI environment will run.
|
||||
- Running `mise t` will show a list of mise tasks you can run with `mise run <name>`.
|
||||
- Running `mise en .` will enter a shell environment which includes node and yarn.
|
||||
|
||||
If you don't have `mise` installed, [you can install it](https://mise.jdx.dev/installing-mise.html#https-mise-run) as easily as running `curl https://mise.run/zsh | sh`.
|
||||
|
||||
## Releasing
|
||||
|
||||
To release a new version:
|
||||
|
||||
1. `git checkout main`
|
||||
1. `npm login` (to refresh token)
|
||||
1. `yarn lerna publish [patch|minor|major] --conventional-commits --force-publish`
|
||||
1. `yarn lerna publish [patch|minor|major] -- --conventional-commits`
|
||||
1. In GitHub release interface, create a new release from the tag, copy changelog changes to release description.
|
||||
|
||||
## License
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default {
|
||||
presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
codecov:
|
||||
branch: main
|
||||
ignore:
|
||||
- "packages/entity-example"
|
||||
@@ -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.72.0",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
"useWorkspaces": true,
|
||||
"version": "0.37.0"
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
[tools]
|
||||
node = "24.22.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,95 +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-restricted-properties': [
|
||||
'warn',
|
||||
{
|
||||
object: 'it',
|
||||
property: 'only',
|
||||
message: 'it.only should not be committed to main.',
|
||||
},
|
||||
{
|
||||
object: 'test',
|
||||
property: 'only',
|
||||
message: 'test.only should not be committed to main.',
|
||||
},
|
||||
{
|
||||
object: 'describe',
|
||||
property: 'only',
|
||||
message: 'describe.only should not be committed to main.',
|
||||
},
|
||||
{
|
||||
object: 'it',
|
||||
property: 'skip',
|
||||
message: 'it.skip should not be committed to main.',
|
||||
},
|
||||
{
|
||||
object: 'test',
|
||||
property: 'skip',
|
||||
message: 'test.skip should not be committed to main.',
|
||||
},
|
||||
{
|
||||
object: 'describe',
|
||||
property: 'skip',
|
||||
message: 'describe.skip should not be committed to main.',
|
||||
},
|
||||
],
|
||||
'no-console': 'warn',
|
||||
'no-void': ['warn', { allowAsStatement: true }],
|
||||
'no-restricted-imports': ['error', { paths: ['.', '..', '../..'] }],
|
||||
|
||||
// TypeScript rules
|
||||
'typescript/ban-ts-comment': 'warn',
|
||||
'typescript/unbound-method': 'warn',
|
||||
'typescript/restrict-template-expressions': 'off',
|
||||
'typescript/no-base-to-string': 'off',
|
||||
'typescript/no-redundant-type-constituents': 'off',
|
||||
'typescript/require-array-sort-compare': 'off',
|
||||
'typescript/explicit-function-return-type': ['warn', { allowExpressions: true }],
|
||||
|
||||
// Jest rules
|
||||
'jest/valid-title': 'off',
|
||||
'jest/expect-expect': 'off',
|
||||
'jest/no-conditional-expect': 'off',
|
||||
'jest/no-export': 'off',
|
||||
'jest/require-to-throw-message': 'off',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.config.js'],
|
||||
env: { node: true },
|
||||
globals: { URL: 'readonly' },
|
||||
},
|
||||
{
|
||||
files: ['**/__tests__/**/*.ts', '**/__integration-tests__/**/*.ts'],
|
||||
rules: {
|
||||
'typescript/no-confusing-void-expression': 'off',
|
||||
'typescript/unbound-method': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/**/*.ts'],
|
||||
excludeFiles: ['**/*tests__/**', 'packages/entity-codemod/src/transforms/**'],
|
||||
rules: {
|
||||
'no-restricted-exports': ['error', { restrictDefaultExports: { direct: true } }],
|
||||
},
|
||||
},
|
||||
],
|
||||
ignorePatterns: [
|
||||
'packages/entity-codemod/**/__testfixtures__/**',
|
||||
'packages/*/build',
|
||||
'coverage',
|
||||
'coverage-integration',
|
||||
'doc',
|
||||
],
|
||||
});
|
||||
+45
-30
@@ -4,39 +4,54 @@
|
||||
"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.28",
|
||||
"@types/node": "24.19.2",
|
||||
"ctix": "2.8.2",
|
||||
"jest": "30.5.2",
|
||||
"lerna": "9.0.7",
|
||||
"oxfmt": "0.73.0",
|
||||
"oxlint": "1.88.0",
|
||||
"oxlint-config-universe": "0.2.0",
|
||||
"oxlint-tsgolint": "0.25.0",
|
||||
"prettier": "3.9.10",
|
||||
"typedoc": "0.28.20",
|
||||
"typescript": "6.0.3"
|
||||
"@expo/batcher": "^1.0.0",
|
||||
"@expo/entity": "file:packages/entity",
|
||||
"@expo/results": "^1.0.0",
|
||||
"@types/invariant": "^2.2.37",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/jsbn": "^1.2.29",
|
||||
"@types/lru-cache": "^5.1.1",
|
||||
"@types/node": "^20.14.1",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"barrelsby": "^2.8.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-universe": "^13.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.3.0",
|
||||
"ioredis": "^5.4.1",
|
||||
"jest": "^29.7.0",
|
||||
"lerna": "^5.4.0",
|
||||
"lru-cache": "^6.0.0",
|
||||
"nullthrows": "^1.1.1",
|
||||
"pg": "8.12.0",
|
||||
"prettier": "^3.3.0",
|
||||
"ts-jest": "^29.1.4",
|
||||
"ts-mockito": "^2.6.1",
|
||||
"typedoc": "^0.25.13",
|
||||
"typescript": "^5.4.5",
|
||||
"uuid": "^8.3.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"collect-v8-coverage": "^1.0.3"
|
||||
"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-ip-address-field": "file:packages/entity-ip-address-field",
|
||||
"@expo/entity-secondary-cache-local-memory": "file:packages/entity-secondary-cache-local-memory",
|
||||
"@expo/entity-secondary-cache-redis": "file:packages/entity-secondary-cache-redis"
|
||||
},
|
||||
"packageManager": "[email protected]+sha512.b2e1e7524f654f2749d32b4ebcb4622473cb5bcbc485df2007e12a154e50162a4d795526768bc5f5b8f81717bfd79deb2472813d86fb5ae2eb551fa9c872b08f"
|
||||
"volta": {
|
||||
"node": "20.14.0"
|
||||
}
|
||||
}
|
||||
@@ -3,242 +3,169 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.72.0](https://github.com/expo/entity/compare/v0.71.0...v0.72.0) (2026-10-29)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.71.0](https://github.com/expo/entity/compare/v0.70.0...v0.71.0) (2026-10-09)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.70.0](https://github.com/expo/entity/compare/v0.69.0...v0.70.0) (2026-10-05)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.61.0](https://github.com/expo/entity/compare/v0.60.0...v0.61.0) (2026-03-12)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.60.0](https://github.com/expo/entity/compare/v0.59.0...v0.60.0) (2026-03-06)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.59.0](https://github.com/expo/entity/compare/v0.58.0...v0.59.0) (2026-03-03)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.58.0](https://github.com/expo/entity/compare/v0.57.0...v0.58.0) (2026-03-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- escape all matching portions of parts of cacher cache keys ([#486](https://github.com/expo/entity/issues/486)) ([21f2f6f](https://github.com/expo/entity/commit/21f2f6f461ee771e8b1c1b22dbaf9b275094c0db))
|
||||
|
||||
# [0.57.0](https://github.com/expo/entity/compare/v0.56.0...v0.57.0) (2026-02-28)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.56.0](https://github.com/expo/entity/compare/v0.55.0...v0.56.0) (2026-02-28)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.55.0](https://github.com/expo/entity/compare/v0.54.0...v0.55.0) (2026-01-27)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.54.0](https://github.com/expo/entity/compare/v0.53.0...v0.54.0) (2026-01-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** pin dependencies ([#328](https://github.com/expo/entity/issues/328)) ([5679f27](https://github.com/expo/entity/commit/5679f27209f1515ca5e626d858c4aa054b414625))
|
||||
|
||||
# [0.53.0](https://github.com/expo/entity/compare/v0.52.0...v0.53.0) (2025-12-19)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.52.0](https://github.com/expo/entity/compare/v0.51.0...v0.52.0) (2025-12-19)
|
||||
|
||||
### Features
|
||||
|
||||
- switch local cache to an interface ([#322](https://github.com/expo/entity/issues/322)) ([c453726](https://github.com/expo/entity/commit/c4537262d810ea01e519f9aec6ae5b17a54ce936))
|
||||
|
||||
# [0.51.0](https://github.com/expo/entity/compare/v0.50.0...v0.51.0) (2025-11-21)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.50.0](https://github.com/expo/entity/compare/v0.49.0...v0.50.0) (2025-10-07)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.49.0](https://github.com/expo/entity/compare/v0.48.0...v0.49.0) (2025-10-07)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.48.0](https://github.com/expo/entity/compare/v0.47.0...v0.48.0) (2025-09-19)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.47.0](https://github.com/expo/entity/compare/v0.46.0...v0.47.0) (2025-09-19)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.46.0](https://github.com/expo/entity/compare/v0.45.0...v0.46.0) (2025-06-26)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.45.0](https://github.com/expo/entity/compare/v0.44.0...v0.45.0) (2025-06-06)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.44.0](https://github.com/expo/entity/compare/v0.43.0...v0.44.0) (2025-05-29)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.43.0](https://github.com/expo/entity/compare/v0.42.0...v0.43.0) (2025-04-10)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.42.0](https://github.com/expo/entity/compare/v0.41.0...v0.42.0) (2025-04-10)
|
||||
|
||||
### Features
|
||||
|
||||
- add @expo/entity-testing-utils package ([#280](https://github.com/expo/entity/issues/280)) ([485894a](https://github.com/expo/entity/commit/485894af16e233d533a9480285bbda56812cbe0b))
|
||||
- add composite field loading and caching ([#272](https://github.com/expo/entity/issues/272)) ([f0aa0da](https://github.com/expo/entity/commit/f0aa0dafebdb56418cbd22dda437233d850db3e7))
|
||||
- change entity id field generic to field name and derive type where necessary ([#278](https://github.com/expo/entity/issues/278)) ([b7e524c](https://github.com/expo/entity/commit/b7e524c41892797608d0b884410b8d520a80a9ef))
|
||||
- convert batched/cached loader interface to holder pattern ([#271](https://github.com/expo/entity/issues/271)) ([06b3cb7](https://github.com/expo/entity/commit/06b3cb7f8076ef9e5b83cdf154da159801bd14cc)), closes [#201](https://github.com/expo/entity/issues/201)
|
||||
- invalidate n+/-1 cacheKeyVersion for entities for push safety ([#275](https://github.com/expo/entity/issues/275)) ([d9c1852](https://github.com/expo/entity/commit/d9c1852706f98f0761ff24ddd244e4ed2beca580))
|
||||
|
||||
# [0.41.0](https://github.com/expo/entity/compare/v0.40.0...v0.41.0) (2025-03-07)
|
||||
|
||||
- feat!: Default all loaders and mutators to enforcing (#256) ([a976e76](https://github.com/expo/entity/commit/a976e765098bca27049585e43a3bed5c71b9ac6b)), closes [#256](https://github.com/expo/entity/issues/256)
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
- Default all loaders and mutators to enforcing
|
||||
|
||||
# [0.40.0](https://github.com/expo/entity/compare/v0.39.0...v0.40.0) (2025-02-27)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "v0.40.0" ([fa78ce8](https://github.com/expo/entity/commit/fa78ce84e2f2eb8199e36fd51779fc68905001c0))
|
||||
|
||||
# [0.39.0](https://github.com/expo/entity/compare/v0.38.0...v0.39.0) (2024-11-21)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.38.0](https://github.com/expo/entity/compare/v0.37.0...v0.38.0) (2024-06-27)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
# [0.37.0](https://github.com/expo/entity/compare/v0.36.0...v0.37.0) (2024-06-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update barrels ([#241](https://github.com/expo/entity/issues/241)) ([8c5f81b](https://github.com/expo/entity/commit/8c5f81bcca107e9b7bbea5f0ab41b0105057806e))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
@@ -1,40 +1,38 @@
|
||||
{
|
||||
"name": "@expo/entity-cache-adapter-local-memory",
|
||||
"version": "0.72.0",
|
||||
"version": "0.37.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 --config ../../.barrelsby.json"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"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": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/entity-testing-utils": "workspace:^",
|
||||
"@isaacs/ttlcache": "2.1.5",
|
||||
"@jest/globals": "30.5.2",
|
||||
"typescript": "6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"@expo/entity": "^0.37.0"
|
||||
}
|
||||
}
|
||||
@@ -1,67 +1,44 @@
|
||||
import type {
|
||||
import {
|
||||
CacheLoadResult,
|
||||
CacheStatus,
|
||||
EntityConfiguration,
|
||||
IEntityGenericCacher,
|
||||
IEntityLoadKey,
|
||||
IEntityLoadValue,
|
||||
} from '@expo/entity';
|
||||
import { CacheStatus } from '@expo/entity';
|
||||
import invariant from 'invariant';
|
||||
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>> {
|
||||
/**
|
||||
* Gets a value from the cache for specified key.
|
||||
* @param key - key to get
|
||||
* @returns the cached value, or undefined if not present
|
||||
*/
|
||||
get(key: string): LocalMemoryCacheValue<TFields> | undefined;
|
||||
|
||||
/**
|
||||
* Sets a value in the cache for specified key.
|
||||
* @param key - key to set
|
||||
* @param value - value to set
|
||||
*/
|
||||
set(key: string, value: LocalMemoryCacheValue<TFields>): void;
|
||||
|
||||
/**
|
||||
* Deletes a value from the cache for specified key.
|
||||
* @param key - key to delete
|
||||
*/
|
||||
delete(key: string): void;
|
||||
}
|
||||
|
||||
export class GenericLocalMemoryCacher<
|
||||
TFields extends Record<string, any>,
|
||||
TIDField extends keyof TFields,
|
||||
> implements IEntityGenericCacher<TFields, TIDField> {
|
||||
export default class GenericLocalMemoryCacher<TFields extends Record<string, any>>
|
||||
implements IEntityGenericCacher<TFields>
|
||||
{
|
||||
constructor(
|
||||
private readonly entityConfiguration: EntityConfiguration<TFields, TIDField>,
|
||||
private readonly localMemoryCache: ILocalMemoryCache<TFields>,
|
||||
private readonly entityConfiguration: EntityConfiguration<TFields>,
|
||||
private readonly localMemoryCache: LocalMemoryCache<TFields>,
|
||||
) {}
|
||||
|
||||
static createNoOpCache<TFields extends Record<string, any>>(): ILocalMemoryCache<TFields> {
|
||||
return {
|
||||
get(_key: string): LocalMemoryCacheValue<TFields> | undefined {
|
||||
return undefined;
|
||||
},
|
||||
set(_key: string, _value: LocalMemoryCacheValue<TFields>): void {},
|
||||
delete(_key: string): void {},
|
||||
};
|
||||
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;
|
||||
return new LRUCache<string, LocalMemoryCacheValue<TFields>>({
|
||||
max: options.maxSize ?? DEFAULT_LRU_CACHE_SIZE,
|
||||
length: (value) => (value === DOES_NOT_EXIST_LOCAL_MEMORY_CACHE ? 0 : 1),
|
||||
maxAge: maxAgeSeconds * 1000, // convert to ms
|
||||
});
|
||||
}
|
||||
|
||||
static createNoOpCache<TFields>(): LocalMemoryCache<TFields> {
|
||||
return new LRUCache<string, LocalMemoryCacheValue<TFields>>({
|
||||
max: 0,
|
||||
maxAge: -1,
|
||||
});
|
||||
}
|
||||
|
||||
public async loadManyAsync(
|
||||
@@ -102,38 +79,27 @@ 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);
|
||||
public 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 ${String(fieldName)}`);
|
||||
const parts = [
|
||||
this.entityConfiguration.tableName,
|
||||
cacheKeyType,
|
||||
`${this.entityConfiguration.cacheKeyVersion}`,
|
||||
...keyAndValueParts,
|
||||
columnName,
|
||||
String(fieldValue),
|
||||
];
|
||||
|
||||
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)];
|
||||
}
|
||||
}
|
||||
@@ -1,49 +1,47 @@
|
||||
import type {
|
||||
EntityConfiguration,
|
||||
import {
|
||||
computeIfAbsent,
|
||||
IEntityCacheAdapter,
|
||||
EntityConfiguration,
|
||||
GenericEntityCacheAdapter,
|
||||
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';
|
||||
|
||||
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 {
|
||||
export default class LocalMemoryCacheAdapterProvider implements IEntityCacheAdapterProvider {
|
||||
/**
|
||||
* @returns a no-op local memory cache adapter provider, or one that doesn't cache at all.
|
||||
* @returns a no-op local memory cache adapter provider, or one that doesn't cache locally.
|
||||
*/
|
||||
static createNoOpProvider(): IEntityCacheAdapterProvider {
|
||||
return new LocalMemoryCacheAdapterProvider(<TFields extends Record<string, any>>() =>
|
||||
return new LocalMemoryCacheAdapterProvider(<TFields>() =>
|
||||
GenericLocalMemoryCacher.createNoOpCache<TFields>(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a local memory cache adapter provider configured with the supplied local memory cache creator function.
|
||||
* @returns a local memory cache adapter provider configured with the supplied options.
|
||||
*/
|
||||
static createProviderWithCacheCreator(
|
||||
cacheCreator: LocalMemoryCacheCreator,
|
||||
static createProviderWithOptions(
|
||||
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 localMemoryCacheAdapterMap = new Map<string, GenericEntityCacheAdapter<any>>();
|
||||
|
||||
private constructor(private readonly localMemoryCacheCreator: LocalMemoryCacheCreator) {}
|
||||
private constructor(
|
||||
private readonly localMemoryCacheCreator: <TFields>() => LocalMemoryCache<TFields>,
|
||||
) {}
|
||||
|
||||
public getCacheAdapter<TFields extends Record<string, any>, TIDField extends keyof TFields>(
|
||||
entityConfiguration: EntityConfiguration<TFields, TIDField>,
|
||||
): IEntityCacheAdapter<TFields, TIDField> {
|
||||
public getCacheAdapter<TFields extends Record<string, any>>(
|
||||
entityConfiguration: EntityConfiguration<TFields>,
|
||||
): IEntityCacheAdapter<TFields> {
|
||||
return computeIfAbsent(this.localMemoryCacheAdapterMap, entityConfiguration.tableName, () => {
|
||||
const localMemoryCache = this.localMemoryCacheCreator<TFields>();
|
||||
return new GenericEntityCacheAdapter(
|
||||
|
||||
-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,
|
||||
);
|
||||
};
|
||||
+59
-87
@@ -1,64 +1,56 @@
|
||||
import type { GenericEntityCacheAdapter, IEntityGenericCacher } from '@expo/entity';
|
||||
import {
|
||||
CacheAdapterFlavor,
|
||||
CacheAdapterFlavorDefinition,
|
||||
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 GenericLocalMemoryCacher from '../GenericLocalMemoryCacher';
|
||||
import LocalMemoryCacheAdapterProvider from '../LocalMemoryCacheAdapterProvider';
|
||||
import LocalMemoryTestEntity from '../testfixtures/LocalMemoryTestEntity';
|
||||
import {
|
||||
createLocalMemoryTestEntityCompanionProvider,
|
||||
createNoOpLocalMemoryIntegrationTestEntityCompanionProvider,
|
||||
} from '../__testfixtures__/createLocalMemoryTestEntityCompanionProvider.ts';
|
||||
} from '../testfixtures/createLocalMemoryTestEntityCompanionProvider';
|
||||
|
||||
describe(GenericLocalMemoryCacher, () => {
|
||||
it('has correct caching behavior', async () => {
|
||||
const entityCompanionProvider = createLocalMemoryTestEntityCompanionProvider();
|
||||
const viewerContext = new ViewerContext(entityCompanionProvider);
|
||||
const genericCacher = (
|
||||
const genericCacher =
|
||||
viewerContext.entityCompanionProvider.getCompanionForEntity(LocalMemoryTestEntity)[
|
||||
'tableDataCoordinator'
|
||||
]['cacheAdapter'] as GenericEntityCacheAdapter<LocalMemoryTestEntityFields, 'id'>
|
||||
)['genericCacher'];
|
||||
]['cacheAdapter']['genericCacher'];
|
||||
const cacheKeyMaker = genericCacher['makeCacheKey'].bind(genericCacher);
|
||||
|
||||
const date = new Date();
|
||||
const entity1Created = await LocalMemoryTestEntity.creator(viewerContext)
|
||||
.setField('name', 'blah')
|
||||
.setField('dateField', date)
|
||||
.createAsync();
|
||||
.enforceCreateAsync();
|
||||
|
||||
// loading an entity should put it in cache
|
||||
const entity1 = await LocalMemoryTestEntity.loader(viewerContext).loadByIDAsync(
|
||||
entity1Created.getID(),
|
||||
);
|
||||
const entity1 = await LocalMemoryTestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByIDAsync(entity1Created.getID());
|
||||
|
||||
const localMemoryCacheAdapterProvider = entityCompanionProvider['cacheAdapterFlavors'].get(
|
||||
'local-memory',
|
||||
)!.cacheAdapterProvider as LocalMemoryCacheAdapterProvider;
|
||||
const localMemoryCacheAdapterProvider = (
|
||||
entityCompanionProvider['cacheAdapterFlavors'] as ReadonlyMap<
|
||||
CacheAdapterFlavor,
|
||||
CacheAdapterFlavorDefinition
|
||||
>
|
||||
).get('local-memory')!.cacheAdapterProvider as LocalMemoryCacheAdapterProvider;
|
||||
const entitySpecificGenericCacher = localMemoryCacheAdapterProvider[
|
||||
'localMemoryCacheAdapterMap'
|
||||
].get(
|
||||
viewerContext.entityCompanionProvider.getCompanionForEntity(LocalMemoryTestEntity)
|
||||
.entityCompanionDefinition.entityConfiguration.tableName,
|
||||
)!['genericCacher'] as IEntityGenericCacher<LocalMemoryTestEntityFields, 'id'>;
|
||||
)!['genericCacher'];
|
||||
const cachedResult = await entitySpecificGenericCacher.loadManyAsync([
|
||||
genericCacher.makeCacheKeyForStorage(
|
||||
new SingleFieldHolder('id'),
|
||||
new SingleFieldValueHolder(entity1.getID()),
|
||||
),
|
||||
cacheKeyMaker('id', entity1.getID()),
|
||||
]);
|
||||
const cachedValue = cachedResult.get(
|
||||
genericCacher.makeCacheKeyForStorage(
|
||||
new SingleFieldHolder('id'),
|
||||
new SingleFieldValueHolder(entity1.getID()),
|
||||
),
|
||||
)!;
|
||||
const cachedValue = cachedResult.get(cacheKeyMaker('id', entity1.getID()))!;
|
||||
expect(cachedValue).toMatchObject({
|
||||
status: CacheStatus.HIT,
|
||||
item: {
|
||||
@@ -71,86 +63,71 @@ describe(GenericLocalMemoryCacher, () => {
|
||||
// simulate non existent db fetch, should write negative result ('') to cache
|
||||
const nonExistentId = uuidv4();
|
||||
|
||||
const entityNonExistentResult =
|
||||
await LocalMemoryTestEntity.loaderWithAuthorizationResults(viewerContext).loadByIDAsync(
|
||||
nonExistentId,
|
||||
);
|
||||
const entityNonExistentResult = await LocalMemoryTestEntity.loader(viewerContext)
|
||||
.withAuthorizationResults()
|
||||
.loadByIDAsync(nonExistentId);
|
||||
expect(entityNonExistentResult.ok).toBe(false);
|
||||
|
||||
const nonExistentCachedResult = await entitySpecificGenericCacher.loadManyAsync([
|
||||
genericCacher.makeCacheKeyForStorage(
|
||||
new SingleFieldHolder('id'),
|
||||
new SingleFieldValueHolder(nonExistentId),
|
||||
),
|
||||
cacheKeyMaker('id', nonExistentId),
|
||||
]);
|
||||
expect(
|
||||
nonExistentCachedResult.get(
|
||||
genericCacher.makeCacheKeyForStorage(
|
||||
new SingleFieldHolder('id'),
|
||||
new SingleFieldValueHolder(nonExistentId),
|
||||
),
|
||||
),
|
||||
).toMatchObject({
|
||||
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.loaderWithAuthorizationResults(viewerContext).loadByIDAsync(
|
||||
nonExistentId,
|
||||
);
|
||||
const entityNonExistentResult2 = await LocalMemoryTestEntity.loader(viewerContext)
|
||||
.withAuthorizationResults()
|
||||
.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,
|
||||
);
|
||||
await LocalMemoryTestEntity.loader(viewerContext)
|
||||
.utils()
|
||||
.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('respects the parameters of a noop cache', async () => {
|
||||
const entityCompanionProvider = createNoOpLocalMemoryIntegrationTestEntityCompanionProvider();
|
||||
const viewerContext = new ViewerContext(entityCompanionProvider);
|
||||
const genericCacher = (
|
||||
const genericCacher =
|
||||
viewerContext.entityCompanionProvider.getCompanionForEntity(LocalMemoryTestEntity)[
|
||||
'tableDataCoordinator'
|
||||
]['cacheAdapter'] as GenericEntityCacheAdapter<LocalMemoryTestEntityFields, 'id'>
|
||||
)['genericCacher'];
|
||||
const cacheKeyMaker = genericCacher['makeCacheKeyForStorage'].bind(genericCacher);
|
||||
]['cacheAdapter']['genericCacher'];
|
||||
const cacheKeyMaker = genericCacher['makeCacheKey'].bind(genericCacher);
|
||||
|
||||
const date = new Date();
|
||||
const entity1Created = await LocalMemoryTestEntity.creator(viewerContext)
|
||||
.setField('name', 'blah')
|
||||
.setField('dateField', date)
|
||||
.createAsync();
|
||||
.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).loadByIDAsync(
|
||||
entity1Created.getID(),
|
||||
);
|
||||
const entity1 = await LocalMemoryTestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByIDAsync(entity1Created.getID());
|
||||
|
||||
const localMemoryCacheAdapterProvider = entityCompanionProvider['cacheAdapterFlavors'].get(
|
||||
'local-memory',
|
||||
)!.cacheAdapterProvider as LocalMemoryCacheAdapterProvider;
|
||||
const localMemoryCacheAdapterProvider = (
|
||||
entityCompanionProvider['cacheAdapterFlavors'] as ReadonlyMap<
|
||||
CacheAdapterFlavor,
|
||||
CacheAdapterFlavorDefinition
|
||||
>
|
||||
).get('local-memory')!.cacheAdapterProvider as LocalMemoryCacheAdapterProvider;
|
||||
const entitySpecificGenericCacher = localMemoryCacheAdapterProvider[
|
||||
'localMemoryCacheAdapterMap'
|
||||
].get(
|
||||
viewerContext.entityCompanionProvider.getCompanionForEntity(LocalMemoryTestEntity)
|
||||
.entityCompanionDefinition.entityConfiguration.tableName,
|
||||
)!['genericCacher'] as IEntityGenericCacher<LocalMemoryTestEntityFields, 'id'>;
|
||||
)!['genericCacher'];
|
||||
const cachedResult = await entitySpecificGenericCacher.loadManyAsync([
|
||||
cacheKeyMaker(new SingleFieldHolder('id'), new SingleFieldValueHolder(entity1.getID())),
|
||||
cacheKeyMaker('id', entity1.getID()),
|
||||
]);
|
||||
const cachedValue = cachedResult.get(
|
||||
cacheKeyMaker(new SingleFieldHolder('id'), new SingleFieldValueHolder(entity1.getID())),
|
||||
)!;
|
||||
const cachedValue = cachedResult.get(cacheKeyMaker('id', entity1.getID()))!;
|
||||
expect(cachedValue).toMatchObject({
|
||||
status: CacheStatus.MISS,
|
||||
});
|
||||
@@ -158,20 +135,15 @@ describe(GenericLocalMemoryCacher, () => {
|
||||
// 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,
|
||||
);
|
||||
const entityNonExistentResult = await LocalMemoryTestEntity.loader(viewerContext)
|
||||
.withAuthorizationResults()
|
||||
.loadByIDAsync(nonExistentId);
|
||||
expect(entityNonExistentResult.ok).toBe(false);
|
||||
|
||||
const nonExistentCachedResult = await entitySpecificGenericCacher.loadManyAsync([
|
||||
cacheKeyMaker(new SingleFieldHolder('id'), new SingleFieldValueHolder(nonExistentId)),
|
||||
cacheKeyMaker('id', nonExistentId),
|
||||
]);
|
||||
expect(
|
||||
nonExistentCachedResult.get(
|
||||
cacheKeyMaker(new SingleFieldHolder('id'), new SingleFieldValueHolder(nonExistentId)),
|
||||
),
|
||||
).toMatchObject({
|
||||
expect(nonExistentCachedResult.get(cacheKeyMaker('id', nonExistentId))).toMatchObject({
|
||||
status: CacheStatus.MISS,
|
||||
});
|
||||
});
|
||||
|
||||
+70
-92
@@ -1,26 +1,19 @@
|
||||
import {
|
||||
CacheStatus,
|
||||
UUIDField,
|
||||
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 {
|
||||
import GenericLocalMemoryCacher, {
|
||||
DOES_NOT_EXIST_LOCAL_MEMORY_CACHE,
|
||||
GenericLocalMemoryCacher,
|
||||
} from '../GenericLocalMemoryCacher.ts';
|
||||
} from '../GenericLocalMemoryCacher';
|
||||
|
||||
type BlahFields = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const entityConfiguration = new EntityConfiguration<BlahFields, 'id'>({
|
||||
const entityConfiguration = new EntityConfiguration<BlahFields>({
|
||||
idField: 'id',
|
||||
tableName: 'blah',
|
||||
schema: {
|
||||
@@ -30,78 +23,83 @@ const entityConfiguration = new EntityConfiguration<BlahFields, 'id'>({
|
||||
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(GenericLocalMemoryCacher, () => {
|
||||
describe(GenericLocalMemoryCacher.createLRUCache, () => {
|
||||
it('creates a cache with default options', () => {
|
||||
const cache = GenericLocalMemoryCacher.createLRUCache();
|
||||
expect(cache.max).toBe(10000);
|
||||
expect(cache.maxAge).toBe(10000);
|
||||
});
|
||||
|
||||
it('respects specified options', () => {
|
||||
const cache = GenericLocalMemoryCacher.createLRUCache({
|
||||
ttlSeconds: 3,
|
||||
maxSize: 10,
|
||||
});
|
||||
expect(cache.max).toBe(10);
|
||||
expect(cache.maxAge).toBe(3000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe(GenericLocalMemoryCacher.createNoOpCache, () => {
|
||||
it('creates a no-op cache', () => {
|
||||
const cache = GenericLocalMemoryCacher.createNoOpCache<{ hello: 'world' }>();
|
||||
cache.set('a', { hello: 'world' });
|
||||
expect(cache.get('a')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Use within GenericEntityCacheAdapter', () => {
|
||||
describe('loadManyAsync', () => {
|
||||
it('returns appropriate cache results', async () => {
|
||||
const cacheAdapter = new GenericEntityCacheAdapter(
|
||||
new GenericLocalMemoryCacher(entityConfiguration, createTTLCache()),
|
||||
new GenericLocalMemoryCacher(
|
||||
entityConfiguration,
|
||||
GenericLocalMemoryCacher.createLRUCache(),
|
||||
),
|
||||
);
|
||||
|
||||
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 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',
|
||||
]);
|
||||
|
||||
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({
|
||||
expect(results.get('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.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 values', async () => {
|
||||
it('returns empty map when passed empty array of fieldValues', async () => {
|
||||
const cacheAdapter = new GenericEntityCacheAdapter(
|
||||
new GenericLocalMemoryCacher(entityConfiguration, createTTLCache()),
|
||||
new GenericLocalMemoryCacher(
|
||||
entityConfiguration,
|
||||
GenericLocalMemoryCacher.createLRUCache(),
|
||||
),
|
||||
);
|
||||
const results = await cacheAdapter.loadManyAsync(
|
||||
new SingleFieldHolder<BlahFields, 'id', 'id'>('id'),
|
||||
[] as SingleFieldValueHolder<BlahFields, 'id'>[],
|
||||
);
|
||||
expect(results).toEqual(new SingleFieldValueHolderMap(new Map()));
|
||||
const results = await cacheAdapter.loadManyAsync('id', []);
|
||||
expect(results).toEqual(new Map());
|
||||
});
|
||||
});
|
||||
|
||||
describe('cacheManyAsync', () => {
|
||||
it('correctly caches all objects', async () => {
|
||||
const localMemoryCache = createTTLCache<BlahFields>();
|
||||
const localMemoryCache = GenericLocalMemoryCacher.createLRUCache<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' }]]),
|
||||
);
|
||||
await cacheAdapter.cacheManyAsync('id', new Map([['test-id-1', { id: 'test-id-1' }]]));
|
||||
|
||||
const cacheKey = localMemoryCacher['makeCacheKeyForStorage'](
|
||||
new SingleFieldHolder('id'),
|
||||
new SingleFieldValueHolder('test-id-1'),
|
||||
);
|
||||
const cacheKey = localMemoryCacher['makeCacheKey']('id', 'test-id-1');
|
||||
expect(localMemoryCache.get(cacheKey)).toMatchObject({
|
||||
id: 'test-id-1',
|
||||
});
|
||||
@@ -110,61 +108,41 @@ describe('Use within GenericEntityCacheAdapter', () => {
|
||||
|
||||
describe('cacheDBMissesAsync', () => {
|
||||
it('correctly caches misses', async () => {
|
||||
const localMemoryCache = createTTLCache<BlahFields>();
|
||||
const localMemoryCache = GenericLocalMemoryCacher.createLRUCache<BlahFields>({});
|
||||
|
||||
const localMemoryCacher = new GenericLocalMemoryCacher(entityConfiguration, localMemoryCache);
|
||||
const cacheAdapter = new GenericEntityCacheAdapter(localMemoryCacher);
|
||||
await cacheAdapter.cacheDBMissesAsync(new SingleFieldHolder('id'), [
|
||||
new SingleFieldValueHolder('test-id-1'),
|
||||
]);
|
||||
await cacheAdapter.cacheDBMissesAsync('id', ['test-id-1']);
|
||||
|
||||
const cacheKey = localMemoryCacher['makeCacheKeyForStorage'](
|
||||
new SingleFieldHolder('id'),
|
||||
new SingleFieldValueHolder('test-id-1'),
|
||||
);
|
||||
const cacheKey = localMemoryCacher['makeCacheKey']('id', '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 localMemoryCache = GenericLocalMemoryCacher.createLRUCache<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'),
|
||||
]);
|
||||
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(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,
|
||||
});
|
||||
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 values', async () => {
|
||||
it('returns when passed empty array of fieldValues', async () => {
|
||||
const cacheAdapter = new GenericEntityCacheAdapter(
|
||||
new GenericLocalMemoryCacher(entityConfiguration, createTTLCache<BlahFields>()),
|
||||
);
|
||||
await cacheAdapter.invalidateManyAsync(
|
||||
new SingleFieldHolder<BlahFields, 'id', 'id'>('id'),
|
||||
[] as SingleFieldValueHolder<BlahFields, 'id'>[],
|
||||
new GenericLocalMemoryCacher(
|
||||
entityConfiguration,
|
||||
GenericLocalMemoryCacher.createLRUCache<BlahFields>({}),
|
||||
),
|
||||
);
|
||||
await cacheAdapter.invalidateManyAsync('id', []);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,5 +4,6 @@
|
||||
* @module @expo/entity-cache-adapter-local-memory
|
||||
*/
|
||||
|
||||
export * from './GenericLocalMemoryCacher.ts';
|
||||
export * from './LocalMemoryCacheAdapterProvider.ts';
|
||||
export { default as GenericLocalMemoryCacher } from './GenericLocalMemoryCacher';
|
||||
export * from './GenericLocalMemoryCacher';
|
||||
export { default as LocalMemoryCacheAdapterProvider } from './LocalMemoryCacheAdapterProvider';
|
||||
+34
-35
@@ -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,14 +16,14 @@ export type LocalMemoryTestEntityFields = {
|
||||
dateField: Date | null;
|
||||
};
|
||||
|
||||
export class LocalMemoryTestEntity extends Entity<
|
||||
export default class LocalMemoryTestEntity extends Entity<
|
||||
LocalMemoryTestEntityFields,
|
||||
'id',
|
||||
string,
|
||||
ViewerContext
|
||||
> {
|
||||
static defineCompanionDefinition(): EntityCompanionDefinition<
|
||||
LocalMemoryTestEntityFields,
|
||||
'id',
|
||||
string,
|
||||
ViewerContext,
|
||||
LocalMemoryTestEntity,
|
||||
LocalMemoryTestEntityPrivacyPolicy
|
||||
@@ -37,14 +38,14 @@ export class LocalMemoryTestEntity extends Entity<
|
||||
|
||||
export class LocalMemoryTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
LocalMemoryTestEntityFields,
|
||||
'id',
|
||||
string,
|
||||
ViewerContext,
|
||||
LocalMemoryTestEntity
|
||||
> {
|
||||
protected override readonly createRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<
|
||||
LocalMemoryTestEntityFields,
|
||||
'id',
|
||||
string,
|
||||
ViewerContext,
|
||||
LocalMemoryTestEntity
|
||||
>(),
|
||||
@@ -52,7 +53,7 @@ export class LocalMemoryTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
protected override readonly readRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<
|
||||
LocalMemoryTestEntityFields,
|
||||
'id',
|
||||
string,
|
||||
ViewerContext,
|
||||
LocalMemoryTestEntity
|
||||
>(),
|
||||
@@ -60,7 +61,7 @@ export class LocalMemoryTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
protected override readonly updateRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<
|
||||
LocalMemoryTestEntityFields,
|
||||
'id',
|
||||
string,
|
||||
ViewerContext,
|
||||
LocalMemoryTestEntity
|
||||
>(),
|
||||
@@ -68,32 +69,30 @@ 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',
|
||||
});
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
NoOpEntityMetricsAdapter,
|
||||
IEntityMetricsAdapter,
|
||||
EntityCompanionProvider,
|
||||
StubQueryContextProvider,
|
||||
StubDatabaseAdapterProvider,
|
||||
} from '@expo/entity';
|
||||
|
||||
import LocalMemoryCacheAdapterProvider from '../LocalMemoryCacheAdapterProvider';
|
||||
|
||||
export const createLocalMemoryTestEntityCompanionProvider = (
|
||||
localMemoryOptions: { maxSize?: number; ttlSeconds?: number } = {},
|
||||
metricsAdapter: IEntityMetricsAdapter = new NoOpEntityMetricsAdapter(),
|
||||
): EntityCompanionProvider => {
|
||||
const localMemoryCacheAdapterProvider =
|
||||
localMemoryOptions.maxSize === 0 && localMemoryOptions.ttlSeconds === 0
|
||||
? LocalMemoryCacheAdapterProvider.createNoOpProvider()
|
||||
: LocalMemoryCacheAdapterProvider.createProviderWithOptions(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 createLocalMemoryTestEntityCompanionProvider(
|
||||
{ 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,369 +3,397 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.72.0](https://github.com/expo/entity/compare/v0.71.0...v0.72.0) (2026-10-29)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.71.0](https://github.com/expo/entity/compare/v0.70.0...v0.71.0) (2026-10-09)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.70.0](https://github.com/expo/entity/compare/v0.69.0...v0.70.0) (2026-10-05)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.61.0](https://github.com/expo/entity/compare/v0.60.0...v0.61.0) (2026-03-12)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.60.0](https://github.com/expo/entity/compare/v0.59.0...v0.60.0) (2026-03-06)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.59.0](https://github.com/expo/entity/compare/v0.58.0...v0.59.0) (2026-03-03)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.58.0](https://github.com/expo/entity/compare/v0.57.0...v0.58.0) (2026-03-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- escape all matching portions of parts of cacher cache keys ([#486](https://github.com/expo/entity/issues/486)) ([21f2f6f](https://github.com/expo/entity/commit/21f2f6f461ee771e8b1c1b22dbaf9b275094c0db))
|
||||
|
||||
# [0.57.0](https://github.com/expo/entity/compare/v0.56.0...v0.57.0) (2026-02-28)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.56.0](https://github.com/expo/entity/compare/v0.55.0...v0.56.0) (2026-02-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- eliminate some unnecessary any casts ([#448](https://github.com/expo/entity/issues/448)) ([cc78029](https://github.com/expo/entity/commit/cc78029117c14510b0934b5e0d19876a45570f8d))
|
||||
|
||||
# [0.55.0](https://github.com/expo/entity/compare/v0.54.0...v0.55.0) (2026-01-27)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.54.0](https://github.com/expo/entity/compare/v0.53.0...v0.54.0) (2026-01-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** pin dependencies ([#328](https://github.com/expo/entity/issues/328)) ([5679f27](https://github.com/expo/entity/commit/5679f27209f1515ca5e626d858c4aa054b414625))
|
||||
|
||||
# [0.53.0](https://github.com/expo/entity/compare/v0.52.0...v0.53.0) (2025-12-19)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.52.0](https://github.com/expo/entity/compare/v0.51.0...v0.52.0) (2025-12-19)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.51.0](https://github.com/expo/entity/compare/v0.50.0...v0.51.0) (2025-11-21)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.50.0](https://github.com/expo/entity/compare/v0.49.0...v0.50.0) (2025-10-07)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.49.0](https://github.com/expo/entity/compare/v0.48.0...v0.49.0) (2025-10-07)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.48.0](https://github.com/expo/entity/compare/v0.47.0...v0.48.0) (2025-09-19)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.47.0](https://github.com/expo/entity/compare/v0.46.0...v0.47.0) (2025-09-19)
|
||||
|
||||
### Features
|
||||
|
||||
- add support for Buffer fields ([#305](https://github.com/expo/entity/issues/305)) ([31b52a5](https://github.com/expo/entity/commit/31b52a5f75842f4810f66c12ad03a10d9192f889))
|
||||
|
||||
# [0.46.0](https://github.com/expo/entity/compare/v0.45.0...v0.46.0) (2025-06-26)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.45.0](https://github.com/expo/entity/compare/v0.44.0...v0.45.0) (2025-06-06)
|
||||
|
||||
### Features
|
||||
|
||||
- add ability to define a custom cache key invalidation version supplier ([#290](https://github.com/expo/entity/issues/290)) ([a3ab61b](https://github.com/expo/entity/commit/a3ab61bfa8d2eab3bbd9c703c5e483184bb67f13))
|
||||
|
||||
# [0.44.0](https://github.com/expo/entity/compare/v0.43.0...v0.44.0) (2025-05-29)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.43.0](https://github.com/expo/entity/compare/v0.42.0...v0.43.0) (2025-04-10)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.42.0](https://github.com/expo/entity/compare/v0.41.0...v0.42.0) (2025-04-10)
|
||||
|
||||
### Features
|
||||
|
||||
- add @expo/entity-testing-utils package ([#280](https://github.com/expo/entity/issues/280)) ([485894a](https://github.com/expo/entity/commit/485894af16e233d533a9480285bbda56812cbe0b))
|
||||
- add composite field loading and caching ([#272](https://github.com/expo/entity/issues/272)) ([f0aa0da](https://github.com/expo/entity/commit/f0aa0dafebdb56418cbd22dda437233d850db3e7))
|
||||
- change entity id field generic to field name and derive type where necessary ([#278](https://github.com/expo/entity/issues/278)) ([b7e524c](https://github.com/expo/entity/commit/b7e524c41892797608d0b884410b8d520a80a9ef))
|
||||
- convert batched/cached loader interface to holder pattern ([#271](https://github.com/expo/entity/issues/271)) ([06b3cb7](https://github.com/expo/entity/commit/06b3cb7f8076ef9e5b83cdf154da159801bd14cc)), closes [#201](https://github.com/expo/entity/issues/201)
|
||||
- invalidate n+/-1 cacheKeyVersion for entities for push safety ([#275](https://github.com/expo/entity/issues/275)) ([d9c1852](https://github.com/expo/entity/commit/d9c1852706f98f0761ff24ddd244e4ed2beca580))
|
||||
|
||||
# [0.41.0](https://github.com/expo/entity/compare/v0.40.0...v0.41.0) (2025-03-07)
|
||||
|
||||
- feat!: Default all loaders and mutators to enforcing (#256) ([a976e76](https://github.com/expo/entity/commit/a976e765098bca27049585e43a3bed5c71b9ac6b)), closes [#256](https://github.com/expo/entity/issues/256)
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
- Default all loaders and mutators to enforcing
|
||||
|
||||
# [0.40.0](https://github.com/expo/entity/compare/v0.39.0...v0.40.0) (2025-02-27)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "v0.40.0" ([fa78ce8](https://github.com/expo/entity/commit/fa78ce84e2f2eb8199e36fd51779fc68905001c0))
|
||||
|
||||
# [0.39.0](https://github.com/expo/entity/compare/v0.38.0...v0.39.0) (2024-11-21)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.38.0](https://github.com/expo/entity/compare/v0.37.0...v0.38.0) (2024-06-27)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
# [0.37.0](https://github.com/expo/entity/compare/v0.36.0...v0.37.0) (2024-06-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update barrels ([#241](https://github.com/expo/entity/issues/241)) ([8c5f81b](https://github.com/expo/entity/commit/8c5f81bcca107e9b7bbea5f0ab41b0105057806e))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
@@ -13,7 +13,13 @@ import Redis from 'ioredis';
|
||||
|
||||
const genericRedisCacherContext = {
|
||||
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-',
|
||||
ttlSecondsPositive: 86400, // 1 day
|
||||
ttlSecondsNegative: 600, // 10 minutes
|
||||
@@ -34,4 +40,4 @@ export const createDefaultEntityCompanionProvider = (
|
||||
}
|
||||
);
|
||||
};
|
||||
```
|
||||
```
|
||||
@@ -1,45 +1,37 @@
|
||||
{
|
||||
"name": "@expo/entity-cache-adapter-redis",
|
||||
"version": "0.72.0",
|
||||
"version": "0.37.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 --config ../../.barrelsby.json"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"keywords": [
|
||||
"entity"
|
||||
],
|
||||
"license": "MIT",
|
||||
"author": "Expo",
|
||||
"files": [
|
||||
"build",
|
||||
"!*.tsbuildinfo",
|
||||
"!__*",
|
||||
"src"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "build/src/index.js",
|
||||
"types": "build/src/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"prepack": "rm -rf build && yarn build",
|
||||
"clean": "yarn build --clean",
|
||||
"lint": "yarn run --top-level oxlint --type-aware src",
|
||||
"lint-fix": "yarn lint --fix",
|
||||
"test": "yarn test:all --rootDir $(pwd)",
|
||||
"integration": "yarn integration:all --rootDir $(pwd)"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/entity": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/batcher": "1.1.2",
|
||||
"@expo/entity-testing-utils": "workspace:^",
|
||||
"@jest/globals": "30.5.2",
|
||||
"ioredis": "5.11.1",
|
||||
"ts-mockito": "2.6.1",
|
||||
"typescript": "6.0.3"
|
||||
},
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@expo/entity": "*",
|
||||
"ioredis": ">=5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"devDependencies": {
|
||||
"@expo/entity": "^0.37.0",
|
||||
"ioredis": "^5.2.5"
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,15 @@
|
||||
import type {
|
||||
CacheLoadResult,
|
||||
EntityConfiguration,
|
||||
IEntityGenericCacher,
|
||||
IEntityLoadKey,
|
||||
IEntityLoadValue,
|
||||
} from '@expo/entity';
|
||||
import {
|
||||
CacheLoadResult,
|
||||
CacheStatus,
|
||||
EntityConfiguration,
|
||||
transformCacheObjectToFields,
|
||||
transformFieldsToCacheObject,
|
||||
IEntityGenericCacher,
|
||||
} from '@expo/entity';
|
||||
import invariant from 'invariant';
|
||||
|
||||
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.
|
||||
@@ -30,76 +26,6 @@ export interface IRedis {
|
||||
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
|
||||
@@ -107,15 +33,10 @@ export interface GenericRedisCacheContext {
|
||||
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.
|
||||
* 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.
|
||||
*/
|
||||
cacheKeyDelimiter: string;
|
||||
makeKeyFn: (...parts: string[]) => string;
|
||||
|
||||
/**
|
||||
* Prefix prepended to all entity cache keys. Useful for adding a short, human-readable
|
||||
@@ -134,23 +55,15 @@ 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 extends Record<string, any>>
|
||||
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[],
|
||||
@@ -234,67 +147,18 @@ 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 = [
|
||||
public 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 ${String(fieldName)}`);
|
||||
return this.context.makeKeyFn(
|
||||
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,
|
||||
`v2.${this.entityConfiguration.cacheKeyVersion}`,
|
||||
columnName,
|
||||
String(fieldValue),
|
||||
);
|
||||
}
|
||||
|
||||
public makeCacheKeysForInvalidation<
|
||||
TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
|
||||
TSerializedLoadValue,
|
||||
TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
|
||||
>(key: TLoadKey, value: TLoadValue): readonly string[] {
|
||||
switch (this.context.invalidationConfig.invalidationStrategy) {
|
||||
case RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION:
|
||||
return [
|
||||
this.makeCacheKeyForCacheKeyVersion(key, value, this.entityConfiguration.cacheKeyVersion),
|
||||
];
|
||||
case RedisCacheInvalidationStrategy.SURROUNDING_CACHE_KEY_VERSIONS:
|
||||
return getSurroundingCacheKeyVersionsForInvalidation(
|
||||
this.entityConfiguration.cacheKeyVersion,
|
||||
).map((cacheKeyVersion) =>
|
||||
this.makeCacheKeyForCacheKeyVersion(key, value, cacheKeyVersion),
|
||||
);
|
||||
case RedisCacheInvalidationStrategy.CUSTOM: {
|
||||
const cacheKeyVersions = this.context.invalidationConfig.cacheKeyVersionsToInvalidateFn(
|
||||
this.entityConfiguration.cacheKeyVersion,
|
||||
);
|
||||
if (cacheKeyVersions.length === 0) {
|
||||
throw new Error(
|
||||
`GenericRedisCacheContext.invalidationConfig.cacheKeyVersionsToInvalidateFn returned an empty list for cacheKeyVersion ${this.entityConfiguration.cacheKeyVersion}; this would silently disable invalidation`,
|
||||
);
|
||||
}
|
||||
return cacheKeyVersions.map((cacheKeyVersion) =>
|
||||
this.makeCacheKeyForCacheKeyVersion(key, value, cacheKeyVersion),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
import type {
|
||||
import {
|
||||
IEntityCacheAdapterProvider,
|
||||
EntityConfiguration,
|
||||
IEntityCacheAdapter,
|
||||
IEntityCacheAdapterProvider,
|
||||
GenericEntityCacheAdapter,
|
||||
} from '@expo/entity';
|
||||
import { GenericEntityCacheAdapter } from '@expo/entity';
|
||||
|
||||
import type { GenericRedisCacheContext } from './GenericRedisCacher.ts';
|
||||
import { GenericRedisCacher } from './GenericRedisCacher.ts';
|
||||
import GenericRedisCacher, { GenericRedisCacheContext } from './GenericRedisCacher';
|
||||
|
||||
export class RedisCacheAdapterProvider implements IEntityCacheAdapterProvider {
|
||||
export default class RedisCacheAdapterProvider implements IEntityCacheAdapterProvider {
|
||||
constructor(private readonly context: GenericRedisCacheContext) {}
|
||||
|
||||
getCacheAdapter<TFields extends Record<string, any>, TIDField extends keyof TFields>(
|
||||
entityConfiguration: EntityConfiguration<TFields, TIDField>,
|
||||
): IEntityCacheAdapter<TFields, TIDField> {
|
||||
getCacheAdapter<TFields extends Record<string, any>>(
|
||||
entityConfiguration: EntityConfiguration<TFields>,
|
||||
): IEntityCacheAdapter<TFields> {
|
||||
return new GenericEntityCacheAdapter(new GenericRedisCacher(this.context, entityConfiguration));
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
],
|
||||
]);
|
||||
+41
-51
@@ -1,18 +1,18 @@
|
||||
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 { ViewerContext, zipToMap } from '@expo/entity';
|
||||
import invariant from 'invariant';
|
||||
import { Redis } from 'ioredis';
|
||||
import Redis from 'ioredis';
|
||||
import nullthrows from 'nullthrows';
|
||||
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';
|
||||
import GenericRedisCacher, {
|
||||
IRedis,
|
||||
IRedisTransaction,
|
||||
GenericRedisCacheContext,
|
||||
} from '../GenericRedisCacher';
|
||||
import RedisTestEntity from '../testfixtures/RedisTestEntity';
|
||||
import { createRedisIntegrationTestEntityCompanionProvider } from '../testfixtures/createRedisIntegrationTestEntityCompanionProvider';
|
||||
|
||||
class BatchedRedis implements IRedis {
|
||||
private readonly mgetBatcher = new Batcher<string[], (string | null)[]>(
|
||||
@@ -68,13 +68,16 @@ describe(GenericRedisCacher, () => {
|
||||
beforeAll(() => {
|
||||
genericRedisCacheContext = {
|
||||
redisClient,
|
||||
cacheKeyDelimiter: ':',
|
||||
makeKeyFn(...parts: string[]): string {
|
||||
const delimiter = ':';
|
||||
const escapedParts = parts.map((part) =>
|
||||
part.replace('\\', '\\\\').replace(delimiter, `\\${delimiter}`),
|
||||
);
|
||||
return escapedParts.join(delimiter);
|
||||
},
|
||||
cacheKeyPrefix: 'test-',
|
||||
ttlSecondsPositive: 86400, // 1 day
|
||||
ttlSecondsNegative: 600, // 10 minutes
|
||||
invalidationConfig: {
|
||||
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -94,15 +97,15 @@ describe(GenericRedisCacher, () => {
|
||||
|
||||
const mgetSpy = jest.spyOn(redis, 'mget');
|
||||
|
||||
const genericCacher = (
|
||||
const genericCacher =
|
||||
viewerContext.entityCompanionProvider.getCompanionForEntity(RedisTestEntity)[
|
||||
'tableDataCoordinator'
|
||||
]['cacheAdapter'] as GenericEntityCacheAdapter<RedisTestEntityFields, 'id'>
|
||||
)['genericCacher'];
|
||||
]['cacheAdapter']['genericCacher'];
|
||||
const cacheKeyMaker = genericCacher['makeCacheKey'].bind(genericCacher);
|
||||
|
||||
const entity1Created = await RedisTestEntity.creator(viewerContext)
|
||||
.setField('name', 'blah')
|
||||
.createAsync();
|
||||
.enforceCreateAsync();
|
||||
|
||||
// loading an entity should put it in cache. load by multiple requests and multiple fields in same tick to ensure batch works
|
||||
mgetSpy.mockClear();
|
||||
@@ -116,12 +119,11 @@ describe(GenericRedisCacher, () => {
|
||||
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'),
|
||||
),
|
||||
RedisTestEntity.loader(viewerContext1).enforcing().loadByIDAsync(entity1Created.getID()),
|
||||
RedisTestEntity.loader(viewerContext2).enforcing().loadByIDAsync(entity1Created.getID()),
|
||||
RedisTestEntity.loader(viewerContext3)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('name', entity1Created.getField('name')),
|
||||
]);
|
||||
|
||||
expect(mgetSpy).toHaveBeenCalledTimes(1);
|
||||
@@ -129,10 +131,7 @@ describe(GenericRedisCacher, () => {
|
||||
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 cacheKeyEntity1 = cacheKeyMaker('id', entity1Created.getID());
|
||||
const cachedJSON = await redis.get(cacheKeyEntity1);
|
||||
const cachedValue = JSON.parse(cachedJSON!);
|
||||
expect(cachedValue).toMatchObject({
|
||||
@@ -140,40 +139,31 @@ describe(GenericRedisCacher, () => {
|
||||
name: 'blah',
|
||||
});
|
||||
|
||||
const cacheKeyEntity1NameField = genericCacher.makeCacheKeyForStorage(
|
||||
new SingleFieldHolder('name'),
|
||||
new SingleFieldValueHolder(entity1Created.getField('name')),
|
||||
);
|
||||
await RedisTestEntity.loader(viewerContext).loadByFieldEqualingAsync(
|
||||
'name',
|
||||
entity1Created.getField('name'),
|
||||
);
|
||||
const cacheKeyEntity1NameField = cacheKeyMaker('name', entity1Created.getField('name'));
|
||||
await RedisTestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.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,
|
||||
);
|
||||
const entityNonExistentResult = await RedisTestEntity.loader(viewerContext)
|
||||
.withAuthorizationResults()
|
||||
.loadByIDAsync(nonExistentId);
|
||||
expect(entityNonExistentResult.ok).toBe(false);
|
||||
const cacheKeyNonExistent = genericCacher.makeCacheKeyForStorage(
|
||||
new SingleFieldHolder('id'),
|
||||
new SingleFieldValueHolder(nonExistentId),
|
||||
);
|
||||
const cacheKeyNonExistent = cacheKeyMaker('id', 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,
|
||||
);
|
||||
const entityNonExistentResult2 = await RedisTestEntity.loader(viewerContext)
|
||||
.withAuthorizationResults()
|
||||
.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 RedisTestEntity.loader(viewerContext)
|
||||
.utils()
|
||||
.invalidateFieldsAsync(entity1.getAllFields());
|
||||
await expect(redis.get(cacheKeyEntity1)).resolves.toBeNull();
|
||||
await expect(redis.get(cacheKeyEntity1NameField)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
+49
-137
@@ -1,22 +1,12 @@
|
||||
import type { GenericEntityCacheAdapter } from '@expo/entity';
|
||||
import {
|
||||
CompositeFieldHolder,
|
||||
CompositeFieldValueHolder,
|
||||
SingleFieldHolder,
|
||||
SingleFieldValueHolder,
|
||||
ViewerContext,
|
||||
} from '@expo/entity';
|
||||
import { ViewerContext } from '@expo/entity';
|
||||
import { enforceAsyncResult } from '@expo/results';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from '@jest/globals';
|
||||
import { Redis } from 'ioredis';
|
||||
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';
|
||||
import GenericRedisCacher, { GenericRedisCacheContext } from '../GenericRedisCacher';
|
||||
import RedisTestEntity from '../testfixtures/RedisTestEntity';
|
||||
import { createRedisIntegrationTestEntityCompanionProvider } from '../testfixtures/createRedisIntegrationTestEntityCompanionProvider';
|
||||
|
||||
class TestViewerContext extends ViewerContext {}
|
||||
|
||||
@@ -26,13 +16,16 @@ describe(GenericRedisCacher, () => {
|
||||
beforeAll(() => {
|
||||
genericRedisCacheContext = {
|
||||
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-',
|
||||
ttlSecondsPositive: 86400, // 1 day
|
||||
ttlSecondsNegative: 600, // 10 minutes
|
||||
invalidationConfig: {
|
||||
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -47,114 +40,57 @@ describe(GenericRedisCacher, () => {
|
||||
const viewerContext = new TestViewerContext(
|
||||
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
|
||||
);
|
||||
const genericCacher = (
|
||||
const genericCacher =
|
||||
viewerContext.entityCompanionProvider.getCompanionForEntity(RedisTestEntity)[
|
||||
'tableDataCoordinator'
|
||||
]['cacheAdapter'] as GenericEntityCacheAdapter<RedisTestEntityFields, 'id'>
|
||||
)['genericCacher'];
|
||||
]['cacheAdapter']['genericCacher'];
|
||||
const cacheKeyMaker = genericCacher['makeCacheKey'].bind(genericCacher);
|
||||
|
||||
const entity1Created = await RedisTestEntity.creator(viewerContext)
|
||||
.setField('name', 'blah')
|
||||
.setField('dateField', new Date())
|
||||
.setField('bufferField', Buffer.from('hello world'))
|
||||
.createAsync();
|
||||
.enforceCreateAsync();
|
||||
|
||||
// loading an entity should put it in cache
|
||||
const entity1 = await RedisTestEntity.loader(viewerContext).loadByIDAsync(
|
||||
entity1Created.getID(),
|
||||
);
|
||||
const entity1 = await RedisTestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByIDAsync(entity1Created.getID());
|
||||
|
||||
const cachedSingleJSON = await (genericRedisCacheContext.redisClient as Redis).get(
|
||||
genericCacher.makeCacheKeyForStorage(
|
||||
new SingleFieldHolder('id'),
|
||||
new SingleFieldValueHolder(entity1.getID()),
|
||||
),
|
||||
const cachedJSON = await (genericRedisCacheContext.redisClient as Redis).get(
|
||||
cacheKeyMaker('id', entity1.getID()),
|
||||
);
|
||||
const cachedSingleValue = JSON.parse(cachedSingleJSON!);
|
||||
expect(cachedSingleValue).toMatchObject({
|
||||
const cachedValue = JSON.parse(cachedJSON!);
|
||||
expect(cachedValue).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,
|
||||
);
|
||||
const entityNonExistentResult = await RedisTestEntity.loader(viewerContext)
|
||||
.withAuthorizationResults()
|
||||
.loadByIDAsync(nonExistentId);
|
||||
expect(entityNonExistentResult.ok).toBe(false);
|
||||
|
||||
const nonExistentCachedValue = await (genericRedisCacheContext.redisClient as Redis).get(
|
||||
genericCacher.makeCacheKeyForStorage(
|
||||
new SingleFieldHolder('id'),
|
||||
new SingleFieldValueHolder(nonExistentId),
|
||||
),
|
||||
cacheKeyMaker('id', 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,
|
||||
);
|
||||
const entityNonExistentResult2 = await RedisTestEntity.loader(viewerContext)
|
||||
.withAuthorizationResults()
|
||||
.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(),
|
||||
await RedisTestEntity.loader(viewerContext)
|
||||
.utils()
|
||||
.invalidateFieldsAsync(entity1.getAllFields());
|
||||
const cachedValueNull = await (genericRedisCacheContext.redisClient as Redis).get(
|
||||
cacheKeyMaker('id', entity1.getID()),
|
||||
);
|
||||
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);
|
||||
expect(cachedValueNull).toBe(null);
|
||||
});
|
||||
|
||||
it('caches and restores date fields', async () => {
|
||||
@@ -163,20 +99,20 @@ describe(GenericRedisCacher, () => {
|
||||
);
|
||||
const date = new Date();
|
||||
const entity1 = await enforceAsyncResult(
|
||||
RedisTestEntity.creatorWithAuthorizationResults(viewerContext)
|
||||
.setField('dateField', date)
|
||||
.createAsync(),
|
||||
RedisTestEntity.creator(viewerContext).setField('dateField', date).createAsync(),
|
||||
);
|
||||
expect(entity1.getField('dateField')).toEqual(date);
|
||||
|
||||
const entity2 = await RedisTestEntity.loader(viewerContext).loadByIDAsync(entity1.getID());
|
||||
const entity2 = await RedisTestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.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());
|
||||
const entity3 = await RedisTestEntity.loader(vc2).enforcing().loadByIDAsync(entity1.getID());
|
||||
expect(entity3.getField('dateField')).toEqual(date);
|
||||
});
|
||||
|
||||
@@ -185,44 +121,20 @@ describe(GenericRedisCacher, () => {
|
||||
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
|
||||
);
|
||||
const entity1 = await enforceAsyncResult(
|
||||
RedisTestEntity.creatorWithAuthorizationResults(viewerContext)
|
||||
.setField('name', '')
|
||||
.createAsync(),
|
||||
);
|
||||
const entity2 = await RedisTestEntity.loader(viewerContext).loadByFieldEqualingAsync(
|
||||
'name',
|
||||
'',
|
||||
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(genericRedisCacheContext),
|
||||
);
|
||||
const entity3 = await RedisTestEntity.loader(vc2).loadByFieldEqualingAsync('name', '');
|
||||
const entity3 = await RedisTestEntity.loader(vc2)
|
||||
.enforcing()
|
||||
.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);
|
||||
});
|
||||
});
|
||||
+15
-21
@@ -1,16 +1,13 @@
|
||||
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, { GenericRedisCacheContext } from '../GenericRedisCacher';
|
||||
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 {}
|
||||
|
||||
@@ -20,13 +17,16 @@ describe(GenericRedisCacher, () => {
|
||||
beforeAll(() => {
|
||||
genericRedisCacheContext = {
|
||||
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-',
|
||||
ttlSecondsPositive: 86400, // 1 day
|
||||
ttlSecondsNegative: 600, // 10 minutes
|
||||
invalidationConfig: {
|
||||
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -46,12 +46,10 @@ describe(GenericRedisCacher, () => {
|
||||
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()],
|
||||
@@ -73,7 +71,6 @@ describe(GenericRedisCacher, () => {
|
||||
});
|
||||
expect(loadedObjectMap.size).toBe(1);
|
||||
});
|
||||
|
||||
it('has correct negative caching behaviour', async () => {
|
||||
const genericRedisCacher = new GenericRedisCacher(
|
||||
genericRedisCacheContext,
|
||||
@@ -86,7 +83,6 @@ 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),
|
||||
@@ -96,12 +92,10 @@ describe(GenericRedisCacher, () => {
|
||||
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()],
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
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 GenericRedisCacher, { GenericRedisCacheContext } from '../GenericRedisCacher';
|
||||
import RedisTestEntity from '../testfixtures/RedisTestEntity';
|
||||
import { createRedisIntegrationTestEntityCompanionProvider } from '../testfixtures/createRedisIntegrationTestEntityCompanionProvider';
|
||||
|
||||
class TestViewerContext extends ViewerContext {}
|
||||
|
||||
@@ -17,13 +15,16 @@ describe(GenericRedisCacher, () => {
|
||||
beforeAll(() => {
|
||||
genericRedisCacheContext = {
|
||||
redisClient,
|
||||
cacheKeyDelimiter: ':',
|
||||
makeKeyFn(...parts: string[]): string {
|
||||
const delimiter = ':';
|
||||
const escapedParts = parts.map((part) =>
|
||||
part.replace('\\', '\\\\').replace(delimiter, `\\${delimiter}`),
|
||||
);
|
||||
return escapedParts.join(delimiter);
|
||||
},
|
||||
cacheKeyPrefix: 'test-',
|
||||
ttlSecondsPositive: 86400, // 1 day
|
||||
ttlSecondsNegative: 600, // 10 minutes
|
||||
invalidationConfig: {
|
||||
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -39,7 +40,7 @@ describe(GenericRedisCacher, () => {
|
||||
);
|
||||
|
||||
await expect(
|
||||
RedisTestEntity.creator(vc1).setField('name', 'blah').createAsync(),
|
||||
RedisTestEntity.creator(vc1).setField('name', 'blah').enforceCreateAsync(),
|
||||
).rejects.toThrow(EntityCacheAdapterTransientError);
|
||||
});
|
||||
});
|
||||
@@ -1,33 +1,18 @@
|
||||
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 { CacheStatus, UUIDField, EntityConfiguration } from '@expo/entity';
|
||||
import { Redis, Pipeline } from 'ioredis';
|
||||
import { mock, when, instance, anything, verify } from 'ts-mockito';
|
||||
|
||||
import { GenericRedisCacher, RedisCacheInvalidationStrategy } from '../GenericRedisCacher.ts';
|
||||
import GenericRedisCacher from '../GenericRedisCacher';
|
||||
|
||||
type BlahFields = {
|
||||
id: string;
|
||||
fieldA: string;
|
||||
fieldB: string;
|
||||
};
|
||||
|
||||
const entityConfiguration = new EntityConfiguration<BlahFields, 'id'>({
|
||||
const entityConfiguration = new EntityConfiguration<BlahFields>({
|
||||
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',
|
||||
@@ -48,29 +33,17 @@ describe(GenericRedisCacher, () => {
|
||||
const genericCacher = new GenericRedisCacher(
|
||||
{
|
||||
redisClient: instance(mockRedisClient),
|
||||
cacheKeyDelimiter: ':',
|
||||
makeKeyFn: (...parts) => parts.join(':'),
|
||||
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'),
|
||||
);
|
||||
const cacheKeyWat = genericCacher['makeCacheKey']('id', 'wat');
|
||||
const cacheKeyWho = genericCacher['makeCacheKey']('id', 'who');
|
||||
const cacheKeyWhy = genericCacher['makeCacheKey']('id', 'why');
|
||||
|
||||
redisResults.set(cacheKeyWat, JSON.stringify({ id: 'wat' }));
|
||||
redisResults.set(cacheKeyWho, '');
|
||||
@@ -90,13 +63,10 @@ describe(GenericRedisCacher, () => {
|
||||
const genericCacher = new GenericRedisCacher(
|
||||
{
|
||||
redisClient: instance(mock<Redis>()),
|
||||
cacheKeyDelimiter: ':',
|
||||
makeKeyFn: (...parts) => parts.join(':'),
|
||||
cacheKeyPrefix: 'hello-',
|
||||
ttlSecondsPositive: 1,
|
||||
ttlSecondsNegative: 2,
|
||||
invalidationConfig: {
|
||||
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
|
||||
},
|
||||
},
|
||||
entityConfiguration,
|
||||
);
|
||||
@@ -116,7 +86,7 @@ describe(GenericRedisCacher, () => {
|
||||
return pipeline;
|
||||
},
|
||||
);
|
||||
when(mockPipeline.exec()).thenResolve([]);
|
||||
when(mockPipeline.exec()).thenResolve({} as any);
|
||||
const pipeline = instance(mockPipeline);
|
||||
|
||||
const mockRedisClient = mock<Redis>();
|
||||
@@ -125,28 +95,20 @@ describe(GenericRedisCacher, () => {
|
||||
const genericCacher = new GenericRedisCacher(
|
||||
{
|
||||
redisClient: instance(mockRedisClient),
|
||||
cacheKeyDelimiter: ':',
|
||||
makeKeyFn: (...parts) => parts.join(':'),
|
||||
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 cacheKey = genericCacher['makeCacheKey']('id', 'wat');
|
||||
|
||||
const value = { id: 'wat', fieldA: 'a', fieldB: 'b' };
|
||||
|
||||
await genericCacher.cacheManyAsync(new Map([[cacheKey, value]]));
|
||||
await genericCacher.cacheManyAsync(new Map([[cacheKey, { id: 'wat' }]]));
|
||||
|
||||
expect(redisResults.get(cacheKey)).toMatchObject({
|
||||
value: JSON.stringify(value),
|
||||
value: JSON.stringify({ id: 'wat' }),
|
||||
code: 'EX',
|
||||
ttl: 1,
|
||||
});
|
||||
@@ -163,7 +125,7 @@ describe(GenericRedisCacher, () => {
|
||||
return pipeline;
|
||||
},
|
||||
);
|
||||
when(mockPipeline.exec()).thenResolve([]);
|
||||
when(mockPipeline.exec()).thenResolve({} as any);
|
||||
const pipeline = instance(mockPipeline);
|
||||
|
||||
const mockRedisClient = mock<Redis>();
|
||||
@@ -172,21 +134,15 @@ describe(GenericRedisCacher, () => {
|
||||
const genericCacher = new GenericRedisCacher(
|
||||
{
|
||||
redisClient: instance(mockRedisClient),
|
||||
cacheKeyDelimiter: ':',
|
||||
makeKeyFn: (...parts) => parts.join(':'),
|
||||
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 cacheKey = genericCacher['makeCacheKey']('id', 'wat');
|
||||
|
||||
await genericCacher.cacheDBMissesAsync([cacheKey]);
|
||||
|
||||
@@ -199,230 +155,39 @@ describe(GenericRedisCacher, () => {
|
||||
});
|
||||
|
||||
describe('invalidateManyAsync', () => {
|
||||
it('invalidates correctly with RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION', async () => {
|
||||
it('invalidates correctly', async () => {
|
||||
const mockRedisClient = mock<Redis>();
|
||||
when(mockRedisClient.del()).thenResolve(1);
|
||||
|
||||
const genericCacher = new GenericRedisCacher(
|
||||
{
|
||||
redisClient: instance(mockRedisClient),
|
||||
cacheKeyDelimiter: ':',
|
||||
makeKeyFn: (...parts) => parts.join(':'),
|
||||
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');
|
||||
const cacheKey = genericCacher['makeCacheKey']('id', 'wat');
|
||||
|
||||
await genericCacher.invalidateManyAsync(cacheKeys);
|
||||
verify(mockRedisClient.del(...cacheKeys)).once();
|
||||
});
|
||||
await genericCacher.invalidateManyAsync([cacheKey]);
|
||||
|
||||
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();
|
||||
verify(mockRedisClient.del(cacheKey)).once();
|
||||
});
|
||||
|
||||
it('returns when passed empty array of fieldValues', async () => {
|
||||
const genericCacher = new GenericRedisCacher(
|
||||
{
|
||||
redisClient: instance(mock<Redis>()),
|
||||
cacheKeyDelimiter: ':',
|
||||
makeKeyFn: (...parts) => parts.join(':'),
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
+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,8 @@
|
||||
* @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 GenericRedisCacher } from './GenericRedisCacher';
|
||||
export * from './GenericRedisCacher';
|
||||
export { default as RedisCacheAdapterProvider } from './RedisCacheAdapterProvider';
|
||||
export * from './RedisCommon';
|
||||
export { default as wrapNativeRedisCallAsync } from './errors/wrapNativeRedisCallAsync';
|
||||
+34
-24
@@ -1,26 +1,25 @@
|
||||
import type { EntityCompanionDefinition, ViewerContext } from '@expo/entity';
|
||||
import {
|
||||
AlwaysAllowPrivacyPolicyRule,
|
||||
BufferField,
|
||||
DateField,
|
||||
Entity,
|
||||
EntityConfiguration,
|
||||
EntityPrivacyPolicy,
|
||||
StringField,
|
||||
ViewerContext,
|
||||
UUIDField,
|
||||
DateField,
|
||||
StringField,
|
||||
EntityConfiguration,
|
||||
EntityCompanionDefinition,
|
||||
Entity,
|
||||
} from '@expo/entity';
|
||||
|
||||
export type RedisTestEntityFields = {
|
||||
id: string;
|
||||
name: string;
|
||||
dateField: Date | null;
|
||||
bufferField: Buffer | null;
|
||||
};
|
||||
|
||||
export class RedisTestEntity extends Entity<RedisTestEntityFields, 'id', ViewerContext> {
|
||||
export default class RedisTestEntity extends Entity<RedisTestEntityFields, string, ViewerContext> {
|
||||
static defineCompanionDefinition(): EntityCompanionDefinition<
|
||||
RedisTestEntityFields,
|
||||
'id',
|
||||
string,
|
||||
ViewerContext,
|
||||
RedisTestEntity,
|
||||
RedisTestEntityPrivacyPolicy
|
||||
@@ -35,25 +34,45 @@ export class RedisTestEntity extends Entity<RedisTestEntityFields, 'id', ViewerC
|
||||
|
||||
export class RedisTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
RedisTestEntityFields,
|
||||
'id',
|
||||
string,
|
||||
ViewerContext,
|
||||
RedisTestEntity
|
||||
> {
|
||||
protected override readonly createRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<RedisTestEntityFields, 'id', ViewerContext, RedisTestEntity>(),
|
||||
new AlwaysAllowPrivacyPolicyRule<
|
||||
RedisTestEntityFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
RedisTestEntity
|
||||
>(),
|
||||
];
|
||||
protected override readonly readRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<RedisTestEntityFields, 'id', ViewerContext, RedisTestEntity>(),
|
||||
new AlwaysAllowPrivacyPolicyRule<
|
||||
RedisTestEntityFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
RedisTestEntity
|
||||
>(),
|
||||
];
|
||||
protected override readonly updateRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<RedisTestEntityFields, 'id', ViewerContext, RedisTestEntity>(),
|
||||
new AlwaysAllowPrivacyPolicyRule<
|
||||
RedisTestEntityFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
RedisTestEntity
|
||||
>(),
|
||||
];
|
||||
protected override readonly deleteRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<RedisTestEntityFields, 'id', ViewerContext, RedisTestEntity>(),
|
||||
new AlwaysAllowPrivacyPolicyRule<
|
||||
RedisTestEntityFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
RedisTestEntity
|
||||
>(),
|
||||
];
|
||||
}
|
||||
|
||||
export const redisTestEntityConfiguration = new EntityConfiguration<RedisTestEntityFields, 'id'>({
|
||||
export const redisTestEntityConfiguration = new EntityConfiguration<RedisTestEntityFields>({
|
||||
idField: 'id',
|
||||
tableName: 'redis_test_entities',
|
||||
schema: {
|
||||
@@ -68,16 +87,7 @@ export const redisTestEntityConfiguration = new EntityConfiguration<RedisTestEnt
|
||||
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 },
|
||||
],
|
||||
});
|
||||
+10
-7
@@ -1,13 +1,16 @@
|
||||
import type { IEntityMetricsAdapter } from '@expo/entity';
|
||||
import { EntityCompanionProvider, NoOpEntityMetricsAdapter } from '@expo/entity';
|
||||
import { StubDatabaseAdapterProvider, StubQueryContextProvider } from '@expo/entity-testing-utils';
|
||||
import {
|
||||
NoOpEntityMetricsAdapter,
|
||||
IEntityMetricsAdapter,
|
||||
EntityCompanionProvider,
|
||||
StubQueryContextProvider,
|
||||
StubDatabaseAdapterProvider,
|
||||
} from '@expo/entity';
|
||||
|
||||
import type { GenericRedisCacheContext } from '../GenericRedisCacher.ts';
|
||||
import { RedisCacheAdapterProvider } from '../RedisCacheAdapterProvider.ts';
|
||||
import { GenericRedisCacheContext } from '../GenericRedisCacher';
|
||||
import RedisCacheAdapterProvider from '../RedisCacheAdapterProvider';
|
||||
|
||||
// share across all in calls in test to simulate postgres
|
||||
const adapterProvider = new StubDatabaseAdapterProvider();
|
||||
const queryContextProvider = new StubQueryContextProvider();
|
||||
|
||||
export const createRedisIntegrationTestEntityCompanionProvider = (
|
||||
genericRedisCacheContext: GenericRedisCacheContext,
|
||||
@@ -20,7 +23,7 @@ export const createRedisIntegrationTestEntityCompanionProvider = (
|
||||
'postgres',
|
||||
{
|
||||
adapterProvider,
|
||||
queryContextProvider,
|
||||
queryContextProvider: StubQueryContextProvider,
|
||||
},
|
||||
],
|
||||
]),
|
||||
-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,145 +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.72.0](https://github.com/expo/entity/compare/v0.71.0...v0.72.0) (2026-10-29)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.71.0](https://github.com/expo/entity/compare/v0.70.0...v0.71.0) (2026-10-09)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.70.0](https://github.com/expo/entity/compare/v0.69.0...v0.70.0) (2026-10-05)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.61.0](https://github.com/expo/entity/compare/v0.60.0...v0.61.0) (2026-03-12)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.60.0](https://github.com/expo/entity/compare/v0.59.0...v0.60.0) (2026-03-06)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.59.0](https://github.com/expo/entity/compare/v0.58.0...v0.59.0) (2026-03-03)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.58.0](https://github.com/expo/entity/compare/v0.57.0...v0.58.0) (2026-03-02)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.57.0](https://github.com/expo/entity/compare/v0.56.0...v0.57.0) (2026-02-28)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.56.0](https://github.com/expo/entity/compare/v0.55.0...v0.56.0) (2026-02-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- refactor install method to instead be free functions ([#441](https://github.com/expo/entity/issues/441)) ([51d632d](https://github.com/expo/entity/commit/51d632da1619dc039d5c41ce6533f63254ec4337)), closes [#410](https://github.com/expo/entity/issues/410)
|
||||
- revert change to codemod now that Entity.knexLoader is supported again via subclass ([#443](https://github.com/expo/entity/issues/443)) ([ed6b2e0](https://github.com/expo/entity/commit/ed6b2e05539c77ee2abd2c6da74e2a451967d06e)), closes [#441](https://github.com/expo/entity/issues/441)
|
||||
|
||||
### Features
|
||||
|
||||
- add codemod for upcoming v0.55.0-v0.56.0 ([#413](https://github.com/expo/entity/issues/413)) ([aba0ce5](https://github.com/expo/entity/commit/aba0ce5b5f38cb7b01956cde14a4714e2a03abb4)), closes [#407](https://github.com/expo/entity/issues/407) [-#410](https://github.com/-/issues/410)
|
||||
|
||||
# [0.55.0](https://github.com/expo/entity/compare/v0.54.0...v0.55.0) (2026-01-27)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.54.0](https://github.com/expo/entity/compare/v0.53.0...v0.54.0) (2026-01-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** pin dependencies ([#328](https://github.com/expo/entity/issues/328)) ([5679f27](https://github.com/expo/entity/commit/5679f27209f1515ca5e626d858c4aa054b414625))
|
||||
|
||||
# [0.53.0](https://github.com/expo/entity/compare/v0.52.0...v0.53.0) (2025-12-19)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.52.0](https://github.com/expo/entity/compare/v0.51.0...v0.52.0) (2025-12-19)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.51.0](https://github.com/expo/entity/compare/v0.50.0...v0.51.0) (2025-11-21)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.50.0](https://github.com/expo/entity/compare/v0.49.0...v0.50.0) (2025-10-07)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.49.0](https://github.com/expo/entity/compare/v0.48.0...v0.49.0) (2025-10-07)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.48.0](https://github.com/expo/entity/compare/v0.47.0...v0.48.0) (2025-09-19)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.47.0](https://github.com/expo/entity/compare/v0.46.0...v0.47.0) (2025-09-19)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.46.0](https://github.com/expo/entity/compare/v0.45.0...v0.46.0) (2025-06-26)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.45.0](https://github.com/expo/entity/compare/v0.44.0...v0.45.0) (2025-06-06)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.44.0](https://github.com/expo/entity/compare/v0.43.0...v0.44.0) (2025-05-29)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.43.0](https://github.com/expo/entity/compare/v0.42.0...v0.43.0) (2025-04-10)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.42.0](https://github.com/expo/entity/compare/v0.41.0...v0.42.0) (2025-04-10)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-codemod
|
||||
|
||||
# [0.41.0](https://github.com/expo/entity/compare/v0.40.0...v0.41.0) (2025-03-07)
|
||||
|
||||
### Features
|
||||
|
||||
- Create codemod for v0.40.0 -> v0.41.0 ([#260](https://github.com/expo/entity/issues/260)) ([4a2b61f](https://github.com/expo/entity/commit/4a2b61f2bd8edfc19109ef4c8ac780eaf0f7795c))
|
||||
|
||||
# [0.40.0](https://github.com/expo/entity/compare/v0.39.0...v0.40.0) (2025-02-27)
|
||||
|
||||
### Features
|
||||
|
||||
- Create codemod for v0.39.0 -> v0.40.0 ([#259](https://github.com/expo/entity/issues/259)) ([d63ddd6](https://github.com/expo/entity/commit/d63ddd65997a441a914a71b158a111339970aeab))
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "v0.40.0" ([fa78ce8](https://github.com/expo/entity/commit/fa78ce84e2f2eb8199e36fd51779fc68905001c0))
|
||||
@@ -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.72.0",
|
||||
"description": "jscodeshift codemods for @expo/entity upgrades",
|
||||
"keywords": [
|
||||
"entity"
|
||||
],
|
||||
"license": "MIT",
|
||||
"author": "Expo",
|
||||
"files": [
|
||||
"build",
|
||||
"!*.tsbuildinfo",
|
||||
"!__*",
|
||||
"src"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "build/src/index.js",
|
||||
"types": "build/src/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"prepack": "rm -rf build && yarn build",
|
||||
"clean": "yarn build --clean",
|
||||
"lint": "yarn run --top-level oxlint --type-aware src",
|
||||
"lint-fix": "yarn lint --fix",
|
||||
"test": "yarn test:all --rootDir $(pwd)"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "30.5.2",
|
||||
"@types/jscodeshift": "17.3.0",
|
||||
"@types/node": "24.19.2",
|
||||
"jscodeshift": "17.4.0",
|
||||
"typescript": "6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// oxlint-disable unicorn/no-empty-file
|
||||
// placeholder file to fix tsc/typedoc
|
||||
-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,80 +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.72.0](https://github.com/expo/entity/compare/v0.71.0...v0.72.0) (2026-10-29)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
|
||||
|
||||
# [0.71.0](https://github.com/expo/entity/compare/v0.70.0...v0.71.0) (2026-10-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- declare runtime dependencies imported by published packages ([#717](https://github.com/expo/entity/issues/717)) ([3c96f3b](https://github.com/expo/entity/commit/3c96f3b9ea2b097c979447354f0abe35ed5e8d57))
|
||||
|
||||
# [0.70.0](https://github.com/expo/entity/compare/v0.69.0...v0.70.0) (2026-10-05)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
|
||||
|
||||
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
|
||||
|
||||
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
|
||||
|
||||
### Features
|
||||
|
||||
- add forShare and skipLocked selection modifiers to knex loader ([#705](https://github.com/expo/entity/issues/705)) ([243a038](https://github.com/expo/entity/commit/243a0386fb9755f3926ade8dcc1b92adf38cd923))
|
||||
- add forUpdate to knex loader ([#704](https://github.com/expo/entity/issues/704)) ([727f166](https://github.com/expo/entity/commit/727f16662d8baa3757bb8e94f51d837f17fd333d))
|
||||
|
||||
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
|
||||
|
||||
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
|
||||
|
||||
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
|
||||
|
||||
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
|
||||
|
||||
# [0.61.0](https://github.com/expo/entity/compare/v0.60.0...v0.61.0) (2026-03-12)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
|
||||
|
||||
# [0.60.0](https://github.com/expo/entity/compare/v0.59.0...v0.60.0) (2026-03-06)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
|
||||
|
||||
# [0.59.0](https://github.com/expo/entity/compare/v0.58.0...v0.59.0) (2026-03-03)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
|
||||
|
||||
# [0.58.0](https://github.com/expo/entity/compare/v0.57.0...v0.58.0) (2026-03-02)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
|
||||
|
||||
# [0.57.0](https://github.com/expo/entity/compare/v0.56.0...v0.57.0) (2026-02-28)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
|
||||
|
||||
# [0.56.0](https://github.com/expo/entity/compare/v0.55.0...v0.56.0) (2026-02-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- move EntityPrivacyUtils back into core package ([#429](https://github.com/expo/entity/issues/429)) ([56ec27d](https://github.com/expo/entity/commit/56ec27d5f201d83930a2cc05dfc380a054bff1e1))
|
||||
- pass in augmentable classes to installation ([#437](https://github.com/expo/entity/issues/437)) ([f5c728b](https://github.com/expo/entity/commit/f5c728be565e01c0a8ace9ef51f6de8f49184dab)), closes [/github.com/expo/entity/pull/410#pullrequestreview-3765893898](https://github.com//github.com/expo/entity/pull/410/issues/pullrequestreview-3765893898)
|
||||
- refactor install method to instead be free functions ([#441](https://github.com/expo/entity/issues/441)) ([51d632d](https://github.com/expo/entity/commit/51d632da1619dc039d5c41ce6533f63254ec4337)), closes [#410](https://github.com/expo/entity/issues/410)
|
||||
|
||||
### Features
|
||||
|
||||
- add entity-database-adapter-knex-testing-utils containing StubPostgresDatabaseAdapter ([#412](https://github.com/expo/entity/issues/412)) ([eeccf59](https://github.com/expo/entity/commit/eeccf594c11bf090aa83bbf71fb4ead8668aec7d))
|
||||
- add entityField SQL helper ([#481](https://github.com/expo/entity/issues/481)) ([459a5ff](https://github.com/expo/entity/commit/459a5ff85014a8c7396c0856dffacb1491ccb963))
|
||||
- Add raw sqlfragment orderBy capability for pagination ([#457](https://github.com/expo/entity/issues/457)) ([0615ffa](https://github.com/expo/entity/commit/0615ffacffbe9e903a9bf03b171c102f0554b992))
|
||||
- add support for nulls ordering in order by clauses ([#468](https://github.com/expo/entity/issues/468)) ([6c0838d](https://github.com/expo/entity/commit/6c0838dc577a3b6190e3551ee834925d7bd53bc6))
|
||||
@@ -1,3 +0,0 @@
|
||||
# @expo/entity-database-adapter-knex-testing-utils
|
||||
|
||||
Testing utilities for applications using Entity with Knex database adapter.
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"name": "@expo/entity-database-adapter-knex-testing-utils",
|
||||
"version": "0.72.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:^",
|
||||
"@expo/entity-testing-utils": "workspace:^",
|
||||
"invariant": "^2.2.4",
|
||||
"uuid": "^14.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "30.5.2",
|
||||
"@types/invariant": "2.2.37",
|
||||
"@types/node": "24.19.2",
|
||||
"typescript": "6.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@jest/globals": "*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
-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,
|
||||
};
|
||||
}
|
||||
}
|
||||
-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,439 +3,429 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.72.0](https://github.com/expo/entity/compare/v0.71.0...v0.72.0) (2026-10-29)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
# [0.71.0](https://github.com/expo/entity/compare/v0.70.0...v0.71.0) (2026-10-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- declare runtime dependencies imported by published packages ([#717](https://github.com/expo/entity/issues/717)) ([3c96f3b](https://github.com/expo/entity/commit/3c96f3b9ea2b097c979447354f0abe35ed5e8d57))
|
||||
|
||||
# [0.70.0](https://github.com/expo/entity/compare/v0.69.0...v0.70.0) (2026-10-05)
|
||||
|
||||
### Features
|
||||
|
||||
- add transient error for PostgreSQL lock timeouts ([#716](https://github.com/expo/entity/issues/716)) ([13ce74e](https://github.com/expo/entity/commit/13ce74e93aec0a0b0b84edd1e1e89d785a3dbf46)), closes [#714](https://github.com/expo/entity/issues/714) [#715](https://github.com/expo/entity/issues/715)
|
||||
|
||||
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
|
||||
|
||||
### Features
|
||||
|
||||
- add ID-based and field-based knex database loaders for use with locking modifiers ([#706](https://github.com/expo/entity/issues/706)) ([e87e2a3](https://github.com/expo/entity/commit/e87e2a3a28f4279f5911bfd0189964b94a2ced7f))
|
||||
|
||||
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
|
||||
|
||||
### Features
|
||||
|
||||
- add forShare and skipLocked selection modifiers to knex loader ([#705](https://github.com/expo/entity/issues/705)) ([243a038](https://github.com/expo/entity/commit/243a0386fb9755f3926ade8dcc1b92adf38cd923))
|
||||
- add forUpdate to knex loader ([#704](https://github.com/expo/entity/issues/704)) ([727f166](https://github.com/expo/entity/commit/727f16662d8baa3757bb8e94f51d837f17fd333d))
|
||||
|
||||
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- only throw EntityDatabaseAdapterPaginationCursorInvalidError from cursor decoding logic ([#537](https://github.com/expo/entity/issues/537)) ([e3cee07](https://github.com/expo/entity/commit/e3cee07091af8d1e6c5bb2d82f13e837c7658ea9))
|
||||
|
||||
# [0.61.0](https://github.com/expo/entity/compare/v0.60.0...v0.61.0) (2026-03-12)
|
||||
|
||||
### Features
|
||||
|
||||
- update the SQLExpression and SQLFragmentHelpers APIs ([#516](https://github.com/expo/entity/issues/516)) ([112d4dc](https://github.com/expo/entity/commit/112d4dc9b379da5317ff6acaf1d2e9bd597128a6))
|
||||
|
||||
# [0.60.0](https://github.com/expo/entity/compare/v0.59.0...v0.60.0) (2026-03-06)
|
||||
|
||||
### Features
|
||||
|
||||
- Add SQLExpression class that supports fluent comparison method API ([#506](https://github.com/expo/entity/issues/506)) ([535b681](https://github.com/expo/entity/commit/535b6811e08578aba1aadb095f3e401cfe7eaa26))
|
||||
|
||||
# [0.59.0](https://github.com/expo/entity/compare/v0.58.0...v0.59.0) (2026-03-03)
|
||||
|
||||
### Features
|
||||
|
||||
- add support to SQLFragment for `= ANY(?)` ([#498](https://github.com/expo/entity/issues/498)) ([b931daa](https://github.com/expo/entity/commit/b931daa9a082cdb33f4559d6d32e1655f0459b79))
|
||||
|
||||
# [0.58.0](https://github.com/expo/entity/compare/v0.57.0...v0.58.0) (2026-03-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix some security review suggestions ([#485](https://github.com/expo/entity/issues/485)) ([e263bd2](https://github.com/expo/entity/commit/e263bd218cda579365766d0918c4961304bd1d9f))
|
||||
|
||||
# [0.57.0](https://github.com/expo/entity/compare/v0.56.0...v0.57.0) (2026-02-28)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
# [0.56.0](https://github.com/expo/entity/compare/v0.55.0...v0.56.0) (2026-02-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Add default for TSelectedFields for application-instantiated types ([#438](https://github.com/expo/entity/issues/438)) ([3d4aa0e](https://github.com/expo/entity/commit/3d4aa0e9efa519a36f3db69bd8f117a993c100fa))
|
||||
- Apply same viewer context subclass type mechanism to knexLoader and related methods ([#472](https://github.com/expo/entity/issues/472)) ([018a40b](https://github.com/expo/entity/commit/018a40bc01d579ffbf88f308e641921e6fc39097))
|
||||
- correct discriminated union type for forward/backward pagination ([#474](https://github.com/expo/entity/issues/474)) ([90ae6e2](https://github.com/expo/entity/commit/90ae6e299bda26ac9a80d83e83334d4152c022dd))
|
||||
- correct pagination behavior for standard pagination with descending ordering clauses ([#477](https://github.com/expo/entity/issues/477)) ([e32c4c2](https://github.com/expo/entity/commit/e32c4c2bcc05f225a556f95c90f1ee0f4e24de74))
|
||||
- correct precedence parentheses for SQLFragment joins ([#476](https://github.com/expo/entity/issues/476)) ([2c4c649](https://github.com/expo/entity/commit/2c4c649a4bca5bc9ffdc41eb3135b2dfe4db5e57)), closes [/github.com/expo/entity/blob/main/packages/entity-database-adapter-knex/src/internal/EntityKnexDataManager.ts#L303](https://github.com//github.com/expo/entity/blob/main/packages/entity-database-adapter-knex/src/internal/EntityKnexDataManager.ts/issues/L303)
|
||||
- eliminate some unnecessary any casts ([#448](https://github.com/expo/entity/issues/448)) ([cc78029](https://github.com/expo/entity/commit/cc78029117c14510b0934b5e0d19876a45570f8d))
|
||||
- explicitly document behavior when cursor entity no longer exists ([#453](https://github.com/expo/entity/issues/453)) ([4dc156c](https://github.com/expo/entity/commit/4dc156c7a8dc50bc9e2802be133b9e258b289f54)), closes [#422](https://github.com/expo/entity/issues/422) [#431](https://github.com/expo/entity/issues/431)
|
||||
- move EntityPrivacyUtils back into core package ([#429](https://github.com/expo/entity/issues/429)) ([56ec27d](https://github.com/expo/entity/commit/56ec27d5f201d83930a2cc05dfc380a054bff1e1))
|
||||
- pass in augmentable classes to installation ([#437](https://github.com/expo/entity/issues/437)) ([f5c728b](https://github.com/expo/entity/commit/f5c728be565e01c0a8ace9ef51f6de8f49184dab)), closes [/github.com/expo/entity/pull/410#pullrequestreview-3765893898](https://github.com//github.com/expo/entity/pull/410/issues/pullrequestreview-3765893898)
|
||||
- refactor install method to instead be free functions ([#441](https://github.com/expo/entity/issues/441)) ([51d632d](https://github.com/expo/entity/commit/51d632da1619dc039d5c41ce6533f63254ec4337)), closes [#410](https://github.com/expo/entity/issues/410)
|
||||
- update integration test to use BlahEntity.knexLoader syntax ([#444](https://github.com/expo/entity/issues/444)) ([457f58a](https://github.com/expo/entity/commit/457f58ad2c4e1cf09f3132f3b9af5008ba0290c7)), closes [#441](https://github.com/expo/entity/issues/441)
|
||||
- Use TSelectedFields for knex loader order by method ([#424](https://github.com/expo/entity/issues/424)) ([a9d09f4](https://github.com/expo/entity/commit/a9d09f42b67378d06133bcfb5b99c9afb17aac0d))
|
||||
|
||||
### Features
|
||||
|
||||
- add entity-database-adapter-knex-testing-utils containing StubPostgresDatabaseAdapter ([#412](https://github.com/expo/entity/issues/412)) ([eeccf59](https://github.com/expo/entity/commit/eeccf594c11bf090aa83bbf71fb4ead8668aec7d))
|
||||
- add entityField SQL helper ([#481](https://github.com/expo/entity/issues/481)) ([459a5ff](https://github.com/expo/entity/commit/459a5ff85014a8c7396c0856dffacb1491ccb963))
|
||||
- Add ilike and trigram similarity search to pagination ([#431](https://github.com/expo/entity/issues/431)) ([f72cbca](https://github.com/expo/entity/commit/f72cbcaf5a722e6bb8299644ec32b07f1d3362a9)), closes [#422](https://github.com/expo/entity/issues/422)
|
||||
- add method to get pagination cursor for single entity ([#475](https://github.com/expo/entity/issues/475)) ([898f71c](https://github.com/expo/entity/commit/898f71ce082c2593585575b60bd6ef2546717124))
|
||||
- Add paginated loader to entity-database-adapter-knex ([#422](https://github.com/expo/entity/issues/422)) ([58deffd](https://github.com/expo/entity/commit/58deffd426b0214073ede608cd47c04db2596d60))
|
||||
- Add pagination max page size configuration ([#436](https://github.com/expo/entity/issues/436)) ([3ec4b5f](https://github.com/expo/entity/commit/3ec4b5f0cd53bf18d6bcfa27dd29b70d3f4d5f19))
|
||||
- add PostgresEntity/ReadonlyPostgresEntity classes that expose knexLoader methods ([#442](https://github.com/expo/entity/issues/442)) ([40b409f](https://github.com/expo/entity/commit/40b409f6f889febf8b90b8837caecac26ff64174)), closes [#441](https://github.com/expo/entity/issues/441)
|
||||
- Add raw sqlfragment orderBy capability for pagination ([#457](https://github.com/expo/entity/issues/457)) ([0615ffa](https://github.com/expo/entity/commit/0615ffacffbe9e903a9bf03b171c102f0554b992))
|
||||
- add support for nulls ordering in order by clauses ([#468](https://github.com/expo/entity/issues/468)) ([6c0838d](https://github.com/expo/entity/commit/6c0838dc577a3b6190e3551ee834925d7bd53bc6))
|
||||
- support nullable search fields with postgres transforms ([#470](https://github.com/expo/entity/issues/470)) ([53b0604](https://github.com/expo/entity/commit/53b0604152b8ca5ad9fc616d18478f93473c7eb0))
|
||||
|
||||
# [0.55.0](https://github.com/expo/entity/compare/v0.54.0...v0.55.0) (2026-01-27)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
# [0.54.0](https://github.com/expo/entity/compare/v0.53.0...v0.54.0) (2026-01-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** pin dependencies ([#328](https://github.com/expo/entity/issues/328)) ([5679f27](https://github.com/expo/entity/commit/5679f27209f1515ca5e626d858c4aa054b414625))
|
||||
|
||||
# [0.53.0](https://github.com/expo/entity/compare/v0.52.0...v0.53.0) (2025-12-19)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
# [0.52.0](https://github.com/expo/entity/compare/v0.51.0...v0.52.0) (2025-12-19)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
# [0.51.0](https://github.com/expo/entity/compare/v0.50.0...v0.51.0) (2025-11-21)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
# [0.50.0](https://github.com/expo/entity/compare/v0.49.0...v0.50.0) (2025-10-07)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
# [0.49.0](https://github.com/expo/entity/compare/v0.48.0...v0.49.0) (2025-10-07)
|
||||
|
||||
### Features
|
||||
|
||||
- throw specific errors for database adapter issues ([#311](https://github.com/expo/entity/issues/311)) ([f6639f9](https://github.com/expo/entity/commit/f6639f9def4ac20b628dd48fdca8d7cf61b6347b))
|
||||
|
||||
# [0.48.0](https://github.com/expo/entity/compare/v0.47.0...v0.48.0) (2025-09-19)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
# [0.47.0](https://github.com/expo/entity/compare/v0.46.0...v0.47.0) (2025-09-19)
|
||||
|
||||
### Features
|
||||
|
||||
- add support for Buffer fields ([#305](https://github.com/expo/entity/issues/305)) ([31b52a5](https://github.com/expo/entity/commit/31b52a5f75842f4810f66c12ad03a10d9192f889))
|
||||
|
||||
# [0.46.0](https://github.com/expo/entity/compare/v0.45.0...v0.46.0) (2025-06-26)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
# [0.45.0](https://github.com/expo/entity/compare/v0.44.0...v0.45.0) (2025-06-06)
|
||||
|
||||
### Features
|
||||
|
||||
- add createOrGetExistingAsync and createWithUniqueConstraintRecoveryAsync utility methods ([#291](https://github.com/expo/entity/issues/291)) ([78bc264](https://github.com/expo/entity/commit/78bc264b3328692b4b6722388a57d298f02e65c5))
|
||||
|
||||
# [0.44.0](https://github.com/expo/entity/compare/v0.43.0...v0.44.0) (2025-05-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- introduce transactional dataloader mode and fix global transaction ID generation ([#289](https://github.com/expo/entity/issues/289)) ([d3e89bd](https://github.com/expo/entity/commit/d3e89bdd82e6df5c5b35522bc3574fd9e1cfddff))
|
||||
|
||||
### Features
|
||||
|
||||
- add transaction-scoped dataloaders ([#284](https://github.com/expo/entity/issues/284)) ([e054a0d](https://github.com/expo/entity/commit/e054a0d74b3a39158ca7d73d05f59d3633113400)), closes [#98](https://github.com/expo/entity/issues/98) [#194](https://github.com/expo/entity/issues/194)
|
||||
|
||||
# [0.43.0](https://github.com/expo/entity/compare/v0.42.0...v0.43.0) (2025-04-10)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
# [0.42.0](https://github.com/expo/entity/compare/v0.41.0...v0.42.0) (2025-04-10)
|
||||
|
||||
### Features
|
||||
|
||||
- add @expo/entity-testing-utils package ([#280](https://github.com/expo/entity/issues/280)) ([485894a](https://github.com/expo/entity/commit/485894af16e233d533a9480285bbda56812cbe0b))
|
||||
- add composite field loading and caching ([#272](https://github.com/expo/entity/issues/272)) ([f0aa0da](https://github.com/expo/entity/commit/f0aa0dafebdb56418cbd22dda437233d850db3e7))
|
||||
- change entity id field generic to field name and derive type where necessary ([#278](https://github.com/expo/entity/issues/278)) ([b7e524c](https://github.com/expo/entity/commit/b7e524c41892797608d0b884410b8d520a80a9ef))
|
||||
- convert batched/cached loader interface to holder pattern ([#271](https://github.com/expo/entity/issues/271)) ([06b3cb7](https://github.com/expo/entity/commit/06b3cb7f8076ef9e5b83cdf154da159801bd14cc)), closes [#201](https://github.com/expo/entity/issues/201)
|
||||
- enforce explicit id field cache property ([#276](https://github.com/expo/entity/issues/276)) ([1da5cc0](https://github.com/expo/entity/commit/1da5cc01e1affc3b3338a0ab2050504b5853ac6f))
|
||||
|
||||
# [0.41.0](https://github.com/expo/entity/compare/v0.40.0...v0.41.0) (2025-03-07)
|
||||
|
||||
- feat!: Default all loaders and mutators to enforcing (#256) ([a976e76](https://github.com/expo/entity/commit/a976e765098bca27049585e43a3bed5c71b9ac6b)), closes [#256](https://github.com/expo/entity/issues/256)
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
- Default all loaders and mutators to enforcing
|
||||
|
||||
# [0.40.0](https://github.com/expo/entity/compare/v0.39.0...v0.40.0) (2025-02-27)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "v0.40.0" ([fa78ce8](https://github.com/expo/entity/commit/fa78ce84e2f2eb8199e36fd51779fc68905001c0))
|
||||
|
||||
# [0.39.0](https://github.com/expo/entity/compare/v0.38.0...v0.39.0) (2024-11-21)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
# [0.38.0](https://github.com/expo/entity/compare/v0.37.0...v0.38.0) (2024-06-27)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
# [0.37.0](https://github.com/expo/entity/compare/v0.36.0...v0.37.0) (2024-06-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update barrels ([#241](https://github.com/expo/entity/issues/241)) ([8c5f81b](https://github.com/expo/entity/commit/8c5f81bcca107e9b7bbea5f0ab41b0105057806e))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
@@ -39,4 +39,4 @@ export const createDefaultEntityCompanionProvider = (
|
||||
}
|
||||
);
|
||||
};
|
||||
```
|
||||
```
|
||||
@@ -1,43 +1,39 @@
|
||||
{
|
||||
"name": "@expo/entity-database-adapter-knex",
|
||||
"version": "0.72.0",
|
||||
"version": "0.37.0",
|
||||
"description": "Knex database adapter for @expo/entity",
|
||||
"files": [
|
||||
"build",
|
||||
"src"
|
||||
],
|
||||
"main": "build/index.js",
|
||||
"types": "build/index.d.ts",
|
||||
"scripts": {
|
||||
"tsc": "tsc",
|
||||
"clean": "rm -rf build coverage coverage-integration",
|
||||
"lint": "eslint src",
|
||||
"lint-fix": "eslint src --fix",
|
||||
"test": "jest --rootDir . --config ../../resources/jest.config.js --passWithNoTests",
|
||||
"integration": "../../resources/run-with-docker yarn integration-no-setup",
|
||||
"integration-no-setup": "jest --config ../../resources/jest-integration.config.js --rootDir . --runInBand --passWithNoTests",
|
||||
"barrelsby": "barrelsby --config ../../.barrelsby.json"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"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:^",
|
||||
"@expo/results": "^1.0.0",
|
||||
"knex": "^3.2.9"
|
||||
"knex": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/entity-testing-utils": "workspace:^",
|
||||
"@jest/globals": "30.5.2",
|
||||
"pg": "8.23.1",
|
||||
"ts-mockito": "2.6.1",
|
||||
"typescript": "6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"@expo/entity": "^0.37.0",
|
||||
"pg": "8.12.0"
|
||||
}
|
||||
}
|
||||
-762
@@ -1,762 +0,0 @@
|
||||
import type {
|
||||
EntityConfiguration,
|
||||
EntityConstructionUtils,
|
||||
EntityPrivacyPolicy,
|
||||
EntityQueryContext,
|
||||
IEntityClass,
|
||||
IEntityMetricsAdapter,
|
||||
ReadonlyEntity,
|
||||
ViewerContext,
|
||||
} from '@expo/entity';
|
||||
import { EntityNotFoundError, mapMap } from '@expo/entity';
|
||||
import type { Result } from '@expo/results';
|
||||
import { result } from '@expo/results';
|
||||
import assert from 'assert';
|
||||
|
||||
import type {
|
||||
FieldEqualityCondition,
|
||||
NullsOrdering,
|
||||
OrderByOrdering,
|
||||
} from './BasePostgresEntityDatabaseAdapter.ts';
|
||||
import { isSingleValueFieldEqualityCondition } from './BasePostgresEntityDatabaseAdapter.ts';
|
||||
import { BaseSQLQueryBuilder } from './BaseSQLQueryBuilder.ts';
|
||||
import type { PaginationStrategy } from './PaginationStrategy.ts';
|
||||
import type { SQLFragment } from './SQLOperator.ts';
|
||||
import type {
|
||||
Connection,
|
||||
EntityKnexDataManager,
|
||||
PageInfo,
|
||||
} from './internal/EntityKnexDataManager.ts';
|
||||
import type { NonNullableKeys } from './internal/utilityTypes.ts';
|
||||
|
||||
export type EntityLoaderBaseOrderByClause = {
|
||||
/**
|
||||
* The OrderByOrdering to order by.
|
||||
*/
|
||||
order: OrderByOrdering;
|
||||
|
||||
/**
|
||||
* Optional nulls ordering for this order by clause. If not provided, no specific nulls ordering is applied.
|
||||
*/
|
||||
nulls?: NullsOrdering | undefined;
|
||||
};
|
||||
|
||||
export type EntityLoaderFieldNameOrderByClause<
|
||||
TFields extends Record<string, any>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields,
|
||||
> = EntityLoaderBaseOrderByClause & {
|
||||
/**
|
||||
* The field name to order by.
|
||||
*/
|
||||
fieldName: TSelectedFields;
|
||||
};
|
||||
|
||||
export type EntityLoaderFieldFragmentOrderByClause<
|
||||
TFields extends Record<string, any>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields,
|
||||
> = EntityLoaderBaseOrderByClause & {
|
||||
/**
|
||||
* The SQL fragment to order by, which can reference selected fields. Example: `COALESCE(NULLIF(display_name, ''), split_part(full_name, '/', 2))`.
|
||||
* May not contain ASC or DESC, as ordering direction is controlled by the `order` property.
|
||||
*/
|
||||
fieldFragment: SQLFragment<Pick<TFields, TSelectedFields>>;
|
||||
};
|
||||
|
||||
export type EntityLoaderOrderByClause<
|
||||
TFields extends Record<string, any>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields,
|
||||
> =
|
||||
| EntityLoaderFieldNameOrderByClause<TFields, TSelectedFields>
|
||||
| EntityLoaderFieldFragmentOrderByClause<TFields, TSelectedFields>;
|
||||
|
||||
/**
|
||||
* Row locking modifier that locks the selected rows for update using `SELECT ... FOR UPDATE`.
|
||||
* The lock is held until the end of the transaction, so the query context must be transactional.
|
||||
*/
|
||||
export interface EntityLoaderForUpdateRowLockingModifier {
|
||||
forUpdate: true;
|
||||
forShare?: never;
|
||||
}
|
||||
|
||||
/**
|
||||
* Row locking modifier that locks the selected rows in share mode using `SELECT ... FOR SHARE`.
|
||||
* The lock is held until the end of the transaction, so the query context must be transactional.
|
||||
*/
|
||||
export interface EntityLoaderForShareRowLockingModifier {
|
||||
forShare: true;
|
||||
forUpdate?: never;
|
||||
}
|
||||
|
||||
/**
|
||||
* Row locking modifiers for load methods that never report a missing row as an error. Exactly one of
|
||||
* `forUpdate` or `forShare` is required. `skipLocked` may be set to skip rows already locked by another
|
||||
* transaction (using `SKIP LOCKED`) instead of waiting for them; skipped rows are reported as missing.
|
||||
*/
|
||||
export type EntityLoaderRowLockingModifiers = (
|
||||
| EntityLoaderForUpdateRowLockingModifier
|
||||
| EntityLoaderForShareRowLockingModifier
|
||||
) & {
|
||||
skipLocked?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Row locking modifiers for load methods that throw when a row is missing. Exactly one of `forUpdate`
|
||||
* or `forShare` is required. `skipLocked` is not permitted since a row skipped because another transaction
|
||||
* holds a lock on it would be indistinguishable from a row that does not exist.
|
||||
*/
|
||||
export type EntityLoaderRowLockingModifiersWithoutSkipLocked = (
|
||||
| EntityLoaderForUpdateRowLockingModifier
|
||||
| EntityLoaderForShareRowLockingModifier
|
||||
) & {
|
||||
skipLocked?: never;
|
||||
};
|
||||
|
||||
/**
|
||||
* SQL modifiers that only affect the selection but not the projection.
|
||||
*/
|
||||
export interface EntityLoaderQuerySelectionModifiers<
|
||||
TFields extends Record<string, any>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields,
|
||||
> {
|
||||
/**
|
||||
* Order the entities by specified columns and orders.
|
||||
*/
|
||||
orderBy?: readonly EntityLoaderOrderByClause<TFields, TSelectedFields>[];
|
||||
|
||||
/**
|
||||
* Skip the specified number of entities queried before returning.
|
||||
*/
|
||||
offset?: number;
|
||||
|
||||
/**
|
||||
* Limit the number of entities returned.
|
||||
*/
|
||||
limit?: number;
|
||||
|
||||
/**
|
||||
* Lock the selected rows for update using `SELECT ... FOR UPDATE`. The lock is held until the
|
||||
* end of the transaction, so the query context must be transactional. Mutually exclusive with `forShare`.
|
||||
*/
|
||||
forUpdate?: boolean;
|
||||
|
||||
/**
|
||||
* Lock the selected rows in share mode using `SELECT ... FOR SHARE`. The lock is held until the
|
||||
* end of the transaction, so the query context must be transactional. Mutually exclusive with `forUpdate`.
|
||||
*/
|
||||
forShare?: boolean;
|
||||
|
||||
/**
|
||||
* Skip rows that are already locked by another transaction using `SKIP LOCKED` instead of waiting for them.
|
||||
* Requires `forUpdate` or `forShare`.
|
||||
*/
|
||||
skipLocked?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to be used for constructing search fields based on field names.
|
||||
* The function takes a field name and returns a SQLFragment that can be used in the search field portion.
|
||||
*/
|
||||
export type EntityLoaderFieldNameConstructorFn<
|
||||
TFields extends Record<string, any>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields,
|
||||
> = (fieldName: TSelectedFields) => SQLFragment<TFields>;
|
||||
|
||||
/**
|
||||
* Specification for a search field that is a manually constructed SQLFragment. Useful for complex search fields that require
|
||||
* transformations to make nullable fields non-null or to make combinations of multiple fields.
|
||||
*/
|
||||
export type EntityLoaderSearchFieldSQLFragmentFnSpecification<
|
||||
TFields extends Record<string, any>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields,
|
||||
> = {
|
||||
/**
|
||||
* Function that constructs the SQLFragment for the search field.
|
||||
*
|
||||
* @param getFragmentForFieldName - A helper function that takes a field name and returns a SQLFragment for that field, which should be used
|
||||
* to construct the final SQLFragment for the search field.
|
||||
* @returns a SQLFragment representing the search field, which must reference selected fields through the provided helper function.
|
||||
*
|
||||
* @example
|
||||
* For example, to construct a search field that searches by display name (nullable column) with fallback to full name,
|
||||
* the fieldConstructor function could be implemented as follows:
|
||||
* ```
|
||||
* fieldConstructor: (getFragmentForFieldName) => {
|
||||
* const displayNameFragment = getFragmentForFieldName('display_name');
|
||||
* const fullNameFragment = getFragmentForFieldName('full_name');
|
||||
* return sql`COALESCE(NULLIF(${displayNameFragment}, ''), split_part(${fullNameFragment}, '/', 2))`;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
fieldConstructor: (
|
||||
/**
|
||||
* Helper function to get a SQLFragment for a given field name, which should be used to construct the final SQLFragment for the search field.
|
||||
*/
|
||||
getFragmentForFieldName: EntityLoaderFieldNameConstructorFn<TFields, TSelectedFields>,
|
||||
) => SQLFragment<Pick<TFields, TSelectedFields>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set of selected fields that are non-nullable, which can be used for search fields that require non-nullable fields.
|
||||
* To use a nullable field as a search field, use an EntityLoaderSearchFieldSQLFragmentFnSpecification with appropriate SQL
|
||||
* to handle null values, such as `COALESCE(field_name, '')` to treat nulls as empty strings for searching.
|
||||
*/
|
||||
export type NonNullableSelectedFields<
|
||||
TFields extends Record<string, any>,
|
||||
TSelectedFields extends keyof TFields,
|
||||
> = TSelectedFields & NonNullableKeys<TFields>;
|
||||
|
||||
/**
|
||||
* Specification for a search field that can be either a simple selected field name (which must be non-nullable) or a manually constructed
|
||||
* SQLFragment using EntityLoaderSearchFieldSQLFragmentFnSpecification.
|
||||
*/
|
||||
export type EntityLoaderSearchFieldSpecification<
|
||||
TFields extends Record<string, any>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields,
|
||||
> =
|
||||
| NonNullableSelectedFields<TFields, TSelectedFields>
|
||||
| EntityLoaderSearchFieldSQLFragmentFnSpecification<TFields, TSelectedFields>;
|
||||
|
||||
/**
|
||||
* Base specification shared by all search-based pagination strategies.
|
||||
*/
|
||||
export interface SearchSpecificationBase<
|
||||
TFields extends Record<string, any>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields,
|
||||
> {
|
||||
/**
|
||||
* The search term to search for. Must be a non-empty string.
|
||||
*/
|
||||
term: string;
|
||||
|
||||
/**
|
||||
* The fields to search within.
|
||||
*/
|
||||
fields: readonly EntityLoaderSearchFieldSpecification<TFields, TSelectedFields>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Search specification for case-insensitive pattern matching using the SQL ILIKE operator.
|
||||
*/
|
||||
export interface ILikeSearchSpecification<
|
||||
TFields extends Record<string, any>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields,
|
||||
> extends SearchSpecificationBase<TFields, TSelectedFields> {
|
||||
/**
|
||||
* Case-insensitive pattern matching search using SQL ILIKE operator.
|
||||
* Results are ordered by the fields being searched within in the order specified, then by ID for tie-breaking and stable pagination.
|
||||
*/
|
||||
strategy: PaginationStrategy.ILIKE_SEARCH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search specification for similarity search using PostgreSQL trigram similarity.
|
||||
*/
|
||||
export interface TrigramSearchSpecification<
|
||||
TFields extends Record<string, any>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields,
|
||||
> extends SearchSpecificationBase<TFields, TSelectedFields> {
|
||||
/**
|
||||
* Similarity search using PostgreSQL trigram similarity. Results are ordered by exact match priority, then by similarity score, then by specified extra order by fields if provided, then by ID for tie-breaking and stable pagination.
|
||||
* Note that trigram similarity search can be significantly slower than ILIKE search, especially on large datasets without appropriate indexes, and results may not be as relevant as more advanced full-text search solutions.
|
||||
* It is recommended to use this strategy only when ILIKE search does not meet the application's needs and to ensure appropriate database indexing for performance.
|
||||
*/
|
||||
strategy: PaginationStrategy.TRIGRAM_SEARCH;
|
||||
|
||||
/**
|
||||
* Similarity threshold for trigram matching.
|
||||
* Must be between 0 and 1, where:
|
||||
* - 0 matches everything
|
||||
* - 1 requires exact match
|
||||
*
|
||||
* Recommended threshold values:
|
||||
* - 0.3: Loose matching, allows more variation (default PostgreSQL similarity threshold)
|
||||
* - 0.4-0.5: Moderate matching, good balance for most use cases
|
||||
* - 0.6+: Strict matching, requires high similarity
|
||||
*/
|
||||
threshold: number;
|
||||
|
||||
/**
|
||||
* Optional additional fields to order by after similarity score and before ID for tie-breaking.
|
||||
* These fields are independent of search fields and can be used to provide meaningful
|
||||
* ordering when multiple results have the same similarity score.
|
||||
*/
|
||||
extraOrderByFields?: readonly EntityLoaderSearchFieldSpecification<TFields, TSelectedFields>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination specification for standard (non-search) pagination.
|
||||
*/
|
||||
export interface StandardPaginationSpecification<
|
||||
TFields extends Record<string, any>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields,
|
||||
> {
|
||||
/**
|
||||
* Standard pagination without search. Results are ordered by the specified orderBy fields.
|
||||
*/
|
||||
strategy: PaginationStrategy.STANDARD;
|
||||
|
||||
/**
|
||||
* Order the entities by specified columns and orders. If the ID field is not included, it will be automatically added for stable pagination.
|
||||
*/
|
||||
orderBy: readonly EntityLoaderOrderByClause<TFields, TSelectedFields>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination specification for SQL-based pagination (with or without search).
|
||||
*/
|
||||
export type PaginationSpecification<
|
||||
TFields extends Record<string, any>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields,
|
||||
> =
|
||||
| StandardPaginationSpecification<TFields, TSelectedFields>
|
||||
| ILikeSearchSpecification<TFields, TSelectedFields>
|
||||
| TrigramSearchSpecification<TFields, TSelectedFields>;
|
||||
|
||||
/**
|
||||
* Base unified pagination arguments
|
||||
*/
|
||||
interface EntityLoaderBaseUnifiedPaginationArgs<
|
||||
TFields extends Record<string, any>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields,
|
||||
> {
|
||||
/**
|
||||
* SQLFragment representing the WHERE clause to filter the entities being paginated.
|
||||
*/
|
||||
where?: SQLFragment<Pick<TFields, TSelectedFields>>;
|
||||
|
||||
/**
|
||||
* Pagination specification determining how to order and paginate results.
|
||||
*/
|
||||
pagination: PaginationSpecification<TFields, TSelectedFields>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward unified pagination arguments
|
||||
*/
|
||||
export interface EntityLoaderForwardUnifiedPaginationArgs<
|
||||
TFields extends Record<string, any>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields,
|
||||
> extends EntityLoaderBaseUnifiedPaginationArgs<TFields, TSelectedFields> {
|
||||
/**
|
||||
* The number of entities to return starting from the entity after the cursor. Must be a positive integer.
|
||||
*/
|
||||
first: number;
|
||||
|
||||
/**
|
||||
* The number of entities to return starting from the entity before the cursor. Must be a positive integer.
|
||||
* Not allowed with forward pagination.
|
||||
*/
|
||||
last?: never;
|
||||
|
||||
/**
|
||||
* The cursor to paginate after for forward pagination.
|
||||
*/
|
||||
after?: string;
|
||||
|
||||
/**
|
||||
* The cursor to paginate before for backward pagination.
|
||||
* Not allowed with forward pagination.
|
||||
*/
|
||||
before?: never;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward unified pagination arguments
|
||||
*/
|
||||
export interface EntityLoaderBackwardUnifiedPaginationArgs<
|
||||
TFields extends Record<string, any>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields,
|
||||
> extends EntityLoaderBaseUnifiedPaginationArgs<TFields, TSelectedFields> {
|
||||
/**
|
||||
* The number of entities to return starting from the entity after the cursor. Must be a positive integer.
|
||||
* Not allowed with backward pagination.
|
||||
*/
|
||||
first?: never;
|
||||
|
||||
/**
|
||||
* The number of entities to return starting from the entity before the cursor. Must be a positive integer.
|
||||
*/
|
||||
last: number;
|
||||
|
||||
/**
|
||||
* The cursor to paginate after for forward pagination.
|
||||
* Not allowed with backward pagination.
|
||||
*/
|
||||
after?: never;
|
||||
|
||||
/**
|
||||
* The cursor to paginate before for backward pagination.
|
||||
*/
|
||||
before?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load page pagination arguments, which can be either forward or backward unified pagination arguments.
|
||||
*/
|
||||
export type EntityLoaderLoadPageArgs<
|
||||
TFields extends Record<string, any>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields,
|
||||
> =
|
||||
| EntityLoaderForwardUnifiedPaginationArgs<TFields, TSelectedFields>
|
||||
| EntityLoaderBackwardUnifiedPaginationArgs<TFields, TSelectedFields>;
|
||||
|
||||
/**
|
||||
* Authorization-result-based knex entity loader for non-data-loader-based load methods.
|
||||
* All loads through this loader are results (or null for some loader methods), where an
|
||||
* unsuccessful result means an authorization error or entity construction error occurred.
|
||||
* Other errors are thrown.
|
||||
*/
|
||||
export class AuthorizationResultBasedKnexEntityLoader<
|
||||
TFields extends Record<string, any>,
|
||||
TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TIDField,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields,
|
||||
> {
|
||||
constructor(
|
||||
private readonly queryContext: EntityQueryContext,
|
||||
private readonly entityConfiguration: EntityConfiguration<TFields, TIDField>,
|
||||
private readonly entityClass: IEntityClass<
|
||||
TFields,
|
||||
TIDField,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
private readonly knexDataManager: EntityKnexDataManager<TFields, TIDField>,
|
||||
protected readonly metricsAdapter: IEntityMetricsAdapter,
|
||||
private readonly constructionUtils: EntityConstructionUtils<
|
||||
TFields,
|
||||
TIDField,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
|
||||
* @returns entity result for matching ID, where result error can be UnauthorizedError or EntityNotFoundError.
|
||||
*/
|
||||
async loadByIDFromDatabaseAsync(
|
||||
id: TFields[TIDField],
|
||||
modifiers: EntityLoaderRowLockingModifiersWithoutSkipLocked,
|
||||
): Promise<Result<TEntity>> {
|
||||
const entityResults = await this.loadManyByIDsFromDatabaseAsync([id], modifiers);
|
||||
const entityResult = entityResults.get(id);
|
||||
assert(entityResult !== undefined, `${id} should be guaranteed to be present in returned map`);
|
||||
return entityResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
|
||||
* @returns entity result for matching ID, or null if no entity exists for ID, where result error can be UnauthorizedError.
|
||||
*/
|
||||
async loadByIDNullableFromDatabaseAsync(
|
||||
id: TFields[TIDField],
|
||||
modifiers: EntityLoaderRowLockingModifiers,
|
||||
): Promise<Result<TEntity> | null> {
|
||||
const entityResults = await this.loadManyByIDsNullableFromDatabaseAsync([id], modifiers);
|
||||
return entityResults.get(id) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
|
||||
* @returns map from ID to corresponding entity result, where result error can be UnauthorizedError or EntityNotFoundError.
|
||||
*/
|
||||
async loadManyByIDsFromDatabaseAsync(
|
||||
ids: readonly TFields[TIDField][],
|
||||
modifiers: EntityLoaderRowLockingModifiersWithoutSkipLocked,
|
||||
): Promise<ReadonlyMap<TFields[TIDField], Result<TEntity>>> {
|
||||
const entityResults = await this.loadManyByIDsNullableFromDatabaseAsync(ids, modifiers);
|
||||
return mapMap(
|
||||
entityResults,
|
||||
(entityResult, id) =>
|
||||
entityResult ??
|
||||
result(
|
||||
new EntityNotFoundError({
|
||||
entityClass: this.entityClass,
|
||||
fieldName: this.entityConfiguration.idField,
|
||||
fieldValue: id,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
|
||||
* @returns map from ID to nullable corresponding entity result, where result error can be UnauthorizedError.
|
||||
*/
|
||||
async loadManyByIDsNullableFromDatabaseAsync(
|
||||
ids: readonly TFields[TIDField][],
|
||||
modifiers: EntityLoaderRowLockingModifiers,
|
||||
): Promise<ReadonlyMap<TFields[TIDField], Result<TEntity> | null>> {
|
||||
const idField = this.entityConfiguration.idField;
|
||||
this.constructionUtils.validateFieldAndValues(idField, ids);
|
||||
|
||||
const fieldObjects =
|
||||
ids.length > 0
|
||||
? await this.knexDataManager.loadManyByFieldEqualityConjunctionAsync(
|
||||
this.queryContext,
|
||||
[{ fieldName: idField, fieldValues: ids }],
|
||||
modifiers,
|
||||
)
|
||||
: [];
|
||||
|
||||
const idsToFieldObjects = new Map<TFields[TIDField], readonly Readonly<TFields>[]>(
|
||||
ids.map((id) => [id, []]),
|
||||
);
|
||||
for (const fieldObject of fieldObjects) {
|
||||
const id = fieldObject[idField];
|
||||
idsToFieldObjects.set(id, [...(idsToFieldObjects.get(id) ?? []), fieldObject]);
|
||||
}
|
||||
|
||||
const idsToEntityResults =
|
||||
await this.constructionUtils.constructAndAuthorizeEntitiesAsync(idsToFieldObjects);
|
||||
return mapMap(idsToEntityResults, (entityResults) => entityResults[0] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
|
||||
* @returns entity result where uniqueFieldName equals fieldValue, or null if no entity matches the condition, where result error can be UnauthorizedError.
|
||||
* @throws when multiple entities match the condition
|
||||
*/
|
||||
async loadByFieldEqualingFromDatabaseAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
uniqueFieldName: N,
|
||||
fieldValue: NonNullable<TFields[N]>,
|
||||
modifiers: EntityLoaderRowLockingModifiers,
|
||||
): Promise<Result<TEntity> | null> {
|
||||
const entityResults = await this.loadManyByFieldEqualingFromDatabaseAsync(
|
||||
uniqueFieldName,
|
||||
fieldValue,
|
||||
modifiers,
|
||||
);
|
||||
assert(
|
||||
entityResults.length <= 1,
|
||||
`loadByFieldEqualingFromDatabase: Multiple entities of type ${this.entityClass.name} found for ${String(
|
||||
uniqueFieldName,
|
||||
)}=${fieldValue}`,
|
||||
);
|
||||
return entityResults[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
|
||||
* @returns array of entity results where fieldName equals fieldValue, where result error can be UnauthorizedError
|
||||
*/
|
||||
async loadManyByFieldEqualingFromDatabaseAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
fieldName: N,
|
||||
fieldValue: NonNullable<TFields[N]>,
|
||||
modifiers: EntityLoaderRowLockingModifiers,
|
||||
): Promise<readonly Result<TEntity>[]> {
|
||||
return await this.loadManyByFieldEqualityConjunctionAsync(
|
||||
[{ fieldName, fieldValue }],
|
||||
modifiers,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
|
||||
* @returns the first entity results that matches the query, where result error can be
|
||||
* UnauthorizedError
|
||||
*/
|
||||
async loadFirstByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[],
|
||||
querySelectionModifiers: Omit<
|
||||
EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields>,
|
||||
'limit'
|
||||
> &
|
||||
Required<Pick<EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields>, 'orderBy'>>,
|
||||
): Promise<Result<TEntity> | null> {
|
||||
const results = await this.loadManyByFieldEqualityConjunctionAsync(fieldEqualityOperands, {
|
||||
...querySelectionModifiers,
|
||||
limit: 1,
|
||||
});
|
||||
return results[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
|
||||
* @returns array of entity results that match the query, where result error can be UnauthorizedError
|
||||
*/
|
||||
async loadManyByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[],
|
||||
querySelectionModifiers: EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields> = {},
|
||||
): Promise<readonly Result<TEntity>[]> {
|
||||
for (const fieldEqualityOperand of fieldEqualityOperands) {
|
||||
const fieldValues = isSingleValueFieldEqualityCondition(fieldEqualityOperand)
|
||||
? [fieldEqualityOperand.fieldValue]
|
||||
: fieldEqualityOperand.fieldValues;
|
||||
this.constructionUtils.validateFieldAndValues(fieldEqualityOperand.fieldName, fieldValues);
|
||||
}
|
||||
|
||||
const fieldObjects = await this.knexDataManager.loadManyByFieldEqualityConjunctionAsync(
|
||||
this.queryContext,
|
||||
fieldEqualityOperands,
|
||||
querySelectionModifiers,
|
||||
);
|
||||
return await this.constructionUtils.constructAndAuthorizeEntitiesArrayAsync(fieldObjects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count entities matching the conjunction of field equality operands.
|
||||
* This does not perform authorization since count does not load full entities.
|
||||
* Note that this should be used with the same caution as loadManyByFieldEqualityConjunctionAsync
|
||||
* regarding indexing since counts can be expensive on large datasets without appropriate indexes.
|
||||
*
|
||||
* @returns count of entities matching the filters
|
||||
*/
|
||||
async countByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[],
|
||||
): Promise<number> {
|
||||
for (const fieldEqualityOperand of fieldEqualityOperands) {
|
||||
const fieldValues = isSingleValueFieldEqualityCondition(fieldEqualityOperand)
|
||||
? [fieldEqualityOperand.fieldValue]
|
||||
: fieldEqualityOperand.fieldValues;
|
||||
this.constructionUtils.validateFieldAndValues(fieldEqualityOperand.fieldName, fieldValues);
|
||||
}
|
||||
|
||||
return await this.knexDataManager.countByFieldEqualityConjunctionAsync(
|
||||
this.queryContext,
|
||||
fieldEqualityOperands,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count entities matching a SQL fragment.
|
||||
* This does not perform authorization since count does not load full entities.
|
||||
* Note that this should be used with the same caution as loadManyBySQL regarding indexing
|
||||
* since counts can be expensive on large datasets without appropriate indexes.
|
||||
*
|
||||
* @returns count of entities matching the query
|
||||
*/
|
||||
async countBySQLAsync(fragment: SQLFragment<Pick<TFields, TSelectedFields>>): Promise<number> {
|
||||
return await this.knexDataManager.countBySQLFragmentAsync(this.queryContext, fragment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
|
||||
* @returns SQL query builder for building and executing SQL queries that when executed returns entity results where result error can be UnauthorizedError.
|
||||
*/
|
||||
loadManyBySQL(
|
||||
fragment: SQLFragment<Pick<TFields, TSelectedFields>>,
|
||||
modifiers: EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields> = {},
|
||||
): AuthorizationResultBasedSQLQueryBuilder<
|
||||
TFields,
|
||||
TIDField,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
> {
|
||||
return new AuthorizationResultBasedSQLQueryBuilder(
|
||||
this.knexDataManager,
|
||||
this.constructionUtils,
|
||||
this.queryContext,
|
||||
fragment,
|
||||
modifiers,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a page of entities with Relay-style cursor pagination using a unified pagination specification.
|
||||
* Only returns successfully authorized entities for cursor stability; failed authorization results are filtered out.
|
||||
*
|
||||
* @returns Connection with only successfully authorized entities
|
||||
*/
|
||||
async loadPageAsync(
|
||||
args: EntityLoaderLoadPageArgs<TFields, TSelectedFields>,
|
||||
): Promise<Connection<TEntity>> {
|
||||
const pageResult = await this.knexDataManager.loadPageAsync(this.queryContext, args);
|
||||
|
||||
const edgeResults = await Promise.all(
|
||||
pageResult.edges.map(async (edge) => {
|
||||
const entityResult = await this.constructionUtils.constructAndAuthorizeEntityAsync(
|
||||
edge.node,
|
||||
);
|
||||
if (!entityResult.ok) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...edge,
|
||||
node: entityResult.value,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const edges = edgeResults.filter((edge) => edge !== null);
|
||||
const pageInfo: PageInfo = {
|
||||
...pageResult.pageInfo,
|
||||
startCursor: edges[0]?.cursor ?? null,
|
||||
endCursor: edges[edges.length - 1]?.cursor ?? null,
|
||||
};
|
||||
|
||||
return {
|
||||
edges,
|
||||
pageInfo,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
|
||||
* @returns The pagination cursor for the given entity.
|
||||
*/
|
||||
getPaginationCursorForEntity(entity: TEntity): string {
|
||||
return this.knexDataManager.getCursorForEntityID(entity.getID());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL query builder implementation for AuthorizationResultBasedKnexEntityLoader.
|
||||
*/
|
||||
export class AuthorizationResultBasedSQLQueryBuilder<
|
||||
TFields extends Record<string, any>,
|
||||
TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TIDField,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields,
|
||||
> extends BaseSQLQueryBuilder<TFields, TSelectedFields, Result<TEntity>> {
|
||||
constructor(
|
||||
private readonly knexDataManager: EntityKnexDataManager<TFields, TIDField>,
|
||||
private readonly constructionUtils: EntityConstructionUtils<
|
||||
TFields,
|
||||
TIDField,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
private readonly queryContext: EntityQueryContext,
|
||||
sqlFragment: SQLFragment<Pick<TFields, TSelectedFields>>,
|
||||
modifiers: EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields>,
|
||||
) {
|
||||
super(sqlFragment, modifiers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query and return results.
|
||||
*/
|
||||
async executeInternalAsync(): Promise<readonly Result<TEntity>[]> {
|
||||
const fieldObjects = await this.knexDataManager.loadManyBySQLFragmentAsync(
|
||||
this.queryContext,
|
||||
this.getSQLFragment(),
|
||||
this.getModifiers(),
|
||||
);
|
||||
return await this.constructionUtils.constructAndAuthorizeEntitiesArrayAsync(fieldObjects);
|
||||
}
|
||||
}
|
||||
@@ -1,369 +0,0 @@
|
||||
import type { EntityQueryContext } from '@expo/entity';
|
||||
import {
|
||||
EntityDatabaseAdapter,
|
||||
getDatabaseFieldForEntityField,
|
||||
transformDatabaseObjectToFields,
|
||||
} from '@expo/entity';
|
||||
import type { Knex } from 'knex';
|
||||
|
||||
import type { SQLFragment } from './SQLOperator.ts';
|
||||
|
||||
/**
|
||||
* Equality operand that is used for selecting entities with a field with a single value.
|
||||
*/
|
||||
export interface SingleValueFieldEqualityCondition<
|
||||
TFields extends Record<string, any>,
|
||||
N extends keyof TFields = keyof TFields,
|
||||
> {
|
||||
fieldName: N;
|
||||
fieldValue: TFields[N];
|
||||
}
|
||||
|
||||
/**
|
||||
* Equality operand that is used for selecting entities with a field matching one of multiple values.
|
||||
*/
|
||||
export interface MultiValueFieldEqualityCondition<
|
||||
TFields extends Record<string, any>,
|
||||
N extends keyof TFields = keyof TFields,
|
||||
> {
|
||||
fieldName: N;
|
||||
fieldValues: readonly TFields[N][];
|
||||
}
|
||||
|
||||
/**
|
||||
* A single equality operand for use in a selection clause.
|
||||
* See EntityLoader.loadManyByFieldEqualityConjunctionAsync documentation for examples.
|
||||
*/
|
||||
export type FieldEqualityCondition<
|
||||
TFields extends Record<string, any>,
|
||||
N extends keyof TFields = keyof TFields,
|
||||
> = SingleValueFieldEqualityCondition<TFields, N> | MultiValueFieldEqualityCondition<TFields, N>;
|
||||
|
||||
export function isSingleValueFieldEqualityCondition<
|
||||
TFields extends Record<string, any>,
|
||||
N extends keyof TFields = keyof TFields,
|
||||
>(
|
||||
condition: FieldEqualityCondition<TFields, N>,
|
||||
): condition is SingleValueFieldEqualityCondition<TFields, N> {
|
||||
return (condition as SingleValueFieldEqualityCondition<TFields, N>).fieldValue !== undefined;
|
||||
}
|
||||
|
||||
export interface TableFieldSingleValueEqualityCondition {
|
||||
tableField: string;
|
||||
tableValue: any;
|
||||
}
|
||||
|
||||
export interface TableFieldMultiValueEqualityCondition {
|
||||
tableField: string;
|
||||
tableValues: readonly any[];
|
||||
}
|
||||
|
||||
export enum NullsOrdering {
|
||||
FIRST = 'first',
|
||||
LAST = 'last',
|
||||
}
|
||||
|
||||
/**
|
||||
* Ordering options for `orderBy` clauses.
|
||||
*/
|
||||
export enum OrderByOrdering {
|
||||
/**
|
||||
* Ascending order (lowest to highest).
|
||||
* Ascending order puts smaller values first, where "smaller" is defined in terms of the %3C operator.
|
||||
*/
|
||||
ASCENDING = 'asc',
|
||||
|
||||
/**
|
||||
* Descending order (highest to lowest).
|
||||
* Descending order puts larger values first, where "larger" is defined in terms of the %3E operator.
|
||||
*/
|
||||
DESCENDING = 'desc',
|
||||
}
|
||||
|
||||
export type PostgresOrderByClause<TFields extends Record<string, any>> =
|
||||
| {
|
||||
/**
|
||||
* The field name to order by.
|
||||
*/
|
||||
fieldName: keyof TFields;
|
||||
|
||||
/**
|
||||
* The OrderByOrdering to order by.
|
||||
*/
|
||||
order: OrderByOrdering;
|
||||
|
||||
/**
|
||||
* Optional nulls ordering. If not provided, the database default is used
|
||||
* (NULLS LAST for ASC, NULLS FIRST for DESC in PostgreSQL).
|
||||
*/
|
||||
nulls?: NullsOrdering | undefined;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* A raw SQL fragment to order by. May not contain ASC or DESC, as ordering direction is determined by the `order` property.
|
||||
*/
|
||||
fieldFragment: SQLFragment<TFields>;
|
||||
|
||||
/**
|
||||
* The OrderByOrdering to order by.
|
||||
*/
|
||||
order: OrderByOrdering;
|
||||
|
||||
/**
|
||||
* Optional nulls ordering. If not provided, the database default is used
|
||||
* (NULLS LAST for ASC, NULLS FIRST for DESC in PostgreSQL).
|
||||
*/
|
||||
nulls?: NullsOrdering | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* SQL modifiers that only affect the selection but not the projection.
|
||||
*/
|
||||
export interface PostgresQuerySelectionModifiers<TFields extends Record<string, any>> {
|
||||
/**
|
||||
* Order the entities by specified columns and orders.
|
||||
*/
|
||||
orderBy?: readonly PostgresOrderByClause<TFields>[];
|
||||
|
||||
/**
|
||||
* Skip the specified number of entities queried before returning.
|
||||
*/
|
||||
offset?: number;
|
||||
|
||||
/**
|
||||
* Limit the number of entities returned.
|
||||
*/
|
||||
limit?: number;
|
||||
|
||||
/**
|
||||
* Lock the selected rows for update using `SELECT ... FOR UPDATE`. Mutually exclusive with `forShare`.
|
||||
*/
|
||||
forUpdate?: boolean;
|
||||
|
||||
/**
|
||||
* Lock the selected rows in share mode using `SELECT ... FOR SHARE`. Mutually exclusive with `forUpdate`.
|
||||
*/
|
||||
forShare?: boolean;
|
||||
|
||||
/**
|
||||
* Skip rows that are already locked by another transaction using `SKIP LOCKED`. Requires `forUpdate` or `forShare`.
|
||||
*/
|
||||
skipLocked?: boolean;
|
||||
}
|
||||
|
||||
export type TableOrderByClause<TFields extends Record<string, any>> =
|
||||
| {
|
||||
columnName: string;
|
||||
order: OrderByOrdering;
|
||||
nulls: NullsOrdering | undefined;
|
||||
}
|
||||
| {
|
||||
columnFragment: SQLFragment<TFields>;
|
||||
order: OrderByOrdering;
|
||||
nulls: NullsOrdering | undefined;
|
||||
};
|
||||
|
||||
export interface TableQuerySelectionModifiers<TFields extends Record<string, any>> {
|
||||
orderBy: TableOrderByClause<TFields>[] | undefined;
|
||||
offset: number | undefined;
|
||||
limit: number | undefined;
|
||||
forUpdate: boolean | undefined;
|
||||
forShare: boolean | undefined;
|
||||
skipLocked: boolean | undefined;
|
||||
}
|
||||
|
||||
export abstract class BasePostgresEntityDatabaseAdapter<
|
||||
TFields extends Record<string, any>,
|
||||
TIDField extends keyof TFields,
|
||||
> extends EntityDatabaseAdapter<TFields, TIDField> {
|
||||
/**
|
||||
* Get the maximum page size for pagination.
|
||||
* @returns maximum page size if configured, undefined otherwise
|
||||
*/
|
||||
get paginationMaxPageSize(): number | undefined {
|
||||
return undefined;
|
||||
}
|
||||
/**
|
||||
* Fetch many objects matching the conjunction of where clauses constructed from
|
||||
* specified field equality operands.
|
||||
*
|
||||
* @param queryContext - query context with which to perform the fetch
|
||||
* @param fieldEqualityOperands - list of field equality where clause operand specifications
|
||||
* @param querySelectionModifiers - limit, offset, orderBy, and orderByRaw for the query
|
||||
* @returns array of objects matching the query
|
||||
*/
|
||||
async fetchManyByFieldEqualityConjunctionAsync<N extends keyof TFields>(
|
||||
queryContext: EntityQueryContext,
|
||||
fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[],
|
||||
querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>,
|
||||
): Promise<readonly Readonly<TFields>[]> {
|
||||
const tableFieldSingleValueOperands: TableFieldSingleValueEqualityCondition[] = [];
|
||||
const tableFieldMultipleValueOperands: TableFieldMultiValueEqualityCondition[] = [];
|
||||
for (const operand of fieldEqualityOperands) {
|
||||
if (isSingleValueFieldEqualityCondition(operand)) {
|
||||
tableFieldSingleValueOperands.push({
|
||||
tableField: getDatabaseFieldForEntityField(this.entityConfiguration, operand.fieldName),
|
||||
tableValue: operand.fieldValue,
|
||||
});
|
||||
} else {
|
||||
tableFieldMultipleValueOperands.push({
|
||||
tableField: getDatabaseFieldForEntityField(this.entityConfiguration, operand.fieldName),
|
||||
tableValues: operand.fieldValues,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const results = await this.fetchManyByFieldEqualityConjunctionInternalAsync(
|
||||
queryContext.getQueryInterface(),
|
||||
this.entityConfiguration.tableName,
|
||||
tableFieldSingleValueOperands,
|
||||
tableFieldMultipleValueOperands,
|
||||
this.convertToTableQueryModifiers(querySelectionModifiers),
|
||||
);
|
||||
|
||||
return results.map((result) =>
|
||||
transformDatabaseObjectToFields(this.entityConfiguration, this.fieldTransformerMap, result),
|
||||
);
|
||||
}
|
||||
|
||||
protected abstract fetchManyByFieldEqualityConjunctionInternalAsync(
|
||||
queryInterface: Knex,
|
||||
tableName: string,
|
||||
tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
|
||||
tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
|
||||
querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
|
||||
): Promise<object[]>;
|
||||
|
||||
/**
|
||||
* Fetch many objects matching the SQL fragment.
|
||||
*
|
||||
* @param queryContext - query context with which to perform the fetch
|
||||
* @param sqlFragment - SQLFragment for the WHERE clause of the query
|
||||
* @param querySelectionModifiers - limit, offset, and orderByFragment for the query
|
||||
* @returns array of objects matching the query
|
||||
*/
|
||||
async fetchManyBySQLFragmentAsync(
|
||||
queryContext: EntityQueryContext,
|
||||
sqlFragment: SQLFragment<TFields>,
|
||||
querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>,
|
||||
): Promise<readonly Readonly<TFields>[]> {
|
||||
const results = await this.fetchManyBySQLFragmentInternalAsync(
|
||||
queryContext.getQueryInterface(),
|
||||
this.entityConfiguration.tableName,
|
||||
sqlFragment,
|
||||
this.convertToTableQueryModifiers(querySelectionModifiers),
|
||||
);
|
||||
|
||||
return results.map((result) =>
|
||||
transformDatabaseObjectToFields(this.entityConfiguration, this.fieldTransformerMap, result),
|
||||
);
|
||||
}
|
||||
|
||||
protected abstract fetchManyBySQLFragmentInternalAsync(
|
||||
queryInterface: Knex,
|
||||
tableName: string,
|
||||
sqlFragment: SQLFragment<TFields>,
|
||||
querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
|
||||
): Promise<object[]>;
|
||||
|
||||
/**
|
||||
* Count objects matching the conjunction of where clauses constructed from
|
||||
* specified field equality operands.
|
||||
*
|
||||
* @param queryContext - query context with which to perform the count
|
||||
* @param fieldEqualityOperands - list of field equality where clause operand specifications
|
||||
* @returns count of objects matching the query
|
||||
*/
|
||||
async countByFieldEqualityConjunctionAsync<N extends keyof TFields>(
|
||||
queryContext: EntityQueryContext,
|
||||
fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[],
|
||||
): Promise<number> {
|
||||
const tableFieldSingleValueOperands: TableFieldSingleValueEqualityCondition[] = [];
|
||||
const tableFieldMultipleValueOperands: TableFieldMultiValueEqualityCondition[] = [];
|
||||
for (const operand of fieldEqualityOperands) {
|
||||
if (isSingleValueFieldEqualityCondition(operand)) {
|
||||
tableFieldSingleValueOperands.push({
|
||||
tableField: getDatabaseFieldForEntityField(this.entityConfiguration, operand.fieldName),
|
||||
tableValue: operand.fieldValue,
|
||||
});
|
||||
} else {
|
||||
tableFieldMultipleValueOperands.push({
|
||||
tableField: getDatabaseFieldForEntityField(this.entityConfiguration, operand.fieldName),
|
||||
tableValues: operand.fieldValues,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await this.countByFieldEqualityConjunctionInternalAsync(
|
||||
queryContext.getQueryInterface(),
|
||||
this.entityConfiguration.tableName,
|
||||
tableFieldSingleValueOperands,
|
||||
tableFieldMultipleValueOperands,
|
||||
);
|
||||
}
|
||||
|
||||
protected abstract countByFieldEqualityConjunctionInternalAsync(
|
||||
queryInterface: Knex,
|
||||
tableName: string,
|
||||
tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
|
||||
tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
|
||||
): Promise<number>;
|
||||
|
||||
/**
|
||||
* Count objects matching the SQL fragment.
|
||||
*
|
||||
* @param queryContext - query context with which to perform the count
|
||||
* @param sqlFragment - SQLFragment for the WHERE clause of the query
|
||||
* @returns count of objects matching the query
|
||||
*/
|
||||
async countBySQLFragmentAsync(
|
||||
queryContext: EntityQueryContext,
|
||||
sqlFragment: SQLFragment<TFields>,
|
||||
): Promise<number> {
|
||||
return await this.countBySQLFragmentInternalAsync(
|
||||
queryContext.getQueryInterface(),
|
||||
this.entityConfiguration.tableName,
|
||||
sqlFragment,
|
||||
);
|
||||
}
|
||||
|
||||
protected abstract countBySQLFragmentInternalAsync(
|
||||
queryInterface: Knex,
|
||||
tableName: string,
|
||||
sqlFragment: SQLFragment<TFields>,
|
||||
): Promise<number>;
|
||||
|
||||
private convertToTableQueryModifiers(
|
||||
querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>,
|
||||
): TableQuerySelectionModifiers<TFields> {
|
||||
const orderBy = querySelectionModifiers.orderBy;
|
||||
return {
|
||||
orderBy:
|
||||
orderBy !== undefined
|
||||
? orderBy.map((orderBySpecification): TableOrderByClause<TFields> => {
|
||||
if ('fieldName' in orderBySpecification) {
|
||||
return {
|
||||
columnName: getDatabaseFieldForEntityField(
|
||||
this.entityConfiguration,
|
||||
orderBySpecification.fieldName,
|
||||
),
|
||||
order: orderBySpecification.order,
|
||||
nulls: orderBySpecification.nulls,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
columnFragment: orderBySpecification.fieldFragment,
|
||||
order: orderBySpecification.order,
|
||||
nulls: orderBySpecification.nulls,
|
||||
};
|
||||
}
|
||||
})
|
||||
: undefined,
|
||||
offset: querySelectionModifiers.offset,
|
||||
limit: querySelectionModifiers.limit,
|
||||
forUpdate: querySelectionModifiers.forUpdate,
|
||||
forShare: querySelectionModifiers.forShare,
|
||||
skipLocked: querySelectionModifiers.skipLocked,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
import type {
|
||||
EntityLoaderOrderByClause,
|
||||
EntityLoaderQuerySelectionModifiers,
|
||||
} from './AuthorizationResultBasedKnexEntityLoader.ts';
|
||||
import type { NullsOrdering } from './BasePostgresEntityDatabaseAdapter.ts';
|
||||
import { OrderByOrdering } from './BasePostgresEntityDatabaseAdapter.ts';
|
||||
import type { SQLFragment } from './SQLOperator.ts';
|
||||
|
||||
/**
|
||||
* Base SQL query builder that provides common functionality for building SQL queries.
|
||||
*/
|
||||
export abstract class BaseSQLQueryBuilder<
|
||||
TFields extends Record<string, any>,
|
||||
TSelectedFields extends keyof TFields,
|
||||
TResultType,
|
||||
> {
|
||||
private executed = false;
|
||||
|
||||
constructor(
|
||||
private readonly sqlFragment: SQLFragment<Pick<TFields, TSelectedFields>>,
|
||||
private readonly modifiers: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
orderBy?: readonly EntityLoaderOrderByClause<TFields, TSelectedFields>[];
|
||||
forUpdate?: boolean;
|
||||
forShare?: boolean;
|
||||
skipLocked?: boolean;
|
||||
},
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Limit the number of results
|
||||
*/
|
||||
limit(n: number): this {
|
||||
this.modifiers.limit = n;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip a number of results
|
||||
*/
|
||||
offset(n: number): this {
|
||||
this.modifiers.offset = n;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock the selected rows for update using `SELECT ... FOR UPDATE`. The lock is held until the
|
||||
* end of the transaction, so the query must be executed in a transactional query context.
|
||||
* Mutually exclusive with `forShare`.
|
||||
*/
|
||||
forUpdate(): this {
|
||||
this.modifiers.forUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock the selected rows in share mode using `SELECT ... FOR SHARE`. The lock is held until the
|
||||
* end of the transaction, so the query must be executed in a transactional query context.
|
||||
* Mutually exclusive with `forUpdate`.
|
||||
*/
|
||||
forShare(): this {
|
||||
this.modifiers.forShare = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip rows that are already locked by another transaction using `SKIP LOCKED` instead of waiting for them.
|
||||
* Requires `forUpdate()` or `forShare()`.
|
||||
*/
|
||||
skipLocked(): this {
|
||||
this.modifiers.skipLocked = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Order by a field. Can be called multiple times to add multiple order bys.
|
||||
*/
|
||||
orderBy(
|
||||
fieldName: TSelectedFields,
|
||||
order: OrderByOrdering = OrderByOrdering.ASCENDING,
|
||||
nulls?: NullsOrdering,
|
||||
): this {
|
||||
this.modifiers.orderBy = [...(this.modifiers.orderBy ?? []), { fieldName, order, nulls }];
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Order by a SQL fragment expression.
|
||||
* Provides type-safe, parameterized ORDER BY clauses
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* query.orderByFragment(
|
||||
* sql`(data->>'createdAt')::timestamp`,
|
||||
* OrderByOrdering.DESCENDING,
|
||||
* );
|
||||
* // Generates ORDER BY clause that orders by the createdAt field in the JSONB data column, cast to a timestamp, in descending order.
|
||||
* // Note that the SQL fragment is parameterized, so it is safe from SQL injection.
|
||||
* // The generated SQL would look like: ORDER BY (data->>'createdAt')::timestamp DESC
|
||||
* ```
|
||||
*
|
||||
* @param fragment - The SQL fragment to order by. Must not include the ASC/DESC keyword, as ordering direction is determined by the `order` parameter.
|
||||
* @param order - The ordering direction (ascending or descending). Defaults to ascending.
|
||||
*/
|
||||
orderBySQL(
|
||||
fragment: SQLFragment<Pick<TFields, TSelectedFields>>,
|
||||
order: OrderByOrdering = OrderByOrdering.ASCENDING,
|
||||
nulls?: NullsOrdering,
|
||||
): this {
|
||||
this.modifiers.orderBy = [
|
||||
...(this.modifiers.orderBy ?? []),
|
||||
{ fieldFragment: fragment, order, nulls },
|
||||
];
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current modifiers as QuerySelectionModifiersWithOrderByFragment<TFields>
|
||||
*/
|
||||
protected getModifiers(): EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields> {
|
||||
return this.modifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SQL fragment
|
||||
*/
|
||||
protected getSQLFragment(): SQLFragment<Pick<TFields, TSelectedFields>> {
|
||||
return this.sqlFragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query and return results.
|
||||
* Implementation depends on the specific loader type.
|
||||
*/
|
||||
public async executeAsync(): Promise<readonly TResultType[]> {
|
||||
if (this.executed) {
|
||||
throw new Error(
|
||||
'Query has already been executed. Create a new query builder to execute again.',
|
||||
);
|
||||
}
|
||||
this.executed = true;
|
||||
return await this.executeInternalAsync();
|
||||
}
|
||||
|
||||
protected abstract executeInternalAsync(): Promise<readonly TResultType[]>;
|
||||
}
|
||||
@@ -1,390 +0,0 @@
|
||||
import type {
|
||||
EntityConstructionUtils,
|
||||
EntityPrivacyPolicy,
|
||||
EntityQueryContext,
|
||||
IEntityMetricsAdapter,
|
||||
ReadonlyEntity,
|
||||
ViewerContext,
|
||||
} from '@expo/entity';
|
||||
import { mapMap } from '@expo/entity';
|
||||
|
||||
import type {
|
||||
AuthorizationResultBasedKnexEntityLoader,
|
||||
EntityLoaderLoadPageArgs,
|
||||
EntityLoaderQuerySelectionModifiers,
|
||||
EntityLoaderRowLockingModifiers,
|
||||
EntityLoaderRowLockingModifiersWithoutSkipLocked,
|
||||
} from './AuthorizationResultBasedKnexEntityLoader.ts';
|
||||
import type { FieldEqualityCondition } from './BasePostgresEntityDatabaseAdapter.ts';
|
||||
import { BaseSQLQueryBuilder } from './BaseSQLQueryBuilder.ts';
|
||||
import type { SQLFragment } from './SQLOperator.ts';
|
||||
import type { Connection, EntityKnexDataManager } from './internal/EntityKnexDataManager.ts';
|
||||
|
||||
/**
|
||||
* Enforcing knex entity loader for non-data-loader-based load methods.
|
||||
* All loads through this loader will throw if the load is not successful.
|
||||
*/
|
||||
export class EnforcingKnexEntityLoader<
|
||||
TFields extends Record<string, any>,
|
||||
TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TIDField,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields,
|
||||
> {
|
||||
constructor(
|
||||
private readonly knexEntityLoader: AuthorizationResultBasedKnexEntityLoader<
|
||||
TFields,
|
||||
TIDField,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
private readonly queryContext: EntityQueryContext,
|
||||
private readonly knexDataManager: EntityKnexDataManager<TFields, TIDField>,
|
||||
protected readonly metricsAdapter: IEntityMetricsAdapter,
|
||||
private readonly constructionUtils: EntityConstructionUtils<
|
||||
TFields,
|
||||
TIDField,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Load an entity by ID directly from the database, bypassing the dataloader and cache.
|
||||
*
|
||||
* Unlike {@link "@expo/entity"!EnforcingEntityLoader.loadByIDAsync | EnforcingEntityLoader.loadByIDAsync}, this
|
||||
* issues one database query per call and does not read from or write to the entity cache. It exists
|
||||
* for cases that need row locking modifiers, which are required to make this intent explicit.
|
||||
* `skipLocked` is not permitted here since a skipped row would be reported as not found.
|
||||
* Use {@link loadByIDNullableFromDatabaseAsync} with `skipLocked`.
|
||||
*
|
||||
* @param id - ID of the entity
|
||||
* @param modifiers - row locking modifiers for the query
|
||||
* @returns entity matching ID
|
||||
* @throws EntityNotAuthorizedError when viewer is not authorized to view the returned entity
|
||||
* @throws EntityNotFoundError when no entity exists for ID
|
||||
*/
|
||||
async loadByIDFromDatabaseAsync(
|
||||
id: TFields[TIDField],
|
||||
modifiers: EntityLoaderRowLockingModifiersWithoutSkipLocked,
|
||||
): Promise<TEntity> {
|
||||
const entityResult = await this.knexEntityLoader.loadByIDFromDatabaseAsync(id, modifiers);
|
||||
return entityResult.enforceValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an entity by ID directly from the database, or return null if non-existent.
|
||||
* See {@link loadByIDFromDatabaseAsync} for how this differs from the standard loader.
|
||||
*
|
||||
* @param id - ID of the entity
|
||||
* @param modifiers - row locking modifiers for the query
|
||||
* @returns entity for matching ID, or null if no entity exists for ID
|
||||
* @throws EntityNotAuthorizedError when viewer is not authorized to view the returned entity
|
||||
*/
|
||||
async loadByIDNullableFromDatabaseAsync(
|
||||
id: TFields[TIDField],
|
||||
modifiers: EntityLoaderRowLockingModifiers,
|
||||
): Promise<TEntity | null> {
|
||||
const entityResult = await this.knexEntityLoader.loadByIDNullableFromDatabaseAsync(
|
||||
id,
|
||||
modifiers,
|
||||
);
|
||||
return entityResult ? entityResult.enforceValue() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load many entities for a list of IDs directly from the database.
|
||||
* See {@link loadByIDFromDatabaseAsync} for how this differs from the standard loader.
|
||||
*
|
||||
* `skipLocked` is not permitted here since a skipped row would be reported as not found.
|
||||
* Use {@link loadManyByIDsNullableFromDatabaseAsync} with `skipLocked`.
|
||||
*
|
||||
* @param ids - IDs of the entities to load
|
||||
* @param modifiers - row locking modifiers for the query
|
||||
* @returns map from ID to corresponding entity
|
||||
* @throws EntityNotAuthorizedError when viewer is not authorized to view one or more of the returned entities
|
||||
* @throws EntityNotFoundError when no entity exists for one or more of the IDs
|
||||
*/
|
||||
async loadManyByIDsFromDatabaseAsync(
|
||||
ids: readonly TFields[TIDField][],
|
||||
modifiers: EntityLoaderRowLockingModifiersWithoutSkipLocked,
|
||||
): Promise<ReadonlyMap<TFields[TIDField], TEntity>> {
|
||||
const entityResults = await this.knexEntityLoader.loadManyByIDsFromDatabaseAsync(
|
||||
ids,
|
||||
modifiers,
|
||||
);
|
||||
return mapMap(entityResults, (entityResult) => entityResult.enforceValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load many entities for a list of IDs directly from the database, returning null for any IDs that are non-existent.
|
||||
* See {@link loadByIDFromDatabaseAsync} for how this differs from the standard loader.
|
||||
*
|
||||
* @param ids - IDs of the entities to load
|
||||
* @param modifiers - row locking modifiers for the query
|
||||
* @returns map from ID to nullable corresponding entity
|
||||
* @throws EntityNotAuthorizedError when viewer is not authorized to view one or more of the returned entities
|
||||
*/
|
||||
async loadManyByIDsNullableFromDatabaseAsync(
|
||||
ids: readonly TFields[TIDField][],
|
||||
modifiers: EntityLoaderRowLockingModifiers,
|
||||
): Promise<ReadonlyMap<TFields[TIDField], TEntity | null>> {
|
||||
const entityResults = await this.knexEntityLoader.loadManyByIDsNullableFromDatabaseAsync(
|
||||
ids,
|
||||
modifiers,
|
||||
);
|
||||
return mapMap(entityResults, (entityResult) => entityResult?.enforceValue() ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an entity where uniqueFieldName equals fieldValue directly from the database, or null if no entity matches.
|
||||
* See {@link loadByIDFromDatabaseAsync} for how this differs from the standard loader.
|
||||
*
|
||||
* @param uniqueFieldName - entity field being queried
|
||||
* @param fieldValue - uniqueFieldName field value being queried
|
||||
* @param modifiers - row locking modifiers for the query
|
||||
* @returns entity where uniqueFieldName equals fieldValue, or null if no entity matches the condition
|
||||
* @throws when multiple entities match the condition
|
||||
* @throws EntityNotAuthorizedError when viewer is not authorized to view the returned entity
|
||||
*/
|
||||
async loadByFieldEqualingFromDatabaseAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
uniqueFieldName: N,
|
||||
fieldValue: NonNullable<TFields[N]>,
|
||||
modifiers: EntityLoaderRowLockingModifiers,
|
||||
): Promise<TEntity | null> {
|
||||
const entityResult = await this.knexEntityLoader.loadByFieldEqualingFromDatabaseAsync(
|
||||
uniqueFieldName,
|
||||
fieldValue,
|
||||
modifiers,
|
||||
);
|
||||
return entityResult ? entityResult.enforceValue() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load many entities where fieldName equals fieldValue directly from the database.
|
||||
* See {@link loadByIDFromDatabaseAsync} for how this differs from the standard loader.
|
||||
*
|
||||
* @param fieldName - entity field being queried
|
||||
* @param fieldValue - fieldName field value being queried
|
||||
* @param modifiers - row locking modifiers for the query
|
||||
* @returns array of entities where fieldName equals fieldValue
|
||||
* @throws EntityNotAuthorizedError when viewer is not authorized to view one or more of the returned entities
|
||||
*/
|
||||
async loadManyByFieldEqualingFromDatabaseAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
fieldName: N,
|
||||
fieldValue: NonNullable<TFields[N]>,
|
||||
modifiers: EntityLoaderRowLockingModifiers,
|
||||
): Promise<readonly TEntity[]> {
|
||||
const entityResults = await this.knexEntityLoader.loadManyByFieldEqualingFromDatabaseAsync(
|
||||
fieldName,
|
||||
fieldValue,
|
||||
modifiers,
|
||||
);
|
||||
return entityResults.map((entityResult) => entityResult.enforceValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the first entity matching the conjunction of field equality operands and
|
||||
* query modifiers.
|
||||
*
|
||||
* This is a convenience method for {@link loadManyByFieldEqualityConjunctionAsync}. However, the
|
||||
* orderBy query modifier is required to ensure consistent results if more than one entity matches
|
||||
* the filters.
|
||||
*
|
||||
* @throws EntityNotAuthorizedError if viewer is not authorized to view the entity
|
||||
* @returns the first entity matching the filters, or null if none match
|
||||
*/
|
||||
async loadFirstByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
fieldEqualityOperands: FieldEqualityCondition<TFields, N>[],
|
||||
querySelectionModifiers: Omit<
|
||||
EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields>,
|
||||
'limit'
|
||||
> &
|
||||
Required<Pick<EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields>, 'orderBy'>>,
|
||||
): Promise<TEntity | null> {
|
||||
const entityResult = await this.knexEntityLoader.loadFirstByFieldEqualityConjunctionAsync(
|
||||
fieldEqualityOperands,
|
||||
querySelectionModifiers,
|
||||
);
|
||||
return entityResult?.enforceValue() ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load entities matching the conjunction of field equality operands and
|
||||
* query modifiers.
|
||||
*
|
||||
* Typically this is used for complex queries that cannot be expressed through simpler
|
||||
* convenience methods such as {@link "@expo/entity"!EnforcingEntityLoader.loadManyByFieldEqualingAsync | EnforcingEntityLoader.loadManyByFieldEqualingAsync}.
|
||||
*
|
||||
* @throws EntityNotAuthorizedError if viewer is not authorized to view the entity
|
||||
* @returns entities matching the filters
|
||||
*/
|
||||
async loadManyByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
fieldEqualityOperands: FieldEqualityCondition<TFields, N>[],
|
||||
querySelectionModifiers: EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields> = {},
|
||||
): Promise<readonly TEntity[]> {
|
||||
const entityResults = await this.knexEntityLoader.loadManyByFieldEqualityConjunctionAsync(
|
||||
fieldEqualityOperands,
|
||||
querySelectionModifiers,
|
||||
);
|
||||
return entityResults.map((result) => result.enforceValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Count entities matching the conjunction of field equality operands.
|
||||
* This does not perform authorization since count does not load full entities.
|
||||
* Note that this should be used with the same caution as loadManyByFieldEqualityConjunctionAsync
|
||||
* regarding indexing since counts can be expensive on large datasets without appropriate indexes.
|
||||
*
|
||||
* @returns count of entities matching the filters
|
||||
*/
|
||||
async countByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
fieldEqualityOperands: FieldEqualityCondition<TFields, N>[],
|
||||
): Promise<number> {
|
||||
return await this.knexEntityLoader.countByFieldEqualityConjunctionAsync(fieldEqualityOperands);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count entities matching a SQL fragment.
|
||||
* This does not perform authorization since count does not load full entity rows.
|
||||
* Note that this should be used with the same caution as loadManyBySQL regarding indexing
|
||||
* since counts can be expensive on large datasets without appropriate indexes.
|
||||
*
|
||||
* @returns count of entities matching the query
|
||||
*/
|
||||
async countBySQLAsync(fragment: SQLFragment<Pick<TFields, TSelectedFields>>): Promise<number> {
|
||||
return await this.knexEntityLoader.countBySQLAsync(fragment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load entities using a SQL query builder. When executed, all queries will enforce authorization and throw if not authorized.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const entities = await ExampleEntity.loader(vc)
|
||||
* .loadManyBySQL(sql`age >= ${18} AND status = ${'active'}`)
|
||||
* .orderBy('createdAt', 'DESC')
|
||||
* .limit(10)
|
||||
* .executeAsync();
|
||||
*
|
||||
* const { between, inArray } = SQLExpression;
|
||||
* const filtered = await ExampleEntity.loader(vc)
|
||||
* .loadManyBySQL(
|
||||
* sql`${between('age', 18, 65)} AND ${inArray('role', ['admin', 'moderator'])}`
|
||||
* )
|
||||
* .executeAsync();
|
||||
* ```
|
||||
*/
|
||||
loadManyBySQL(
|
||||
fragment: SQLFragment<Pick<TFields, TSelectedFields>>,
|
||||
modifiers: EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields> = {},
|
||||
): EnforcingSQLQueryBuilder<
|
||||
TFields,
|
||||
TIDField,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
> {
|
||||
return new EnforcingSQLQueryBuilder(this.knexEntityLoader, fragment, modifiers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a page of entities with Relay-style cursor pagination.
|
||||
*
|
||||
* @param args - Pagination arguments with pagination and either first/after or last/before
|
||||
* @returns a page of entities matching the pagination arguments
|
||||
* @throws EntityNotAuthorizedError if viewer is not authorized to view any returned entity
|
||||
*/
|
||||
async loadPageAsync(
|
||||
args: EntityLoaderLoadPageArgs<TFields, TSelectedFields>,
|
||||
): Promise<Connection<TEntity>> {
|
||||
const pageResult = await this.knexDataManager.loadPageAsync(this.queryContext, args);
|
||||
const edges = await Promise.all(
|
||||
pageResult.edges.map(async (edge) => {
|
||||
const entityResult = await this.constructionUtils.constructAndAuthorizeEntityAsync(
|
||||
edge.node,
|
||||
);
|
||||
const entity = entityResult.enforceValue();
|
||||
return {
|
||||
...edge,
|
||||
node: entity,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
edges,
|
||||
pageInfo: pageResult.pageInfo,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cursor for a given entity that matches what loadPageAsync would produce.
|
||||
* Useful for constructing pagination cursors for entities returned from other loader methods that can then be passed to loadPageAsync for pagination.
|
||||
* Most commonly used for testing pagination behavior.
|
||||
*
|
||||
* @param entity - The entity to get the pagination cursor for.
|
||||
* @returns The pagination cursor for the given entity.
|
||||
*/
|
||||
getPaginationCursorForEntity(entity: TEntity): string {
|
||||
return this.knexEntityLoader.getPaginationCursorForEntity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL query builder for EnforcingKnexEntityLoader.
|
||||
* Provides a fluent API for building and executing SQL queries with enforced authorization.
|
||||
*/
|
||||
export class EnforcingSQLQueryBuilder<
|
||||
TFields extends Record<string, any>,
|
||||
TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TIDField,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields,
|
||||
> extends BaseSQLQueryBuilder<TFields, TSelectedFields, TEntity> {
|
||||
constructor(
|
||||
private readonly knexEntityLoader: AuthorizationResultBasedKnexEntityLoader<
|
||||
TFields,
|
||||
TIDField,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
sqlFragment: SQLFragment<Pick<TFields, TSelectedFields>>,
|
||||
modifiers: EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields>,
|
||||
) {
|
||||
super(sqlFragment, modifiers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query.
|
||||
* @returns entities matching the query
|
||||
* @throws EntityNotAuthorizedError if viewer is not authorized to view any entity
|
||||
*/
|
||||
async executeInternalAsync(): Promise<readonly TEntity[]> {
|
||||
const entityResults = await this.knexEntityLoader
|
||||
.loadManyBySQL(this.getSQLFragment(), this.getModifiers())
|
||||
.executeAsync();
|
||||
return entityResults.map((result) => result.enforceValue());
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,7 @@ import { EntityFieldDefinition } from '@expo/entity';
|
||||
/**
|
||||
* EntityFieldDefinition for a Postres column with a JS JSON array type.
|
||||
*/
|
||||
export class JSONArrayField<TRequireExplicitCache extends boolean> extends EntityFieldDefinition<
|
||||
any[],
|
||||
TRequireExplicitCache
|
||||
> {
|
||||
export class JSONArrayField extends EntityFieldDefinition<any[]> {
|
||||
protected validateInputValueInternal(value: any[]): boolean {
|
||||
return Array.isArray(value);
|
||||
}
|
||||
@@ -16,9 +13,7 @@ export class JSONArrayField<TRequireExplicitCache extends boolean> extends Entit
|
||||
* EntityFieldDefinition for a Postgres column that may be a JS JSON array type.
|
||||
* Does not do any validation.
|
||||
*/
|
||||
export class MaybeJSONArrayField<
|
||||
TRequireExplicitCache extends boolean,
|
||||
> extends EntityFieldDefinition<any | any[], TRequireExplicitCache> {
|
||||
export class MaybeJSONArrayField extends EntityFieldDefinition<any | any[]> {
|
||||
protected validateInputValueInternal(_value: any): boolean {
|
||||
return true;
|
||||
}
|
||||
@@ -27,10 +22,7 @@ export class MaybeJSONArrayField<
|
||||
/**
|
||||
* EntityFieldDefinition for a Postgres BIGINT column.
|
||||
*/
|
||||
export class BigIntField<TRequireExplicitCache extends boolean> extends EntityFieldDefinition<
|
||||
string,
|
||||
TRequireExplicitCache
|
||||
> {
|
||||
export class BigIntField extends EntityFieldDefinition<string> {
|
||||
protected validateInputValueInternal(value: string): boolean {
|
||||
return typeof value === 'string';
|
||||
}
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import type {
|
||||
EntityCompanion,
|
||||
EntityPrivacyPolicy,
|
||||
EntityPrivacyPolicyEvaluationContext,
|
||||
EntityQueryContext,
|
||||
IEntityMetricsAdapter,
|
||||
ReadonlyEntity,
|
||||
ViewerContext,
|
||||
} from '@expo/entity';
|
||||
import { EntityConstructionUtils } from '@expo/entity';
|
||||
|
||||
import { AuthorizationResultBasedKnexEntityLoader } from './AuthorizationResultBasedKnexEntityLoader.ts';
|
||||
import { EnforcingKnexEntityLoader } from './EnforcingKnexEntityLoader.ts';
|
||||
import type { EntityKnexDataManager } from './internal/EntityKnexDataManager.ts';
|
||||
|
||||
/**
|
||||
* The primary entry point for loading entities via knex queries (non-data-loader methods).
|
||||
*/
|
||||
export class KnexEntityLoaderFactory<
|
||||
TFields extends Record<string, any>,
|
||||
TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TIDField,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields,
|
||||
> {
|
||||
constructor(
|
||||
private readonly entityCompanion: EntityCompanion<
|
||||
TFields,
|
||||
TIDField,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
private readonly knexDataManager: EntityKnexDataManager<TFields, TIDField>,
|
||||
protected readonly metricsAdapter: IEntityMetricsAdapter,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Vend knex loader for loading an entity in a given query context.
|
||||
* @param viewerContext - viewer context of loading user
|
||||
* @param queryContext - query context in which to perform the load
|
||||
*/
|
||||
forLoad(
|
||||
viewerContext: TViewerContext,
|
||||
queryContext: EntityQueryContext,
|
||||
privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext<
|
||||
TFields,
|
||||
TIDField,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
): AuthorizationResultBasedKnexEntityLoader<
|
||||
TFields,
|
||||
TIDField,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
> {
|
||||
const constructionUtils = new EntityConstructionUtils(
|
||||
viewerContext,
|
||||
queryContext,
|
||||
privacyPolicyEvaluationContext,
|
||||
this.entityCompanion.entityCompanionDefinition.entityConfiguration,
|
||||
this.entityCompanion.entityCompanionDefinition.entityClass,
|
||||
this.entityCompanion.entityCompanionDefinition.entitySelectedFields,
|
||||
this.entityCompanion.privacyPolicy,
|
||||
this.metricsAdapter,
|
||||
);
|
||||
|
||||
return new AuthorizationResultBasedKnexEntityLoader(
|
||||
queryContext,
|
||||
this.entityCompanion.entityCompanionDefinition.entityConfiguration,
|
||||
this.entityCompanion.entityCompanionDefinition.entityClass,
|
||||
this.knexDataManager,
|
||||
this.metricsAdapter,
|
||||
constructionUtils,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vend enforcing knex loader for loading an entity in a given query context.
|
||||
* @param viewerContext - viewer context of loading user
|
||||
* @param queryContext - query context in which to perform the load
|
||||
*/
|
||||
forLoadEnforcing(
|
||||
viewerContext: TViewerContext,
|
||||
queryContext: EntityQueryContext,
|
||||
privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext<
|
||||
TFields,
|
||||
TIDField,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
): EnforcingKnexEntityLoader<
|
||||
TFields,
|
||||
TIDField,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
> {
|
||||
const constructionUtils = new EntityConstructionUtils(
|
||||
viewerContext,
|
||||
queryContext,
|
||||
privacyPolicyEvaluationContext,
|
||||
this.entityCompanion.entityCompanionDefinition.entityConfiguration,
|
||||
this.entityCompanion.entityCompanionDefinition.entityClass,
|
||||
this.entityCompanion.entityCompanionDefinition.entitySelectedFields,
|
||||
this.entityCompanion.privacyPolicy,
|
||||
this.metricsAdapter,
|
||||
);
|
||||
|
||||
return new EnforcingKnexEntityLoader(
|
||||
this.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext),
|
||||
queryContext,
|
||||
this.knexDataManager,
|
||||
this.metricsAdapter,
|
||||
constructionUtils,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* Search strategy for SQL-based pagination.
|
||||
*/
|
||||
export enum PaginationStrategy {
|
||||
/**
|
||||
* Standard pagination with ORDER BY. Results are ordered by the specified orderBy fields, with ID field automatically included for stable pagination if not already present.
|
||||
*/
|
||||
STANDARD = 'standard',
|
||||
|
||||
/**
|
||||
* Case-insensitive pattern matching search using SQL ILIKE operator.
|
||||
* Results are ordered by the fields being searched within in the order specified, then by ID for tie-breaking and stable pagination.
|
||||
*/
|
||||
ILIKE_SEARCH = 'ilike-search',
|
||||
|
||||
/**
|
||||
* Similarity search using PostgreSQL trigram similarity. Results are ordered by exact match priority, then by similarity score, then by specified extra order by fields if provided, then by ID for tie-breaking and stable pagination.
|
||||
*
|
||||
* Performance considerations:
|
||||
* - Trigram search can be significantly slower than ILIKE search, especially on large datasets without appropriate indexes.
|
||||
* - Consider using ILIKE search for smaller datasets or when exact substring matching is sufficient
|
||||
* - For larger datasets, ensure proper indexing or consider dedicated full-text search solutions.
|
||||
* - For optimal performance, create GIN or GIST indexes on searchable columns:
|
||||
* ```sql
|
||||
* CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
* CREATE INDEX idx_table_field_trigram ON table_name USING gin(field_name gin_trgm_ops);
|
||||
* -- Or for multiple columns:
|
||||
* CREATE INDEX idx_table_search ON table_name USING gin((field1 || ' ' || field2) gin_trgm_ops);
|
||||
* ```
|
||||
*/
|
||||
TRIGRAM_SEARCH = 'trigram',
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import type {
|
||||
EntityPrivacyPolicy,
|
||||
EntityQueryContext,
|
||||
IEntityClass,
|
||||
ReadonlyEntity,
|
||||
ViewerContext,
|
||||
} from '@expo/entity';
|
||||
import { Entity } from '@expo/entity';
|
||||
|
||||
import type { AuthorizationResultBasedKnexEntityLoader } from './AuthorizationResultBasedKnexEntityLoader.ts';
|
||||
import type { EnforcingKnexEntityLoader } from './EnforcingKnexEntityLoader.ts';
|
||||
import {
|
||||
knexLoader as knexLoaderFn,
|
||||
knexLoaderWithAuthorizationResults as knexLoaderWithAuthorizationResultsFn,
|
||||
} from './knexLoader.ts';
|
||||
|
||||
/**
|
||||
* Abstract base class for mutable entities backed by Postgres.
|
||||
* Provides `knexLoader` and `knexLoaderWithAuthorizationResults` as inherited static methods,
|
||||
* in addition to the mutation methods inherited from `Entity`.
|
||||
*/
|
||||
export abstract class PostgresEntity<
|
||||
TFields extends Record<string, any>,
|
||||
TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
|
||||
TViewerContext extends ViewerContext,
|
||||
TSelectedFields extends keyof TFields = keyof TFields,
|
||||
> extends Entity<TFields, TIDField, TViewerContext, TSelectedFields> {
|
||||
/**
|
||||
* Vend knex loader for loading entities via knex-specific methods in a given query context.
|
||||
*
|
||||
* @param viewerContext - viewer context of loading user
|
||||
* @param queryContext - query context in which to perform the load
|
||||
*/
|
||||
static knexLoader<
|
||||
TMFields extends object,
|
||||
TMIDField extends keyof NonNullable<Pick<TMFields, TMSelectedFields>>,
|
||||
TMViewerContext extends ViewerContext,
|
||||
TMViewerContext2 extends TMViewerContext,
|
||||
TMEntity extends ReadonlyEntity<TMFields, TMIDField, TMViewerContext, TMSelectedFields>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TMFields,
|
||||
TMIDField,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMSelectedFields
|
||||
>,
|
||||
TMSelectedFields extends keyof TMFields = keyof TMFields,
|
||||
>(
|
||||
this: IEntityClass<
|
||||
TMFields,
|
||||
TMIDField,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMPrivacyPolicy,
|
||||
TMSelectedFields
|
||||
>,
|
||||
viewerContext: TMViewerContext2,
|
||||
queryContext: EntityQueryContext = viewerContext
|
||||
.getViewerScopedEntityCompanionForClass(this)
|
||||
.getQueryContextProvider()
|
||||
.getQueryContext(),
|
||||
): EnforcingKnexEntityLoader<
|
||||
TMFields,
|
||||
TMIDField,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMPrivacyPolicy,
|
||||
TMSelectedFields
|
||||
> {
|
||||
return knexLoaderFn(this, viewerContext, queryContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vend knex loader for loading entities via knex-specific methods in a given query context.
|
||||
* Returns authorization results instead of throwing on authorization errors.
|
||||
*
|
||||
* @param viewerContext - viewer context of loading user
|
||||
* @param queryContext - query context in which to perform the load
|
||||
*/
|
||||
static knexLoaderWithAuthorizationResults<
|
||||
TMFields extends object,
|
||||
TMIDField extends keyof NonNullable<Pick<TMFields, TMSelectedFields>>,
|
||||
TMViewerContext extends ViewerContext,
|
||||
TMViewerContext2 extends TMViewerContext,
|
||||
TMEntity extends ReadonlyEntity<TMFields, TMIDField, TMViewerContext, TMSelectedFields>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TMFields,
|
||||
TMIDField,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMSelectedFields
|
||||
>,
|
||||
TMSelectedFields extends keyof TMFields = keyof TMFields,
|
||||
>(
|
||||
this: IEntityClass<
|
||||
TMFields,
|
||||
TMIDField,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMPrivacyPolicy,
|
||||
TMSelectedFields
|
||||
>,
|
||||
viewerContext: TMViewerContext2,
|
||||
queryContext: EntityQueryContext = viewerContext
|
||||
.getViewerScopedEntityCompanionForClass(this)
|
||||
.getQueryContextProvider()
|
||||
.getQueryContext(),
|
||||
): AuthorizationResultBasedKnexEntityLoader<
|
||||
TMFields,
|
||||
TMIDField,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMPrivacyPolicy,
|
||||
TMSelectedFields
|
||||
> {
|
||||
return knexLoaderWithAuthorizationResultsFn(this, viewerContext, queryContext);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,20 @@
|
||||
import type { EntityConfiguration, FieldTransformer, FieldTransformerMap } from '@expo/entity';
|
||||
import { getDatabaseFieldForEntityField, RESERVED_ENTITY_COUNT_QUERY_ALIAS } from '@expo/entity';
|
||||
import type { Knex } from 'knex';
|
||||
|
||||
import type {
|
||||
TableFieldMultiValueEqualityCondition,
|
||||
TableFieldSingleValueEqualityCondition,
|
||||
TableQuerySelectionModifiers,
|
||||
} from './BasePostgresEntityDatabaseAdapter.ts';
|
||||
import {
|
||||
BasePostgresEntityDatabaseAdapter,
|
||||
NullsOrdering,
|
||||
OrderByOrdering,
|
||||
} from './BasePostgresEntityDatabaseAdapter.ts';
|
||||
import { JSONArrayField, MaybeJSONArrayField } from './EntityFields.ts';
|
||||
import type { PostgresEntityDatabaseAdapterConfiguration } from './PostgresEntityDatabaseAdapterProvider.ts';
|
||||
import type { SQLFragment } from './SQLOperator.ts';
|
||||
import { wrapNativePostgresCallAsync } from './errors/wrapNativePostgresCallAsync.ts';
|
||||
EntityDatabaseAdapter,
|
||||
FieldTransformerMap,
|
||||
FieldTransformer,
|
||||
TableQuerySelectionModifiers,
|
||||
TableFieldSingleValueEqualityCondition,
|
||||
TableFieldMultiValueEqualityCondition,
|
||||
TableQuerySelectionModifiersWithOrderByRaw,
|
||||
} from '@expo/entity';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export class PostgresEntityDatabaseAdapter<
|
||||
import { JSONArrayField, MaybeJSONArrayField } from './EntityFields';
|
||||
import wrapNativePostgresCallAsync from './errors/wrapNativePostgresCallAsync';
|
||||
|
||||
export default class PostgresEntityDatabaseAdapter<
|
||||
TFields extends Record<string, any>,
|
||||
TIDField extends keyof TFields,
|
||||
> extends BasePostgresEntityDatabaseAdapter<TFields, TIDField> {
|
||||
constructor(
|
||||
entityConfiguration: EntityConfiguration<TFields, TIDField>,
|
||||
private readonly adapterConfiguration: PostgresEntityDatabaseAdapterConfiguration = {},
|
||||
) {
|
||||
super(entityConfiguration);
|
||||
}
|
||||
|
||||
override get paginationMaxPageSize(): number | undefined {
|
||||
return this.adapterConfiguration.paginationMaxPageSize;
|
||||
}
|
||||
|
||||
> extends EntityDatabaseAdapter<TFields> {
|
||||
protected getFieldTransformerMap(): FieldTransformerMap {
|
||||
return new Map<string, FieldTransformer<any>>([
|
||||
[
|
||||
@@ -61,102 +44,42 @@ export class PostgresEntityDatabaseAdapter<
|
||||
protected async fetchManyWhereInternalAsync(
|
||||
queryInterface: Knex,
|
||||
tableName: string,
|
||||
tableColumns: readonly string[],
|
||||
tableTuples: any[][],
|
||||
tableField: string,
|
||||
tableValues: readonly any[],
|
||||
): Promise<object[]> {
|
||||
// For single column queries, use the ANY operator to derive a consistent
|
||||
// query shape in the postgres query stats table.
|
||||
// This produces a query of the form `SELECT * FROM table WHERE ("id") = ANY(?)`
|
||||
// with value bindings of the form `[[1]]`, thus not making different value cardinalities
|
||||
// produce different query shapes.
|
||||
//
|
||||
// But for multi-column queries, we must use the IN operator as the ANY operator
|
||||
// does not support anonymous composite types. The solution to keep using the ANY operator would be explicit
|
||||
// postgres type casting on each value in each tableTuple, thus creating a unique query shape and defeating the purpose.
|
||||
// The same applies to using UNNEST on anonymous composite types.
|
||||
// Note that this solution is not possible in entity though since we don't have the postgres column types and they
|
||||
// can't be derived dynamically.
|
||||
//
|
||||
// Therefore, for multi-column quries, we use the IN operator which produces a query of the form
|
||||
// `SELECT * FROM table WHERE ("id", "name") IN ((?, ?), (?, ?))` with value bindings of the form
|
||||
// `[[1, 'a'], [2, 'b']]`, which will produce a unique query shape in the postgres query stats table for
|
||||
// each value cardinality.
|
||||
//
|
||||
// We could use the IN operator for single column queries as well, but we prefer to use ANY to at least keep some
|
||||
// consistency in the query shape for the stats table.
|
||||
|
||||
if (tableColumns.length === 1) {
|
||||
return await wrapNativePostgresCallAsync(() =>
|
||||
queryInterface
|
||||
.select()
|
||||
.from(tableName)
|
||||
.whereRaw(`(??) = ANY(?)`, [
|
||||
tableColumns[0]!,
|
||||
tableTuples.map((tableTuple) => tableTuple[0]),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
return await wrapNativePostgresCallAsync(() =>
|
||||
queryInterface.select().from(tableName).whereIn(tableColumns, tableTuples),
|
||||
queryInterface
|
||||
.select()
|
||||
.from(tableName)
|
||||
.whereRaw('?? = ANY(?)', [tableField, tableValues as any[]]),
|
||||
);
|
||||
}
|
||||
|
||||
protected async fetchOneWhereInternalAsync(
|
||||
queryInterface: Knex,
|
||||
tableName: string,
|
||||
tableColumns: readonly string[],
|
||||
tableTuple: readonly any[],
|
||||
): Promise<object | null> {
|
||||
const results = await this.fetchManyByFieldEqualityConjunctionInternalAsync(
|
||||
queryInterface,
|
||||
tableName,
|
||||
tableColumns.map((column, index) => ({
|
||||
tableField: column,
|
||||
tableValue: tableTuple[index],
|
||||
})),
|
||||
[],
|
||||
{
|
||||
limit: 1,
|
||||
orderBy: undefined,
|
||||
offset: undefined,
|
||||
forUpdate: undefined,
|
||||
forShare: undefined,
|
||||
skipLocked: undefined,
|
||||
},
|
||||
);
|
||||
return results[0] ?? null;
|
||||
private applyQueryModifiersToQueryOrderByRaw(
|
||||
query: Knex.QueryBuilder,
|
||||
querySelectionModifiers: TableQuerySelectionModifiersWithOrderByRaw,
|
||||
): Knex.QueryBuilder {
|
||||
let ret = this.applyQueryModifiersToQuery(query, querySelectionModifiers);
|
||||
|
||||
const { orderByRaw } = querySelectionModifiers;
|
||||
if (orderByRaw !== undefined) {
|
||||
ret = ret.orderByRaw(orderByRaw);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private applyQueryModifiersToQuery(
|
||||
query: Knex.QueryBuilder,
|
||||
querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
|
||||
querySelectionModifiers: TableQuerySelectionModifiers,
|
||||
): Knex.QueryBuilder {
|
||||
const { orderBy, offset, limit, forUpdate, forShare, skipLocked } = querySelectionModifiers;
|
||||
const { orderBy, offset, limit } = querySelectionModifiers;
|
||||
|
||||
let ret = query;
|
||||
|
||||
if (orderBy !== undefined) {
|
||||
for (const orderBySpecification of orderBy) {
|
||||
if ('columnName' in orderBySpecification) {
|
||||
ret = ret.orderBy(
|
||||
orderBySpecification.columnName,
|
||||
orderBySpecification.order,
|
||||
orderBySpecification.nulls,
|
||||
);
|
||||
} else {
|
||||
const orderDirection =
|
||||
orderBySpecification.order === OrderByOrdering.ASCENDING ? 'ASC' : 'DESC';
|
||||
const nullsSuffix = orderBySpecification.nulls
|
||||
? ` NULLS ${orderBySpecification.nulls === NullsOrdering.FIRST ? 'FIRST' : 'LAST'}`
|
||||
: '';
|
||||
ret = ret.orderByRaw(
|
||||
`(${orderBySpecification.columnFragment.sql}) ${orderDirection}${nullsSuffix}`,
|
||||
orderBySpecification.columnFragment.getKnexBindings((fieldName) =>
|
||||
getDatabaseFieldForEntityField(this.entityConfiguration, fieldName),
|
||||
),
|
||||
);
|
||||
}
|
||||
ret = ret.orderBy(orderBySpecification.columnName, orderBySpecification.order);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,27 +91,17 @@ export class PostgresEntityDatabaseAdapter<
|
||||
ret = ret.limit(limit);
|
||||
}
|
||||
|
||||
if (forUpdate) {
|
||||
ret = ret.forUpdate();
|
||||
}
|
||||
|
||||
if (forShare) {
|
||||
ret = ret.forShare();
|
||||
}
|
||||
|
||||
if (skipLocked) {
|
||||
ret = ret.skipLocked();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private applyFieldEqualityConjunctionWhereClause(
|
||||
query: Knex.QueryBuilder,
|
||||
protected async fetchManyByFieldEqualityConjunctionInternalAsync(
|
||||
queryInterface: Knex,
|
||||
tableName: string,
|
||||
tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
|
||||
tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
|
||||
): Knex.QueryBuilder {
|
||||
let result = query;
|
||||
querySelectionModifiers: TableQuerySelectionModifiers,
|
||||
): Promise<object[]> {
|
||||
let query = queryInterface.select().from(tableName);
|
||||
|
||||
if (tableFieldSingleValueEqualityOperands.length > 0) {
|
||||
const whereObject: { [key: string]: any } = {};
|
||||
@@ -201,11 +114,11 @@ export class PostgresEntityDatabaseAdapter<
|
||||
for (const { tableField, tableValue } of nonNullTableFieldSingleValueEqualityOperands) {
|
||||
whereObject[tableField] = tableValue;
|
||||
}
|
||||
result = result.where(whereObject);
|
||||
query = query.where(whereObject);
|
||||
}
|
||||
if (nullTableFieldSingleValueEqualityOperands.length > 0) {
|
||||
for (const { tableField } of nullTableFieldSingleValueEqualityOperands) {
|
||||
result = result.whereNull(tableField);
|
||||
query = query.whereNull(tableField);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,7 +126,7 @@ export class PostgresEntityDatabaseAdapter<
|
||||
if (tableFieldMultiValueEqualityOperands.length > 0) {
|
||||
for (const { tableField, tableValues } of tableFieldMultiValueEqualityOperands) {
|
||||
const nonNullTableValues = tableValues.filter((tableValue) => tableValue !== null);
|
||||
result = result.where((builder) => {
|
||||
query = query.where((builder) => {
|
||||
builder.whereRaw('?? = ANY(?)', [tableField, [...nonNullTableValues]]);
|
||||
// there was at least one null, allow null in this equality clause
|
||||
if (nonNullTableValues.length !== tableValues.length) {
|
||||
@@ -223,79 +136,25 @@ export class PostgresEntityDatabaseAdapter<
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected async fetchManyByFieldEqualityConjunctionInternalAsync(
|
||||
queryInterface: Knex,
|
||||
tableName: string,
|
||||
tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
|
||||
tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
|
||||
querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
|
||||
): Promise<object[]> {
|
||||
let query = this.applyFieldEqualityConjunctionWhereClause(
|
||||
queryInterface.select().from(tableName),
|
||||
tableFieldSingleValueEqualityOperands,
|
||||
tableFieldMultiValueEqualityOperands,
|
||||
);
|
||||
query = this.applyQueryModifiersToQuery(query, querySelectionModifiers);
|
||||
return await wrapNativePostgresCallAsync(() => query);
|
||||
}
|
||||
|
||||
private applySQLFragmentWhereClause(
|
||||
query: Knex.QueryBuilder,
|
||||
sqlFragment: SQLFragment<TFields>,
|
||||
): Knex.QueryBuilder {
|
||||
return query.whereRaw(
|
||||
sqlFragment.sql,
|
||||
sqlFragment.getKnexBindings((fieldName) =>
|
||||
getDatabaseFieldForEntityField(this.entityConfiguration, fieldName),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
protected async fetchManyBySQLFragmentInternalAsync(
|
||||
protected async fetchManyByRawWhereClauseInternalAsync(
|
||||
queryInterface: Knex,
|
||||
tableName: string,
|
||||
sqlFragment: SQLFragment<TFields>,
|
||||
querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
|
||||
rawWhereClause: string,
|
||||
bindings: object | any[],
|
||||
querySelectionModifiers: TableQuerySelectionModifiersWithOrderByRaw,
|
||||
): Promise<object[]> {
|
||||
let query = this.applySQLFragmentWhereClause(
|
||||
queryInterface.select().from(tableName),
|
||||
sqlFragment,
|
||||
);
|
||||
query = this.applyQueryModifiersToQuery(query, querySelectionModifiers);
|
||||
let query = queryInterface
|
||||
.select()
|
||||
.from(tableName)
|
||||
.whereRaw(rawWhereClause, bindings as any);
|
||||
query = this.applyQueryModifiersToQueryOrderByRaw(query, querySelectionModifiers);
|
||||
return await wrapNativePostgresCallAsync(() => query);
|
||||
}
|
||||
|
||||
protected async countByFieldEqualityConjunctionInternalAsync(
|
||||
queryInterface: Knex,
|
||||
tableName: string,
|
||||
tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
|
||||
tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
|
||||
): Promise<number> {
|
||||
const query = this.applyFieldEqualityConjunctionWhereClause(
|
||||
queryInterface.count('*', { as: RESERVED_ENTITY_COUNT_QUERY_ALIAS }).from(tableName),
|
||||
tableFieldSingleValueEqualityOperands,
|
||||
tableFieldMultiValueEqualityOperands,
|
||||
);
|
||||
const result = await wrapNativePostgresCallAsync(() => query);
|
||||
return parseInt(String(result[0][RESERVED_ENTITY_COUNT_QUERY_ALIAS]), 10);
|
||||
}
|
||||
|
||||
protected async countBySQLFragmentInternalAsync(
|
||||
queryInterface: Knex,
|
||||
tableName: string,
|
||||
sqlFragment: SQLFragment<TFields>,
|
||||
): Promise<number> {
|
||||
const query = this.applySQLFragmentWhereClause(
|
||||
queryInterface.count('*', { as: RESERVED_ENTITY_COUNT_QUERY_ALIAS }).from(tableName),
|
||||
sqlFragment,
|
||||
);
|
||||
const result = await wrapNativePostgresCallAsync(() => query);
|
||||
return parseInt(String(result[0][RESERVED_ENTITY_COUNT_QUERY_ALIAS]), 10);
|
||||
}
|
||||
|
||||
protected async insertInternalAsync(
|
||||
queryInterface: Knex,
|
||||
tableName: string,
|
||||
@@ -312,11 +171,10 @@ export class PostgresEntityDatabaseAdapter<
|
||||
tableIdField: string,
|
||||
id: any,
|
||||
object: object,
|
||||
): Promise<{ updatedRowCount: number }> {
|
||||
const updatedRowCount = await wrapNativePostgresCallAsync(() =>
|
||||
queryInterface.update(object).into(tableName).where(tableIdField, id),
|
||||
): Promise<object[]> {
|
||||
return await wrapNativePostgresCallAsync(() =>
|
||||
queryInterface.update(object).into(tableName).where(tableIdField, id).returning('*'),
|
||||
);
|
||||
return { updatedRowCount };
|
||||
}
|
||||
|
||||
protected async deleteInternalAsync(
|
||||
|
||||
Loaded 100 of 392 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user