@bearei/nest 2.0.0-dev.2 → 2.0.0-dev.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +1 -1
- package/dist/base-service/base-service.service.d.ts +1 -0
- package/dist/base-service/base-service.service.js +29 -4
- package/dist/exception/exception.enum.d.ts +1 -0
- package/dist/exception/exception.enum.js +1 -0
- package/dist/exception/http.exception.js +1 -0
- package/package.json +6 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ All notable changes to this project are documented in this file. The format is b
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## 2.0.0-dev.4 — 2026-10-27
|
|
8
|
+
|
|
9
|
+
Development snapshot published under the `next` dist-tag for downstream service refactors. It
|
|
10
|
+
includes the 409 conflict mapping for writes that lose the pre-validation race and depends on
|
|
11
|
+
`@bearei/contracts@^2.1.0`. Its contents otherwise match the 2.0.0 scope documented below; it does
|
|
12
|
+
not replace the final 2.0.0 release notes.
|
|
13
|
+
|
|
14
|
+
Install it with `yarn add @bearei/nest@next` or pin `@bearei/nest@2.0.0-dev.4`.
|
|
15
|
+
|
|
16
|
+
## 2.0.0-dev.3 — 2026-10-27
|
|
17
|
+
|
|
18
|
+
Development snapshot published under the `next` dist-tag to verify the unified release pipeline
|
|
19
|
+
across all Bearei repositories: the artifact is pulled back from the registry, re-verified and
|
|
20
|
+
attached to the GitHub release. Its contents match the 2.0.0 scope documented below; it does not
|
|
21
|
+
replace the final 2.0.0 release notes.
|
|
22
|
+
|
|
23
|
+
Install it with `yarn add @bearei/nest@next` or pin `@bearei/nest@2.0.0-dev.3`.
|
|
24
|
+
|
|
7
25
|
## 2.0.0-dev.2 — 2026-10-27
|
|
8
26
|
|
|
9
27
|
Development snapshot published under the `next` dist-tag to verify the npm trusted publishing
|
|
@@ -61,6 +79,7 @@ baseline; because the npm identity is new, the Breaking list below is also the m
|
|
|
61
79
|
- `CommonEntity`: nanoid public `id`, internal numeric id, optimistic-locking `version`, UTC timestamps and idempotent inserts.
|
|
62
80
|
- `ListQueryDto`, `ByIdDto`, `ByEmailDto`, `ExcludeIdDto`, plus `PaginatedResult`, `ReservedRequestHeaders`, `AuthenticatedRequest` and string-literal union types for the shared enums.
|
|
63
81
|
- `ApiErrorBody` and `ReservedRequestHeaderName`, re-exported from `@bearei/contracts`.
|
|
82
|
+
- `HTTP_EXCEPTION.CONFLICT` for concurrent writes that collide with existing data, together with `HTTP_EXCEPTION_CODE.CONFLICT` (`409000`) from `@bearei/contracts`.
|
|
64
83
|
- `createResponseSanitizer` (replaces `createSanitizeResponseEntity`), `formatMobileNumber` (replaces `buildMobileNumber`) and `generateRandomNumberString` (replaces `generateRandomNumber`).
|
|
65
84
|
- `runWithDatabaseRetry` for transient database connection failures.
|
|
66
85
|
- `AllowAccess`, `RequirePermissions` and `RequireSubscriptionFeatures` decorators, alongside the existing validators.
|
|
@@ -82,3 +101,7 @@ baseline; because the npm identity is new, the Breaking list below is also the m
|
|
|
82
101
|
- The `crypto` runtime dependency; signing uses Node's built-in `crypto`.
|
|
83
102
|
- `ENTITY_STATUS`, `EntityStatus`, `entityStatus`, `beforeSoftRemove` and `beforeRecover`.
|
|
84
103
|
- The old `access/`, `app.*` and `authentication.service` modules.
|
|
104
|
+
|
|
105
|
+
### Fixed
|
|
106
|
+
|
|
107
|
+
- Writes that lose the pre-validation race now return `409 Conflict` with `CONFLICT` (`409000`) instead of leaking a PostgreSQL `23505` unique-constraint error as `500`. Covers `create`, `createMany`, `update` and `updateAssociation`; the failure is logged when action logging is enabled.
|
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ yarn lint # ESLint, no fixes
|
|
|
84
84
|
yarn typecheck # tsc --noEmit
|
|
85
85
|
yarn test # Jest with coverage thresholds
|
|
86
86
|
yarn build # compile to dist/
|
|
87
|
-
yarn verify # lint + typecheck + test + build
|
|
87
|
+
yarn verify # format + lint + typecheck + test + build
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
Packaging:
|
|
@@ -23,5 +23,6 @@ export declare class BaseService {
|
|
|
23
23
|
}, actionLogOptions?: ActionLogOptions): Promise<void>;
|
|
24
24
|
createSuccessActionLog<T extends CommonEntity>(entities: T | T[], actionLogOptions: ActionLogOptions): Promise<void>;
|
|
25
25
|
throwError({ httpExceptionKind, ...restOptions }: ThrowBaseServiceExceptionOptions, actionLogOptions?: ActionLogOptions): Promise<never>;
|
|
26
|
+
private throwUniqueViolation;
|
|
26
27
|
private preValidateUpdate;
|
|
27
28
|
}
|
|
@@ -35,7 +35,21 @@ const ASSOCIATION_FIND_OPTION_KEYS = [
|
|
|
35
35
|
'withTotal'
|
|
36
36
|
];
|
|
37
37
|
const ASSOCIATION_BATCH_SIZE = 1024;
|
|
38
|
+
const UNIQUE_VIOLATION_ERROR_CODE = '23505';
|
|
38
39
|
const isNotFoundException = (error) => error instanceof common_1.HttpException && error.getStatus() === common_1.HttpStatus.NOT_FOUND;
|
|
40
|
+
const isUniqueViolationError = (error) => error instanceof typeorm_1.QueryFailedError &&
|
|
41
|
+
error.driverError?.code === UNIQUE_VIOLATION_ERROR_CODE;
|
|
42
|
+
const mapUniqueViolation = async (write, onConflict) => {
|
|
43
|
+
try {
|
|
44
|
+
return await write();
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (!isUniqueViolationError(error)) {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
return onConflict();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
39
53
|
const normalizePagination = (page, pageSize) => {
|
|
40
54
|
const normalizedPage = Math.max(1, Math.trunc(Number(page) || 1));
|
|
41
55
|
const normalizedPageSize = Math.min(contracts_1.MAX_PAGE_SIZE, Math.max(1, Math.trunc(Number(pageSize) || contracts_1.DEFAULT_PAGE_SIZE)));
|
|
@@ -98,7 +112,7 @@ let BaseService = class BaseService {
|
|
|
98
112
|
}
|
|
99
113
|
const createPayload = omitKeys(entity, SERVER_MANAGED_CREATE_KEYS);
|
|
100
114
|
const createdEntity = repository.create(createPayload);
|
|
101
|
-
const savedEntity = await (manager ? manager.save(createdEntity) : createdEntity.save());
|
|
115
|
+
const savedEntity = await mapUniqueViolation(() => (manager ? manager.save(createdEntity) : createdEntity.save()), () => this.throwUniqueViolation(actionLogOptions));
|
|
102
116
|
void this.createSuccessActionLog(savedEntity, actionLogOptions);
|
|
103
117
|
return savedEntity;
|
|
104
118
|
});
|
|
@@ -106,7 +120,7 @@ let BaseService = class BaseService {
|
|
|
106
120
|
createMany(repository, entities, manager) {
|
|
107
121
|
const createPayloads = entities.map(entity => omitKeys(entity, SERVER_MANAGED_CREATE_KEYS));
|
|
108
122
|
const createdEntities = repository.create(createPayloads);
|
|
109
|
-
return manager ? manager.save(createdEntities) : repository.save(createdEntities);
|
|
123
|
+
return mapUniqueViolation(() => (manager ? manager.save(createdEntities) : repository.save(createdEntities)), () => this.throwUniqueViolation());
|
|
110
124
|
}
|
|
111
125
|
delete(repository, { headers, isLogEnabled = true, serviceName, where }, manager) {
|
|
112
126
|
const actionLogOptions = {
|
|
@@ -165,7 +179,7 @@ let BaseService = class BaseService {
|
|
|
165
179
|
}
|
|
166
180
|
const updatePayload = omitKeys(entity, IMMUTABLE_ENTITY_KEYS);
|
|
167
181
|
repository.merge(currentEntity, updatePayload);
|
|
168
|
-
await (manager ? manager.save(currentEntity) : currentEntity.save());
|
|
182
|
+
await mapUniqueViolation(() => (manager ? manager.save(currentEntity) : currentEntity.save()), () => this.throwUniqueViolation(actionLogOptions));
|
|
169
183
|
void this.createSuccessActionLog(currentEntity, actionLogOptions);
|
|
170
184
|
return currentEntity;
|
|
171
185
|
});
|
|
@@ -373,7 +387,7 @@ let BaseService = class BaseService {
|
|
|
373
387
|
}
|
|
374
388
|
const manager = (entityManager ?? repository);
|
|
375
389
|
if (entitiesToAdd.length) {
|
|
376
|
-
await manager.save(entitiesToAdd);
|
|
390
|
+
await mapUniqueViolation(() => manager.save(entitiesToAdd), () => this.throwUniqueViolation(actionLogOptions));
|
|
377
391
|
}
|
|
378
392
|
if (entitiesToRecover.length) {
|
|
379
393
|
await manager.recover(entitiesToRecover);
|
|
@@ -481,6 +495,17 @@ let BaseService = class BaseService {
|
|
|
481
495
|
}
|
|
482
496
|
throw error;
|
|
483
497
|
}
|
|
498
|
+
async throwUniqueViolation(actionLogOptions) {
|
|
499
|
+
const serviceName = actionLogOptions?.serviceName;
|
|
500
|
+
const message = serviceName ?
|
|
501
|
+
`The ${serviceName} fields already exists.`
|
|
502
|
+
: 'A record with the same unique fields already exists.';
|
|
503
|
+
return this.throwError({
|
|
504
|
+
code: exception_1.HTTP_EXCEPTION_CODE.CONFLICT,
|
|
505
|
+
httpExceptionKind: exception_1.HTTP_EXCEPTION.CONFLICT,
|
|
506
|
+
message
|
|
507
|
+
}, actionLogOptions);
|
|
508
|
+
}
|
|
484
509
|
async preValidateUpdate({ actionLogOptions, currentEntity, preValidationOptions, repository, serviceName, where }) {
|
|
485
510
|
const { validationKeys = [], where: preValidationWhere, ...restOptions } = preValidationOptions;
|
|
486
511
|
const validationKeysSet = new Set(validationKeys.map(String));
|
|
@@ -2,6 +2,7 @@ export { HTTP_EXCEPTION_CODE } from '@bearei/contracts';
|
|
|
2
2
|
export declare const HTTP_EXCEPTION: {
|
|
3
3
|
readonly BAD_GATEWAY: "BAD_GATEWAY";
|
|
4
4
|
readonly BAD_REQUEST: "BAD_REQUEST";
|
|
5
|
+
readonly CONFLICT: "CONFLICT";
|
|
5
6
|
readonly FORBIDDEN: "FORBIDDEN";
|
|
6
7
|
readonly GENERIC: "GENERIC";
|
|
7
8
|
readonly INTERNAL_SERVER_ERROR: "INTERNAL_SERVER_ERROR";
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "HTTP_EXCEPTION_CODE", { enumerable: true, get: f
|
|
|
6
6
|
exports.HTTP_EXCEPTION = {
|
|
7
7
|
BAD_GATEWAY: 'BAD_GATEWAY',
|
|
8
8
|
BAD_REQUEST: 'BAD_REQUEST',
|
|
9
|
+
CONFLICT: 'CONFLICT',
|
|
9
10
|
FORBIDDEN: 'FORBIDDEN',
|
|
10
11
|
GENERIC: 'GENERIC',
|
|
11
12
|
INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR',
|
|
@@ -15,6 +15,7 @@ const EXCEPTION_FACTORY_MAP = {
|
|
|
15
15
|
message: buildMessage(message, service),
|
|
16
16
|
statusCode: common_1.HttpStatus.BAD_REQUEST
|
|
17
17
|
}),
|
|
18
|
+
[exception_enum_1.HTTP_EXCEPTION.CONFLICT]: ({ code = exception_enum_1.HTTP_EXCEPTION_CODE.CONFLICT, message = 'Conflict', service }) => new common_1.ConflictException({ code, message: buildMessage(message, service), statusCode: common_1.HttpStatus.CONFLICT }),
|
|
18
19
|
[exception_enum_1.HTTP_EXCEPTION.FORBIDDEN]: ({ code = exception_enum_1.HTTP_EXCEPTION_CODE.FORBIDDEN, message = 'Forbidden', service }) => new common_1.ForbiddenException({ code, message: buildMessage(message, service), statusCode: common_1.HttpStatus.FORBIDDEN }),
|
|
19
20
|
[exception_enum_1.HTTP_EXCEPTION.GENERIC]: ({ code = exception_enum_1.HTTP_EXCEPTION_CODE.INTERNAL_SERVER_ERROR, message = 'Internal Server Error', service, status = common_1.HttpStatus.INTERNAL_SERVER_ERROR }) => new common_1.HttpException({ code, message: buildMessage(message, service), statusCode: status }, status),
|
|
20
21
|
[exception_enum_1.HTTP_EXCEPTION.INTERNAL_SERVER_ERROR]: ({ code = exception_enum_1.HTTP_EXCEPTION_CODE.INTERNAL_SERVER_ERROR, message = 'Internal Server Error', service }) => new common_1.InternalServerErrorException({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bearei/nest",
|
|
3
|
-
"version": "2.0.0-dev.
|
|
3
|
+
"version": "2.0.0-dev.4",
|
|
4
4
|
"description": "Shared NestJS and TypeORM building blocks for Bearei services",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bearei",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@bearei/contracts": "^2.
|
|
41
|
+
"@bearei/contracts": "^2.1.0",
|
|
42
42
|
"bcrypt": "^5.1.1",
|
|
43
43
|
"dayjs": "^1.11.13",
|
|
44
44
|
"ioredis": "^5.4.1",
|
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
"typeorm": "^1.0.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@bearei/eslint-config": "^2.0.
|
|
63
|
-
"@bearei/jest-preset": "^2.0.
|
|
64
|
-
"@bearei/tsconfig": "^2.0.
|
|
62
|
+
"@bearei/eslint-config": "^2.1.0-dev.2",
|
|
63
|
+
"@bearei/jest-preset": "^2.1.0-dev.2",
|
|
64
|
+
"@bearei/tsconfig": "^2.1.0-dev.2",
|
|
65
65
|
"@nestjs/axios": "^4.0.1",
|
|
66
66
|
"@nestjs/cli": "^11.0.0",
|
|
67
67
|
"@nestjs/common": "^11.1.27",
|
|
@@ -119,11 +119,10 @@
|
|
|
119
119
|
"prepack": "yarn build",
|
|
120
120
|
"test": "jest",
|
|
121
121
|
"test:coverage": "jest --coverage",
|
|
122
|
-
"test:cov": "jest --coverage",
|
|
123
122
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
|
124
123
|
"test:watch": "jest --watch",
|
|
125
124
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
126
|
-
"verify": "yarn lint && yarn typecheck && yarn test:coverage && yarn build"
|
|
125
|
+
"verify": "yarn prettier --check . && yarn lint && yarn typecheck && yarn test:coverage && yarn build"
|
|
127
126
|
},
|
|
128
127
|
"packageManager": "yarn@4.18.0+sha512.fcb8716fe7cd0eece141ffc18b92193a9df9204c1ba83189c288835223fc0bbe64af473bab0d5e9927a7daeb5caf2bb07eb2787cc9338ca040ea125f2a1f2f7e"
|
|
129
128
|
}
|