Compare commits

..
Author SHA1 Message Date
Stanisław Chmiela 9c692afbcb fix: classify PostgreSQL lock timeouts as transient 2026-10-05 17:46:12 +02:00
44 changed files with 638 additions and 959 deletions

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@ on:
env:
# renovate: datasource=github-releases depName=jdx/mise
MISE_VERSION: v2026.10.19
MISE_VERSION: v2026.9.7
jobs:
build:
+1 -1
View File
@@ -4,4 +4,4 @@ npmMinimalAgeGate: 2d
npmPreapprovedPackages:
- '@expo/*'
- oxlint-config-universe
- 'oxlint-config-universe'
-18
View File
@@ -3,24 +3,6 @@
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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"packages": ["packages/*"],
"npmClient": "yarn",
"version": "0.72.0",
"version": "0.69.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+1 -1
View File
@@ -1,5 +1,5 @@
[tools]
node = "24.22.1"
node = "24.21.0"
[tasks.yarn]
run = "yarn install --immutable"
+6 -6
View File
@@ -23,20 +23,20 @@
},
"devDependencies": {
"@tsconfig/bases": "1.0.28",
"@types/node": "24.19.2",
"@types/node": "24.19.1",
"ctix": "2.8.2",
"jest": "30.5.2",
"jest": "30.5.1",
"lerna": "9.0.7",
"oxfmt": "0.73.0",
"oxlint": "1.88.0",
"oxfmt": "0.67.0",
"oxlint": "1.82.0",
"oxlint-config-universe": "0.2.0",
"oxlint-tsgolint": "0.25.0",
"prettier": "3.9.10",
"prettier": "3.9.6",
"typedoc": "0.28.20",
"typescript": "6.0.3"
},
"resolutions": {
"collect-v8-coverage": "^1.0.3"
},
"packageManager": "[email protected]8.1+sha512.b2e1e7524f654f2749d32b4ebcb4622473cb5bcbc485df2007e12a154e50162a4d795526768bc5f5b8f81717bfd79deb2472813d86fb5ae2eb551fa9c872b08f"
"packageManager": "[email protected]3.0+sha512.5c20ba010c99815433e5c8453112165e673f1c7948d8d2b267f4b5e52097538658388ebc9f9580656d9b75c5cc996f990f611f99304a2197d4c56d21eea370e7"
}
@@ -3,18 +3,6 @@
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
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-cache-adapter-local-memory",
"version": "0.72.0",
"version": "0.69.0",
"description": "Cross-request local memory cache adapter for @expo/entity",
"keywords": [
"entity"
@@ -31,7 +31,7 @@
"devDependencies": {
"@expo/entity-testing-utils": "workspace:^",
"@isaacs/ttlcache": "2.1.5",
"@jest/globals": "30.5.2",
"@jest/globals": "30.5.1",
"typescript": "6.0.3"
},
"engines": {
@@ -3,18 +3,6 @@
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
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-cache-adapter-redis",
"version": "0.72.0",
"version": "0.69.0",
"description": "Redis cache adapter for @expo/entity",
"keywords": [
"entity"
@@ -31,7 +31,7 @@
"devDependencies": {
"@expo/batcher": "1.1.2",
"@expo/entity-testing-utils": "workspace:^",
"@jest/globals": "30.5.2",
"@jest/globals": "30.5.1",
"ioredis": "5.11.1",
"ts-mockito": "2.6.1",
"typescript": "6.0.3"
-12
View File
@@ -3,18 +3,6 @@
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
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-codemod",
"version": "0.72.0",
"version": "0.69.0",
"description": "jscodeshift codemods for @expo/entity upgrades",
"keywords": [
"entity"
@@ -25,9 +25,9 @@
"test": "yarn test:all --rootDir $(pwd)"
},
"devDependencies": {
"@jest/globals": "30.5.2",
"@jest/globals": "30.5.1",
"@types/jscodeshift": "17.3.0",
"@types/node": "24.19.2",
"@types/node": "24.19.1",
"jscodeshift": "17.4.0",
"typescript": "6.0.3"
},
@@ -3,20 +3,6 @@
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
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-database-adapter-knex-testing-utils",
"version": "0.72.0",
"version": "0.69.0",
"description": "Testing utilities for applications using Entity with Knex database adapter",
"keywords": [
"entity"
@@ -27,14 +27,13 @@
"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",
"@jest/globals": "30.5.1",
"@types/invariant": "2.2.37",
"@types/node": "24.19.2",
"@types/node": "24.19.1",
"typescript": "6.0.3"
},
"peerDependencies": {
@@ -3,22 +3,6 @@
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
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-database-adapter-knex",
"version": "0.72.0",
"version": "0.69.0",
"description": "Knex database adapter for @expo/entity",
"keywords": [
"entity"
@@ -27,12 +27,11 @@
},
"dependencies": {
"@expo/entity": "workspace:^",
"@expo/results": "^1.0.0",
"knex": "^3.2.9"
},
"devDependencies": {
"@expo/entity-testing-utils": "workspace:^",
"@jest/globals": "30.5.2",
"@jest/globals": "30.5.1",
"pg": "8.23.1",
"ts-mockito": "2.6.1",
"typescript": "6.0.3"
@@ -1,4 +1,13 @@
import { EntityDatabaseAdapterUniqueConstraintError, ViewerContext } from '@expo/entity';
import {
EntityDatabaseAdapterCheckConstraintError,
EntityDatabaseAdapterExclusionConstraintError,
EntityDatabaseAdapterForeignKeyConstraintError,
EntityDatabaseAdapterNotNullConstraintError,
EntityDatabaseAdapterTransientError,
EntityDatabaseAdapterUniqueConstraintError,
EntityDatabaseAdapterUnknownError,
ViewerContext,
} from '@expo/entity';
import nullthrows from '@expo/nullthrows';
import { afterAll, beforeAll, beforeEach, describe, expect, it } from '@jest/globals';
import type { Knex } from 'knex';
@@ -6,15 +15,6 @@ import knex from 'knex';
import { ErrorsTestEntity } from '../__testfixtures__/ErrorsTestEntity.ts';
import { createKnexIntegrationTestEntityCompanionProvider } from '../__testfixtures__/createKnexIntegrationTestEntityCompanionProvider.ts';
import {
EntityDatabaseAdapterCheckConstraintError,
EntityDatabaseAdapterExclusionConstraintError,
EntityDatabaseAdapterForeignKeyConstraintError,
EntityDatabaseAdapterLockNotAvailableError,
EntityDatabaseAdapterNotNullConstraintError,
EntityDatabaseAdapterTransientError,
EntityDatabaseAdapterUnknownError,
} from '../errors/KnexEntityDatabaseAdapterError.ts';
describe('postgres errors', () => {
let knexInstance: Knex;
@@ -160,34 +160,6 @@ describe('postgres errors', () => {
).rejects.toThrow(EntityDatabaseAdapterExclusionConstraintError);
});
it('throws EntityDatabaseAdapterLockNotAvailableError when lock_timeout is exceeded', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const entity = await ErrorsTestEntity.creator(vc)
.setField('id', 1)
.setField('fieldNonNull', 'hello')
.createAsync();
// hold a row lock from a separate transaction while the entity update attempts to acquire it
const lockHolderTransaction = await knexInstance.transaction();
await lockHolderTransaction.raw(
'SELECT * FROM postgres_test_entities WHERE id = ? FOR UPDATE',
[entity.getID()],
);
try {
await expect(
vc.runInTransactionForDatabaseAdapterFlavorAsync('postgres', async (queryContext) => {
await queryContext.getQueryInterface().raw("SET LOCAL lock_timeout = '100ms'");
await ErrorsTestEntity.updater(entity, queryContext)
.setField('fieldNonNull', 'world')
.updateAsync();
}),
).rejects.toThrow(EntityDatabaseAdapterLockNotAvailableError);
} finally {
await lockHolderTransaction.rollback();
}
});
it('throws EntityDatabaseAdapterUnknownError otherwise', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await expect(
@@ -1,163 +0,0 @@
import { EntityDatabaseAdapterError, EntityErrorState } from '@expo/entity';
/**
* Error code for an entity error thrown by the knex database adapter.
* Each error code should map to a specific class of Entity error.
*/
export enum KnexEntityDatabaseAdapterErrorCode {
ERR_ENTITY_DATABASE_ADAPTER_TRANSIENT = 'ERR_ENTITY_DATABASE_ADAPTER_TRANSIENT',
ERR_ENTITY_DATABASE_ADAPTER_UNKNOWN = 'ERR_ENTITY_DATABASE_ADAPTER_UNKNOWN',
ERR_ENTITY_DATABASE_ADAPTER_CHECK_CONSTRAINT = 'ERR_ENTITY_DATABASE_ADAPTER_CHECK_CONSTRAINT',
ERR_ENTITY_DATABASE_ADAPTER_EXCLUSION_CONSTRAINT = 'ERR_ENTITY_DATABASE_ADAPTER_EXCLUSION_CONSTRAINT',
ERR_ENTITY_DATABASE_ADAPTER_FOREIGN_KEY_CONSTRAINT = 'ERR_ENTITY_DATABASE_ADAPTER_FOREIGN_KEY_CONSTRAINT',
ERR_ENTITY_DATABASE_ADAPTER_NOT_NULL_CONSTRAINT = 'ERR_ENTITY_DATABASE_ADAPTER_NOT_NULL_CONSTRAINT',
ERR_ENTITY_DATABASE_ADAPTER_PAGINATION_CURSOR_INVALID = 'ERR_ENTITY_DATABASE_ADAPTER_PAGINATION_CURSOR_INVALID',
ERR_ENTITY_DATABASE_ADAPTER_LOCK_NOT_AVAILABLE = 'ERR_ENTITY_DATABASE_ADAPTER_LOCK_NOT_AVAILABLE',
}
/**
* Thrown when a transient error occurrs within the database adapter.
* Transient errors may succeed if retried.
*/
export class EntityDatabaseAdapterTransientError extends EntityDatabaseAdapterError {
static {
this.prototype.name = 'EntityDatabaseAdapterTransientError';
}
get state(): EntityErrorState.TRANSIENT {
return EntityErrorState.TRANSIENT;
}
get code(): KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_TRANSIENT {
return KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_TRANSIENT;
}
}
/**
* Thrown when an unknown error occurrs within the database adapter.
* This is a catch-all error class for DBMS-specific errors that do not fit into other categories.
*/
export class EntityDatabaseAdapterUnknownError extends EntityDatabaseAdapterError {
static {
this.prototype.name = 'EntityDatabaseAdapterUnknownError';
}
get state(): EntityErrorState.UNKNOWN {
return EntityErrorState.UNKNOWN;
}
get code(): KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_UNKNOWN {
return KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_UNKNOWN;
}
}
/**
* Thrown when a check constraint is violated within the database adapter.
* This indicates that a value being inserted or updated does not satisfy a defined data integrity constraint.
*/
export class EntityDatabaseAdapterCheckConstraintError extends EntityDatabaseAdapterError {
static {
this.prototype.name = 'EntityDatabaseAdapterCheckConstraintError';
}
get state(): EntityErrorState.PERMANENT {
return EntityErrorState.PERMANENT;
}
get code(): KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_CHECK_CONSTRAINT {
return KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_CHECK_CONSTRAINT;
}
}
/**
* Thrown when an exclusion constraint is violated within the database adapter.
* This indicates that a value being inserted or updated conflicts with an existing value based on a defined exclusion constraint.
*/
export class EntityDatabaseAdapterExclusionConstraintError extends EntityDatabaseAdapterError {
static {
this.prototype.name = 'EntityDatabaseAdapterExclusionConstraintError';
}
get state(): EntityErrorState.PERMANENT {
return EntityErrorState.PERMANENT;
}
get code(): KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_EXCLUSION_CONSTRAINT {
return KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_EXCLUSION_CONSTRAINT;
}
}
/**
* Thrown when a foreign key constraint is violated within the database adapter.
* This indicates that a value being inserted, updated, or deleted references a non-existent value in a related table
* or is referenced in a related table.
*/
export class EntityDatabaseAdapterForeignKeyConstraintError extends EntityDatabaseAdapterError {
static {
this.prototype.name = 'EntityDatabaseAdapterForeignKeyConstraintError';
}
get state(): EntityErrorState.PERMANENT {
return EntityErrorState.PERMANENT;
}
get code(): KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_FOREIGN_KEY_CONSTRAINT {
return KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_FOREIGN_KEY_CONSTRAINT;
}
}
/**
* Thrown when a not-null constraint is violated within the database adapter.
* This indicates that a null value is being inserted or updated into a column that does not allow null values.
*/
export class EntityDatabaseAdapterNotNullConstraintError extends EntityDatabaseAdapterError {
static {
this.prototype.name = 'EntityDatabaseAdapterNotNullConstraintError';
}
get state(): EntityErrorState.PERMANENT {
return EntityErrorState.PERMANENT;
}
get code(): KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_NOT_NULL_CONSTRAINT {
return KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_NOT_NULL_CONSTRAINT;
}
}
/**
* Thrown when a pagination cursor cannot be decoded or does not match the expected pagination
* specification.
*/
export class EntityDatabaseAdapterPaginationCursorInvalidError extends EntityDatabaseAdapterError {
static {
this.prototype.name = 'EntityDatabaseAdapterPaginationCursorInvalidError';
}
get state(): EntityErrorState.PERMANENT {
return EntityErrorState.PERMANENT;
}
get code(): KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_PAGINATION_CURSOR_INVALID {
return KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_PAGINATION_CURSOR_INVALID;
}
}
/**
* Thrown when a lock on a row or table cannot be acquired (PostgreSQL error code 55P03, `lock_not_available`).
* This happens when a statement exceeds `lock_timeout` while waiting for a lock, or when a `NOWAIT`
* lock request conflicts with a lock held by another transaction.
* This error is transient since the conflicting lock is released when the other transaction completes.
*/
export class EntityDatabaseAdapterLockNotAvailableError extends EntityDatabaseAdapterError {
static {
this.prototype.name = 'EntityDatabaseAdapterLockNotAvailableError';
}
get state(): EntityErrorState.TRANSIENT {
return EntityErrorState.TRANSIENT;
}
get code(): KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_LOCK_NOT_AVAILABLE {
return KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_LOCK_NOT_AVAILABLE;
}
}
@@ -1,76 +0,0 @@
import { EntityDatabaseAdapterError, EntityErrorState } from '@expo/entity';
import { describe, expect, it } from '@jest/globals';
import {
EntityDatabaseAdapterCheckConstraintError,
EntityDatabaseAdapterExclusionConstraintError,
EntityDatabaseAdapterForeignKeyConstraintError,
EntityDatabaseAdapterLockNotAvailableError,
EntityDatabaseAdapterNotNullConstraintError,
EntityDatabaseAdapterPaginationCursorInvalidError,
EntityDatabaseAdapterTransientError,
EntityDatabaseAdapterUnknownError,
KnexEntityDatabaseAdapterErrorCode,
} from '../KnexEntityDatabaseAdapterError.ts';
describe('KnexEntityDatabaseAdapterError', () => {
it('instantiates all errors with correct state and code', () => {
const transientError = new EntityDatabaseAdapterTransientError('test');
expect(transientError).toBeInstanceOf(EntityDatabaseAdapterError);
expect(transientError.state).toBe(EntityErrorState.TRANSIENT);
expect(transientError.code).toBe(
KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_TRANSIENT,
);
const unknownError = new EntityDatabaseAdapterUnknownError('test');
expect(unknownError).toBeInstanceOf(EntityDatabaseAdapterError);
expect(unknownError.state).toBe(EntityErrorState.UNKNOWN);
expect(unknownError.code).toBe(
KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_UNKNOWN,
);
const checkError = new EntityDatabaseAdapterCheckConstraintError('test');
expect(checkError).toBeInstanceOf(EntityDatabaseAdapterError);
expect(checkError.state).toBe(EntityErrorState.PERMANENT);
expect(checkError.code).toBe(
KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_CHECK_CONSTRAINT,
);
const exclusionError = new EntityDatabaseAdapterExclusionConstraintError('test');
expect(exclusionError).toBeInstanceOf(EntityDatabaseAdapterError);
expect(exclusionError.state).toBe(EntityErrorState.PERMANENT);
expect(exclusionError.code).toBe(
KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_EXCLUSION_CONSTRAINT,
);
const foreignKeyError = new EntityDatabaseAdapterForeignKeyConstraintError('test');
expect(foreignKeyError).toBeInstanceOf(EntityDatabaseAdapterError);
expect(foreignKeyError.state).toBe(EntityErrorState.PERMANENT);
expect(foreignKeyError.code).toBe(
KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_FOREIGN_KEY_CONSTRAINT,
);
const notNullError = new EntityDatabaseAdapterNotNullConstraintError('test');
expect(notNullError).toBeInstanceOf(EntityDatabaseAdapterError);
expect(notNullError.state).toBe(EntityErrorState.PERMANENT);
expect(notNullError.code).toBe(
KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_NOT_NULL_CONSTRAINT,
);
const paginationCursorInvalidError = new EntityDatabaseAdapterPaginationCursorInvalidError(
'test',
);
expect(paginationCursorInvalidError).toBeInstanceOf(EntityDatabaseAdapterError);
expect(paginationCursorInvalidError.state).toBe(EntityErrorState.PERMANENT);
expect(paginationCursorInvalidError.code).toBe(
KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_PAGINATION_CURSOR_INVALID,
);
const lockNotAvailableError = new EntityDatabaseAdapterLockNotAvailableError('test');
expect(lockNotAvailableError).toBeInstanceOf(EntityDatabaseAdapterError);
expect(lockNotAvailableError.state).toBe(EntityErrorState.TRANSIENT);
expect(lockNotAvailableError.code).toBe(
KnexEntityDatabaseAdapterErrorCode.ERR_ENTITY_DATABASE_ADAPTER_LOCK_NOT_AVAILABLE,
);
});
});
@@ -1,11 +1,10 @@
import { EntityErrorState } from '@expo/entity';
import { EntityDatabaseAdapterTransientError, EntityErrorState } from '@expo/entity';
import { describe, expect, it } from '@jest/globals';
import { EntityDatabaseAdapterLockNotAvailableError } from '../KnexEntityDatabaseAdapterError.ts';
import { wrapNativePostgresCallAsync } from '../wrapNativePostgresCallAsync.ts';
describe(wrapNativePostgresCallAsync, () => {
it('translates a PostgreSQL lock_not_available error and preserves the cause', async () => {
it('classifies a PostgreSQL lock timeout as transient and preserves the cause', async () => {
const error = Object.assign(new Error('canceling statement due to lock timeout'), {
code: '55P03',
});
@@ -13,7 +12,7 @@ describe(wrapNativePostgresCallAsync, () => {
throw error;
});
await expect(result).rejects.toBeInstanceOf(EntityDatabaseAdapterLockNotAvailableError);
await expect(result).rejects.toBeInstanceOf(EntityDatabaseAdapterTransientError);
await expect(result).rejects.toMatchObject({
state: EntityErrorState.TRANSIENT,
message: error.message,
@@ -1,16 +1,14 @@
import type { EntityDatabaseAdapterError } from '@expo/entity';
import { EntityDatabaseAdapterUniqueConstraintError } from '@expo/entity';
import knex from 'knex';
import {
EntityDatabaseAdapterCheckConstraintError,
EntityDatabaseAdapterExclusionConstraintError,
EntityDatabaseAdapterForeignKeyConstraintError,
EntityDatabaseAdapterLockNotAvailableError,
EntityDatabaseAdapterNotNullConstraintError,
EntityDatabaseAdapterTransientError,
EntityDatabaseAdapterUniqueConstraintError,
EntityDatabaseAdapterUnknownError,
} from './KnexEntityDatabaseAdapterError.ts';
} from '@expo/entity';
import knex from 'knex';
function wrapNativePostgresError(
error: Error & { code?: string },
@@ -41,7 +39,7 @@ function translatePostgresError(
case '23P01':
return new EntityDatabaseAdapterExclusionConstraintError(error.message, error);
case '55P03':
return new EntityDatabaseAdapterLockNotAvailableError(error.message, error);
return new EntityDatabaseAdapterTransientError(error.message, error);
default:
return new EntityDatabaseAdapterUnknownError(error.message, error);
}
@@ -18,7 +18,6 @@ export * from './PostgresEntityDatabaseAdapterProvider.ts';
export * from './PostgresEntityQueryContextProvider.ts';
export * from './ReadonlyPostgresEntity.ts';
export * from './SQLOperator.ts';
export * from './errors/KnexEntityDatabaseAdapterError.ts';
export * from './errors/wrapNativePostgresCallAsync.ts';
export * from './internal/EntityKnexDataManager.ts';
export * from './internal/getKnexDataManager.ts';
@@ -1,5 +1,6 @@
import type { EntityConfiguration, EntityQueryContext, IEntityMetricsAdapter } from '@expo/entity';
import {
EntityDatabaseAdapterPaginationCursorInvalidError,
EntityMetricsCountType,
EntityMetricsLoadType,
getDatabaseFieldForEntityField,
@@ -17,7 +18,6 @@ import type {
import { NullsOrdering, OrderByOrdering } from '../BasePostgresEntityDatabaseAdapter.ts';
import { PaginationStrategy } from '../PaginationStrategy.ts';
import { SQLFragment, SQLExpression, identifier, sql, unsafeRaw } from '../SQLOperator.ts';
import { EntityDatabaseAdapterPaginationCursorInvalidError } from '../errors/KnexEntityDatabaseAdapterError.ts';
import type { DistributiveOmit, NonNullableKeys } from './utilityTypes.ts';
interface DataManagerStandardSpecification<TFields extends Record<string, any>> {
-12
View File
@@ -3,18 +3,6 @@
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-example
# [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-example
# [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-example
# [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-example
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-example",
"version": "0.72.0",
"version": "0.69.0",
"private": true,
"description": "An example integration of the @expo/entity framework",
"keywords": [
@@ -30,12 +30,12 @@
"koa": "3.2.1"
},
"devDependencies": {
"@jest/globals": "30.5.2",
"@jest/globals": "30.5.1",
"@types/koa": "3.0.3",
"@types/koa__cors": "5.0.1",
"@types/koa__router": "12.0.5",
"@types/supertest": "7.2.1",
"supertest": "7.3.1",
"supertest": "7.2.2",
"typescript": "6.0.3"
},
"engines": {
@@ -3,18 +3,6 @@
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-full-integration-tests
# [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-full-integration-tests
# [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-full-integration-tests
# [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-full-integration-tests
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-full-integration-tests",
"version": "0.72.0",
"version": "0.69.0",
"private": true,
"description": "Full redis and knex integration tests for the entity framework",
"keywords": [
@@ -22,7 +22,7 @@
"@expo/entity-database-adapter-knex": "workspace:^"
},
"devDependencies": {
"@jest/globals": "30.5.2",
"@jest/globals": "30.5.1",
"typescript": "6.0.3"
},
"engines": {
@@ -3,18 +3,6 @@
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-ip-address-field
# [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-ip-address-field
# [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-ip-address-field
# [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-ip-address-field
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-ip-address-field",
"version": "0.72.0",
"version": "0.69.0",
"description": "IP address EntityField definitions for @expo/entity",
"keywords": [
"entity"
@@ -30,7 +30,7 @@
},
"devDependencies": {
"@expo/entity-testing-utils": "workspace:^",
"@jest/globals": "30.5.2",
"@jest/globals": "30.5.1",
"typescript": "6.0.3"
},
"engines": {
@@ -3,18 +3,6 @@
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-secondary-cache-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-secondary-cache-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-secondary-cache-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-secondary-cache-local-memory
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-secondary-cache-local-memory",
"version": "0.72.0",
"version": "0.69.0",
"description": "Local memory secondary cache for @expo/entity",
"keywords": [
"entity"
@@ -32,7 +32,7 @@
"devDependencies": {
"@expo/nullthrows": "2.1.0",
"@isaacs/ttlcache": "2.1.5",
"@jest/globals": "30.5.2",
"@jest/globals": "30.5.1",
"typescript": "6.0.3"
},
"engines": {
@@ -3,18 +3,6 @@
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-secondary-cache-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-secondary-cache-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-secondary-cache-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-secondary-cache-redis
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-secondary-cache-redis",
"version": "0.72.0",
"version": "0.69.0",
"description": "Redis secondary cache for @expo/entity",
"keywords": [
"entity"
@@ -30,7 +30,7 @@
},
"devDependencies": {
"@expo/nullthrows": "2.1.0",
"@jest/globals": "30.5.2",
"@jest/globals": "30.5.1",
"ioredis": "5.11.1",
"typescript": "6.0.3"
},
@@ -3,20 +3,6 @@
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-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-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-testing-utils
+4 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-testing-utils",
"version": "0.72.0",
"version": "0.69.0",
"description": "A package containing utilities for testing applications that use Entity",
"keywords": [
"entity"
@@ -26,15 +26,13 @@
},
"dependencies": {
"@expo/entity": "workspace:^",
"invariant": "^2.2.4",
"lodash-es": "^4.17.23",
"ts-mockito": "^2.6.1",
"uuid": "^14.0.0"
"ts-mockito": "^2.6.1"
},
"devDependencies": {
"@jest/globals": "30.5.2",
"@jest/globals": "30.5.1",
"@types/lodash-es": "4.17.12",
"@types/node": "24.19.2",
"@types/node": "24.19.1",
"typescript": "6.0.3"
},
"peerDependencies": {
-16
View File
@@ -3,22 +3,6 @@
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)
**Note:** Version bump only for package @expo/entity
# [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
+4 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/entity",
"version": "0.72.0",
"version": "0.69.0",
"description": "A privacy-first data model",
"keywords": [
"entity"
@@ -25,20 +25,19 @@
"test": "yarn test:all --rootDir $(pwd)"
},
"dependencies": {
"@expo/nullthrows": "^2.1.0",
"@expo/results": "^1.0.0",
"dataloader": "^2.2.3",
"invariant": "^2.2.4"
},
"devDependencies": {
"@jest/globals": "30.5.2",
"@jest/globals": "30.5.1",
"@types/invariant": "2.2.37",
"@types/lodash-es": "4.17.12",
"@types/node": "24.19.2",
"@types/node": "24.19.1",
"lodash-es": "4.18.1",
"ts-mockito": "2.6.1",
"typescript": "6.0.3",
"uuid": "14.0.3"
"uuid": "14.0.2"
},
"engines": {
"node": ">=18"
+7 -31
View File
@@ -224,28 +224,17 @@ export abstract class EntityPrivacyPolicy<
* contributed, so the policy can select a message for a specific reason, for example "You must be a
* member of this account to view this project." when a rule skipped with a NOT_ACCOUNT_MEMBER reason.
*
* The evaluation context and the denied entity are passed so that the message can be selected based
* on them (for example, the entity type or whether the denial is part of a cascading deletion). The
* viewer is not authorized for the entity, and the evaluation context may contain it (as the previous
* value) or other entities (as the cascading deletion cause), so take care that the message does not
* reveal any data that the viewer may not see. Rules should communicate what is needed through reason codes.
* The denied entity and the evaluation context are intentionally not passed to this method. The viewer
* is not authorized to see the entity, and the evaluation context may contain it (as the previous value
* or the cascading deletion cause), so neither may influence or appear in the user-facing message.
* Rules should instead communicate what is needed through reason codes.
*
* @param _viewerContext - viewer context that was denied
* @param _evaluationContext - context in which the policy was evaluated
* @param _notAuthorizedEntity - entity for which the action was denied
* @param _action - the action that was denied
* @param _denialReason - structured, developer-facing reason for the denial
*/
protected getUserFacingDenialReason(
_viewerContext: TViewerContext,
_evaluationContext: EntityPrivacyPolicyEvaluationContext<
TFields,
TIDField,
TViewerContext,
TEntity,
TSelectedFields
>,
_notAuthorizedEntity: TEntity,
_action: EntityAuthorizationAction,
_denialReason: EntityNotAuthorizedDenialReason,
): EntityNotAuthorizedUserFacingReason | null {
@@ -534,7 +523,7 @@ export abstract class EntityPrivacyPolicy<
);
switch (result) {
case RuleEvaluationResult.DENY:
throw this.createNotAuthorizedError(viewerContext, evaluationContext, entity, action, {
throw this.createNotAuthorizedError(viewerContext, entity, action, {
type: EntityNotAuthorizedDenialType.RULE_DENIED,
rule: { ruleIndex: i, ruleName: rule.constructor.name, reasons },
skippedRules,
@@ -551,7 +540,7 @@ export abstract class EntityPrivacyPolicy<
}
}
throw this.createNotAuthorizedError(viewerContext, evaluationContext, entity, action, {
throw this.createNotAuthorizedError(viewerContext, entity, action, {
type: EntityNotAuthorizedDenialType.ALL_RULES_SKIPPED,
skippedRules,
});
@@ -559,24 +548,11 @@ export abstract class EntityPrivacyPolicy<
private createNotAuthorizedError(
viewerContext: TViewerContext,
evaluationContext: EntityPrivacyPolicyEvaluationContext<
TFields,
TIDField,
TViewerContext,
TEntity,
TSelectedFields
>,
entity: TEntity,
action: EntityAuthorizationAction,
denialReason: EntityNotAuthorizedDenialReason,
): EntityNotAuthorizedError<TFields, TIDField, TViewerContext, TEntity, TSelectedFields> {
const userFacingReason = this.getUserFacingDenialReason(
viewerContext,
evaluationContext,
entity,
action,
denialReason,
);
const userFacingReason = this.getUserFacingDenialReason(viewerContext, action, denialReason);
return new EntityNotAuthorizedError<
TFields,
TIDField,
@@ -398,13 +398,6 @@ class ReasonedDenialPolicy extends EntityPrivacyPolicy<
protected override getUserFacingDenialReason(
_viewerContext: ViewerContext,
_evaluationContext: EntityPrivacyPolicyEvaluationContext<
BlahFields,
'id',
ViewerContext,
BlahEntity
>,
notAuthorizedEntity: BlahEntity,
_action: EntityAuthorizationAction,
denialReason: EntityNotAuthorizedDenialReason,
): EntityNotAuthorizedUserFacingReason | null {
@@ -414,10 +407,7 @@ class ReasonedDenialPolicy extends EntityPrivacyPolicy<
skippedRule.reasons.includes(TestSkipReason.NOT_MEMBER),
)
) {
return {
code: 'MUST_BE_MEMBER',
message: `You must be a member to access this ${notAuthorizedEntity.constructor.name}.`,
};
return { code: 'MUST_BE_MEMBER', message: 'You must be a member to do this.' };
}
return null;
}
@@ -951,7 +941,7 @@ describe(EntityPrivacyPolicy, () => {
expect(error.message).not.toContain('NOT_OWNER');
expect(error.userFacingDenialReason).toEqual({
code: 'MUST_BE_MEMBER',
message: 'You must be a member to access this BlahEntity.',
message: 'You must be a member to do this.',
});
});
@@ -9,6 +9,115 @@ export abstract class EntityDatabaseAdapterError extends EntityError {
}
}
/**
* Thrown when a transient error occurrs within the database adapter.
* Transient errors may succeed if retried.
*/
export class EntityDatabaseAdapterTransientError extends EntityDatabaseAdapterError {
static {
this.prototype.name = 'EntityDatabaseAdapterTransientError';
}
get state(): EntityErrorState.TRANSIENT {
return EntityErrorState.TRANSIENT;
}
get code(): EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_TRANSIENT {
return EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_TRANSIENT;
}
}
/**
* Thrown when an unknown error occurrs within the database adapter.
* This is a catch-all error class for DBMS-specific errors that do not fit into other categories.
*/
export class EntityDatabaseAdapterUnknownError extends EntityDatabaseAdapterError {
static {
this.prototype.name = 'EntityDatabaseAdapterUnknownError';
}
get state(): EntityErrorState.UNKNOWN {
return EntityErrorState.UNKNOWN;
}
get code(): EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_UNKNOWN {
return EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_UNKNOWN;
}
}
/**
* Thrown when a check constraint is violated within the database adapter.
* This indicates that a value being inserted or updated does not satisfy a defined data integrity constraint.
*/
export class EntityDatabaseAdapterCheckConstraintError extends EntityDatabaseAdapterError {
static {
this.prototype.name = 'EntityDatabaseAdapterCheckConstraintError';
}
get state(): EntityErrorState.PERMANENT {
return EntityErrorState.PERMANENT;
}
get code(): EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_CHECK_CONSTRAINT {
return EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_CHECK_CONSTRAINT;
}
}
/**
* Thrown when an exclusion constraint is violated within the database adapter.
* This indicates that a value being inserted or updated conflicts with an existing value based on a defined exclusion constraint.
*/
export class EntityDatabaseAdapterExclusionConstraintError extends EntityDatabaseAdapterError {
static {
this.prototype.name = 'EntityDatabaseAdapterExclusionConstraintError';
}
get state(): EntityErrorState.PERMANENT {
return EntityErrorState.PERMANENT;
}
get code(): EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_EXCLUSION_CONSTRAINT {
return EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_EXCLUSION_CONSTRAINT;
}
}
/**
* Thrown when a foreign key constraint is violated within the database adapter.
* This indicates that a value being inserted, updated, or deleted references a non-existent value in a related table
* or is referenced in a related table.
*/
export class EntityDatabaseAdapterForeignKeyConstraintError extends EntityDatabaseAdapterError {
static {
this.prototype.name = 'EntityDatabaseAdapterForeignKeyConstraintError';
}
get state(): EntityErrorState.PERMANENT {
return EntityErrorState.PERMANENT;
}
get code(): EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_FOREIGN_KEY_CONSTRAINT {
return EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_FOREIGN_KEY_CONSTRAINT;
}
}
/**
* Thrown when a not-null constraint is violated within the database adapter.
* This indicates that a null value is being inserted or updated into a column that does not allow null values.
*/
export class EntityDatabaseAdapterNotNullConstraintError extends EntityDatabaseAdapterError {
static {
this.prototype.name = 'EntityDatabaseAdapterNotNullConstraintError';
}
get state(): EntityErrorState.PERMANENT {
return EntityErrorState.PERMANENT;
}
get code(): EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_NOT_NULL_CONSTRAINT {
return EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_NOT_NULL_CONSTRAINT;
}
}
/**
* Thrown when a unique constraint is violated within the database adapter.
* This indicates that a value being inserted or updated duplicates an existing value in a column or set of columns
@@ -122,3 +231,17 @@ export class EntityDatabaseAdapterExcessiveDeleteResultError extends EntityDatab
return EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_EXCESSIVE_DELETE_RESULT;
}
}
export class EntityDatabaseAdapterPaginationCursorInvalidError extends EntityDatabaseAdapterError {
static {
this.prototype.name = 'EntityDatabaseAdapterPaginationCursorError';
}
get state(): EntityErrorState.PERMANENT {
return EntityErrorState.PERMANENT;
}
get code(): EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_PAGINATION_CURSOR_INVALID {
return EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_PAGINATION_CURSOR_INVALID;
}
}
+9 -3
View File
@@ -8,13 +8,18 @@ export enum EntityErrorState {
}
/**
* Error code for an entity error defined in this package. Each error code should map to a specific class of Entity error.
* Other packages that define entity errors (such as database adapter implementations) define their own error code enums.
* Error code for an entity error. Each error code should map to a specific class of Entity error.
*/
export enum EntityErrorCode {
ERR_ENTITY_NOT_AUTHORIZED = 'ERR_ENTITY_NOT_AUTHORIZED',
ERR_ENTITY_NOT_FOUND = 'ERR_ENTITY_NOT_FOUND',
ERR_ENTITY_INVALID_FIELD_VALUE = 'ERR_ENTITY_INVALID_FIELD_VALUE',
ERR_ENTITY_DATABASE_ADAPTER_TRANSIENT = 'ERR_ENTITY_DATABASE_ADAPTER_TRANSIENT',
ERR_ENTITY_DATABASE_ADAPTER_UNKNOWN = 'ERR_ENTITY_DATABASE_ADAPTER_UNKNOWN',
ERR_ENTITY_DATABASE_ADAPTER_CHECK_CONSTRAINT = 'ERR_ENTITY_DATABASE_ADAPTER_CHECK_CONSTRAINT',
ERR_ENTITY_DATABASE_ADAPTER_EXCLUSION_CONSTRAINT = 'ERR_ENTITY_DATABASE_ADAPTER_EXCLUSION_CONSTRAINT',
ERR_ENTITY_DATABASE_ADAPTER_FOREIGN_KEY_CONSTRAINT = 'ERR_ENTITY_DATABASE_ADAPTER_FOREIGN_KEY_CONSTRAINT',
ERR_ENTITY_DATABASE_ADAPTER_NOT_NULL_CONSTRAINT = 'ERR_ENTITY_DATABASE_ADAPTER_NOT_NULL_CONSTRAINT',
ERR_ENTITY_DATABASE_ADAPTER_UNIQUE_CONSTRAINT = 'ERR_ENTITY_DATABASE_ADAPTER_UNIQUE_CONSTRAINT',
ERR_ENTITY_DATABASE_ADAPTER_EXCESSIVE_INSERT_RESULT = 'ERR_ENTITY_DATABASE_ADAPTER_EXCESSIVE_INSERT_RESULT',
ERR_ENTITY_DATABASE_ADAPTER_EMPTY_INSERT_RESULT = 'ERR_ENTITY_DATABASE_ADAPTER_EMPTY_INSERT_RESULT',
@@ -22,6 +27,7 @@ export enum EntityErrorCode {
ERR_ENTITY_DATABASE_ADAPTER_EMPTY_UPDATE_RESULT = 'ERR_ENTITY_DATABASE_ADAPTER_EMPTY_UPDATE_RESULT',
ERR_ENTITY_DATABASE_ADAPTER_EXCESSIVE_DELETE_RESULT = 'ERR_ENTITY_DATABASE_ADAPTER_EXCESSIVE_DELETE_RESULT',
ERR_ENTITY_CACHE_ADAPTER_TRANSIENT = 'ERR_ENTITY_CACHE_ADAPTER_TRANSIENT',
ERR_ENTITY_DATABASE_ADAPTER_PAGINATION_CURSOR_INVALID = 'ERR_ENTITY_DATABASE_ADAPTER_PAGINATION_CURSOR_INVALID',
}
/**
@@ -33,7 +39,7 @@ export abstract class EntityError extends Error {
}
public abstract readonly state: EntityErrorState;
public abstract readonly code: string;
public abstract readonly code: EntityErrorCode;
constructor(message: string, cause?: Error) {
super(message, { cause });
@@ -1,13 +1,20 @@
import { describe, expect, it } from '@jest/globals';
import {
EntityDatabaseAdapterCheckConstraintError,
EntityDatabaseAdapterEmptyInsertResultError,
EntityDatabaseAdapterEmptyUpdateResultError,
EntityDatabaseAdapterError,
EntityDatabaseAdapterExcessiveDeleteResultError,
EntityDatabaseAdapterExcessiveInsertResultError,
EntityDatabaseAdapterExcessiveUpdateResultError,
EntityDatabaseAdapterExclusionConstraintError,
EntityDatabaseAdapterForeignKeyConstraintError,
EntityDatabaseAdapterNotNullConstraintError,
EntityDatabaseAdapterPaginationCursorInvalidError,
EntityDatabaseAdapterTransientError,
EntityDatabaseAdapterUniqueConstraintError,
EntityDatabaseAdapterUnknownError,
} from '../EntityDatabaseAdapterError.ts';
import { EntityErrorCode, EntityErrorState } from '../EntityError.ts';
@@ -15,6 +22,34 @@ describe(EntityDatabaseAdapterError, () => {
// necessary for coverage within the entity package since these errors are
// currently only ever instantiated by database adapter implementations
it('instantiates all errors with correct state and code', () => {
const transientError = new EntityDatabaseAdapterTransientError('test');
expect(transientError.state).toBe(EntityErrorState.TRANSIENT);
expect(transientError.code).toBe(EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_TRANSIENT);
const unknownError = new EntityDatabaseAdapterUnknownError('test');
expect(unknownError.state).toBe(EntityErrorState.UNKNOWN);
expect(unknownError.code).toBe(EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_UNKNOWN);
const checkError = new EntityDatabaseAdapterCheckConstraintError('test');
expect(checkError.state).toBe(EntityErrorState.PERMANENT);
expect(checkError.code).toBe(EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_CHECK_CONSTRAINT);
const exclusionError = new EntityDatabaseAdapterExclusionConstraintError('test');
expect(exclusionError.state).toBe(EntityErrorState.PERMANENT);
expect(exclusionError.code).toBe(
EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_EXCLUSION_CONSTRAINT,
);
const foreignKeyError = new EntityDatabaseAdapterForeignKeyConstraintError('test');
expect(foreignKeyError.state).toBe(EntityErrorState.PERMANENT);
expect(foreignKeyError.code).toBe(
EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_FOREIGN_KEY_CONSTRAINT,
);
const notNullError = new EntityDatabaseAdapterNotNullConstraintError('test');
expect(notNullError.state).toBe(EntityErrorState.PERMANENT);
expect(notNullError.code).toBe(EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_NOT_NULL_CONSTRAINT);
const uniqueError = new EntityDatabaseAdapterUniqueConstraintError('test');
expect(uniqueError.state).toBe(EntityErrorState.PERMANENT);
expect(uniqueError.code).toBe(EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_UNIQUE_CONSTRAINT);
@@ -48,5 +83,13 @@ describe(EntityDatabaseAdapterError, () => {
expect(excessiveDeleteError.code).toBe(
EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_EXCESSIVE_DELETE_RESULT,
);
const paginationCursorInvalidError = new EntityDatabaseAdapterPaginationCursorInvalidError(
'test',
);
expect(paginationCursorInvalidError.state).toBe(EntityErrorState.PERMANENT);
expect(paginationCursorInvalidError.code).toBe(
EntityErrorCode.ERR_ENTITY_DATABASE_ADAPTER_PAGINATION_CURSOR_INVALID,
);
});
});
+2 -2
View File
@@ -1,13 +1,13 @@
version: '2'
services:
redis:
image: redis@sha256:97ec88159e6d7cfb5ddef9015386087d58a912de80112f06ebdfa78fa5989901
image: redis@sha256:6f81e8915c60b065a524e6967e0ad1c639ba6efa84d669f823683ea04d9150ee
ports:
- '${REDIS_PORT}:6379'
volumes:
- redis_data:/data
postgres:
image: postgres:18@sha256:edb0c08dc33befd0262ab3b5cf1c4f3f8cfd8b09e88c05b45adee29d6761ddb6
image: postgres:18@sha256:5a5a84b19854a9ffaa54082c166ff4ec27473a361e496e5ea167f298f2da9722
ports:
- '${PGPORT}:5432'
volumes:
Generated
+393 -402
View File
File diff suppressed because it is too large. Load diff