@bearei/nest 2.0.0-dev.3 → 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 CHANGED
@@ -4,6 +4,15 @@ 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
+
7
16
  ## 2.0.0-dev.3 — 2026-10-27
8
17
 
9
18
  Development snapshot published under the `next` dist-tag to verify the unified release pipeline
@@ -70,6 +79,7 @@ baseline; because the npm identity is new, the Breaking list below is also the m
70
79
  - `CommonEntity`: nanoid public `id`, internal numeric id, optimistic-locking `version`, UTC timestamps and idempotent inserts.
71
80
  - `ListQueryDto`, `ByIdDto`, `ByEmailDto`, `ExcludeIdDto`, plus `PaginatedResult`, `ReservedRequestHeaders`, `AuthenticatedRequest` and string-literal union types for the shared enums.
72
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`.
73
83
  - `createResponseSanitizer` (replaces `createSanitizeResponseEntity`), `formatMobileNumber` (replaces `buildMobileNumber`) and `generateRandomNumberString` (replaces `generateRandomNumber`).
74
84
  - `runWithDatabaseRetry` for transient database connection failures.
75
85
  - `AllowAccess`, `RequirePermissions` and `RequireSubscriptionFeatures` decorators, alongside the existing validators.
@@ -91,3 +101,7 @@ baseline; because the npm identity is new, the Breaking list below is also the m
91
101
  - The `crypto` runtime dependency; signing uses Node's built-in `crypto`.
92
102
  - `ENTITY_STATUS`, `EntityStatus`, `entityStatus`, `beforeSoftRemove` and `beforeRecover`.
93
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",
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.0.0",
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.0",
63
- "@bearei/jest-preset": "^2.0.0",
64
- "@bearei/tsconfig": "^2.0.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
  }