@uityu/uityu-shared 1.0.0
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/README.md +45 -0
- package/dist/errores/app-error.d.ts +7 -0
- package/dist/errores/app-error.js +12 -0
- package/dist/errores/error-codes.d.ts +9 -0
- package/dist/errores/error-codes.js +8 -0
- package/dist/errors/app-error.d.ts +6 -0
- package/dist/errors/app-error.factory.d.ts +63 -0
- package/dist/errors/app-error.factory.js +65 -0
- package/dist/errors/app-error.js +12 -0
- package/dist/errors/error-codes.d.ts +58 -0
- package/dist/errors/error-codes.js +57 -0
- package/dist/http/error-mapper.d.ts +10 -0
- package/dist/http/error-mapper.js +19 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/search/search-index.d.ts +6 -0
- package/dist/search/search-index.js +39 -0
- package/package.json +11 -0
- package/src/errors/app-error.factory.ts +184 -0
- package/src/errors/app-error.ts +11 -0
- package/src/errors/error-codes.ts +59 -0
- package/src/http/error-mapper.ts +31 -0
- package/src/index.ts +5 -0
- package/src/search/search-index.ts +52 -0
- package/tsconfig.json +12 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
**Edit a file, create a new file, and clone from Bitbucket in under 2 minutes**
|
|
2
|
+
|
|
3
|
+
When you're done, you can delete the content in this README and update the file with details for others getting started with your repository.
|
|
4
|
+
|
|
5
|
+
*We recommend that you open this README in another tab as you perform the tasks below. You can [watch our video](https://youtu.be/0ocf7u76WSo) for a full demo of all the steps in this tutorial. Open the video in a new tab to avoid leaving Bitbucket.*
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Edit a file
|
|
10
|
+
|
|
11
|
+
You’ll start by editing this README file to learn how to edit a file in Bitbucket.
|
|
12
|
+
|
|
13
|
+
1. Click **Source** on the left side.
|
|
14
|
+
2. Click the README.md link from the list of files.
|
|
15
|
+
3. Click the **Edit** button.
|
|
16
|
+
4. Delete the following text: *Delete this line to make a change to the README from Bitbucket.*
|
|
17
|
+
5. After making your change, click **Commit** and then **Commit** again in the dialog. The commit page will open and you’ll see the change you just made.
|
|
18
|
+
6. Go back to the **Source** page.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Create a file
|
|
23
|
+
|
|
24
|
+
Next, you’ll add a new file to this repository.
|
|
25
|
+
|
|
26
|
+
1. Click the **New file** button at the top of the **Source** page.
|
|
27
|
+
2. Give the file a filename of **contributors.txt**.
|
|
28
|
+
3. Enter your name in the empty file space.
|
|
29
|
+
4. Click **Commit** and then **Commit** again in the dialog.
|
|
30
|
+
5. Go back to the **Source** page.
|
|
31
|
+
|
|
32
|
+
Before you move on, go ahead and explore the repository. You've already seen the **Source** page, but check out the **Commits**, **Branches**, and **Settings** pages.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Clone a repository
|
|
37
|
+
|
|
38
|
+
Use these steps to clone from SourceTree, our client for using the repository command-line free. Cloning allows you to work on your files locally. If you don't yet have SourceTree, [download and install first](https://www.sourcetreeapp.com/). If you prefer to clone from the command line, see [Clone a repository](https://confluence.atlassian.com/x/4whODQ).
|
|
39
|
+
|
|
40
|
+
1. You’ll see the clone button under the **Source** heading. Click that button.
|
|
41
|
+
2. Now click **Check out in SourceTree**. You may need to create a SourceTree account or log in.
|
|
42
|
+
3. When you see the **Clone New** dialog in SourceTree, update the destination path and name if you’d like to and then click **Clone**.
|
|
43
|
+
4. Open the directory you just created to see your repository’s files.
|
|
44
|
+
|
|
45
|
+
Now that you're more familiar with your Bitbucket repository, go ahead and add a new file locally. You can [push your change back to Bitbucket with SourceTree](https://confluence.atlassian.com/x/iqyBMg), or you can [add, commit,](https://confluence.atlassian.com/x/8QhODQ) and [push from the command line](https://confluence.atlassian.com/x/NQ0zDQ).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ErrorCode } from "./error-codes";
|
|
2
|
+
export declare class AppError extends Error {
|
|
3
|
+
code: ErrorCode;
|
|
4
|
+
status: number;
|
|
5
|
+
errors: Record<string, string>;
|
|
6
|
+
constructor(code: ErrorCode, message?: string, status?: number, errors?: Record<string, string>);
|
|
7
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const ErrorCode: {
|
|
2
|
+
readonly VALIDATION: "validation";
|
|
3
|
+
readonly UNAUTHORIZED: "unauthorized";
|
|
4
|
+
readonly FORBIDDEN: "forbidden";
|
|
5
|
+
readonly NOT_FOUND: "not-found";
|
|
6
|
+
readonly CONFLICT: "conflict";
|
|
7
|
+
readonly INTERNAL: "internal";
|
|
8
|
+
};
|
|
9
|
+
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { AppError } from "./app-error";
|
|
2
|
+
export type AppErrorFactoryOptions = {
|
|
3
|
+
message?: string;
|
|
4
|
+
status?: number;
|
|
5
|
+
errors?: Record<string, string>;
|
|
6
|
+
};
|
|
7
|
+
export declare const AppErrorFactory: {
|
|
8
|
+
authRequired: (options?: AppErrorFactoryOptions) => AppError;
|
|
9
|
+
badRequest: (options?: AppErrorFactoryOptions) => AppError;
|
|
10
|
+
channelExists: (options?: AppErrorFactoryOptions) => AppError;
|
|
11
|
+
channelNotFound: (options?: AppErrorFactoryOptions) => AppError;
|
|
12
|
+
config: (field?: string, options?: AppErrorFactoryOptions) => AppError;
|
|
13
|
+
conflict: (options?: AppErrorFactoryOptions) => AppError;
|
|
14
|
+
courierDocumentTaken: (options?: AppErrorFactoryOptions) => AppError;
|
|
15
|
+
courierNotFound: (options?: AppErrorFactoryOptions) => AppError;
|
|
16
|
+
courierPhoneTaken: (options?: AppErrorFactoryOptions) => AppError;
|
|
17
|
+
customerNotFound: (options?: AppErrorFactoryOptions) => AppError;
|
|
18
|
+
forbidden: (options?: AppErrorFactoryOptions) => AppError;
|
|
19
|
+
invalidJson: (options?: AppErrorFactoryOptions) => AppError;
|
|
20
|
+
invalidReport: (options?: AppErrorFactoryOptions) => AppError;
|
|
21
|
+
invalidState: (options?: AppErrorFactoryOptions) => AppError;
|
|
22
|
+
inviteRevoked: (options?: AppErrorFactoryOptions) => AppError;
|
|
23
|
+
loginStrategiesMissing: (options?: AppErrorFactoryOptions) => AppError;
|
|
24
|
+
loginUnsupportedProvider: (provider?: string, options?: AppErrorFactoryOptions) => AppError;
|
|
25
|
+
noItems: (options?: AppErrorFactoryOptions) => AppError;
|
|
26
|
+
notFound: (options?: AppErrorFactoryOptions) => AppError;
|
|
27
|
+
notificationNotFound: (options?: AppErrorFactoryOptions) => AppError;
|
|
28
|
+
parameterExists: (options?: AppErrorFactoryOptions) => AppError;
|
|
29
|
+
parameterNotFound: (field?: string, options?: AppErrorFactoryOptions) => AppError;
|
|
30
|
+
productExists: (options?: AppErrorFactoryOptions) => AppError;
|
|
31
|
+
productNotFound: (options?: AppErrorFactoryOptions) => AppError;
|
|
32
|
+
serviceExists: (options?: AppErrorFactoryOptions) => AppError;
|
|
33
|
+
serviceNotFound: (options?: AppErrorFactoryOptions) => AppError;
|
|
34
|
+
sunatApiError: (options?: AppErrorFactoryOptions) => AppError;
|
|
35
|
+
sunatAuthError: (options?: AppErrorFactoryOptions) => AppError;
|
|
36
|
+
sunatAuthFailed: (message?: string, options?: AppErrorFactoryOptions) => AppError;
|
|
37
|
+
sunatAuthInvalidResponse: (options?: AppErrorFactoryOptions) => AppError;
|
|
38
|
+
sunatAuthTimeout: (options?: AppErrorFactoryOptions) => AppError;
|
|
39
|
+
sunatSettingsNotConfigured: (options?: AppErrorFactoryOptions) => AppError;
|
|
40
|
+
sunatSettingsNotFound: (options?: AppErrorFactoryOptions) => AppError;
|
|
41
|
+
sunatTimeout: (options?: AppErrorFactoryOptions) => AppError;
|
|
42
|
+
systemNavItemNotFound: (itemId?: string, options?: AppErrorFactoryOptions) => AppError;
|
|
43
|
+
tenantContextConflict: (options?: AppErrorFactoryOptions) => AppError;
|
|
44
|
+
tenantContextRequired: (options?: AppErrorFactoryOptions) => AppError;
|
|
45
|
+
tenantContextUnavailable: (options?: AppErrorFactoryOptions) => AppError;
|
|
46
|
+
tenantForbidden: (options?: AppErrorFactoryOptions) => AppError;
|
|
47
|
+
tenantMemberAlreadyExists: (options?: AppErrorFactoryOptions) => AppError;
|
|
48
|
+
tenantMemberNotFound: (options?: AppErrorFactoryOptions) => AppError;
|
|
49
|
+
tenantMismatch: (options?: AppErrorFactoryOptions) => AppError;
|
|
50
|
+
tenantNotFound: (options?: AppErrorFactoryOptions) => AppError;
|
|
51
|
+
tenantRoleImmutable: (options?: AppErrorFactoryOptions) => AppError;
|
|
52
|
+
tenantRoleNotFound: (options?: AppErrorFactoryOptions) => AppError;
|
|
53
|
+
tenantSeatsExceeded: (options?: AppErrorFactoryOptions) => AppError;
|
|
54
|
+
tenantSlugInUse: (options?: AppErrorFactoryOptions) => AppError;
|
|
55
|
+
ubigeoExists: (options?: AppErrorFactoryOptions) => AppError;
|
|
56
|
+
ubigeoNotFound: (options?: AppErrorFactoryOptions) => AppError;
|
|
57
|
+
unauthorized: (options?: AppErrorFactoryOptions) => AppError;
|
|
58
|
+
userExists: (options?: AppErrorFactoryOptions) => AppError;
|
|
59
|
+
userInactive: (options?: AppErrorFactoryOptions) => AppError;
|
|
60
|
+
userNotFound: (options?: AppErrorFactoryOptions) => AppError;
|
|
61
|
+
validation: (field?: string, options?: AppErrorFactoryOptions) => AppError;
|
|
62
|
+
internal: (options?: AppErrorFactoryOptions) => AppError;
|
|
63
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { AppError } from "./app-error";
|
|
2
|
+
import { ErrorCode } from "./error-codes";
|
|
3
|
+
const DEFAULT_ERRORS = {};
|
|
4
|
+
function buildError(code, defaultMessage, defaultStatus, options) {
|
|
5
|
+
return new AppError(code, options?.message ?? defaultMessage, options?.status ?? defaultStatus, options?.errors ?? DEFAULT_ERRORS);
|
|
6
|
+
}
|
|
7
|
+
export const AppErrorFactory = {
|
|
8
|
+
authRequired: (options) => buildError(ErrorCode.AUTH_REQUIRED, "El idToken de Google es requerido", 401, options),
|
|
9
|
+
badRequest: (options) => buildError(ErrorCode.BAD_REQUEST, "Faltan parámetros", 400, options),
|
|
10
|
+
channelExists: (options) => buildError(ErrorCode.CHANNEL_EXISTS, "El canal ya existe", 409, options),
|
|
11
|
+
channelNotFound: (options) => buildError(ErrorCode.CHANNEL_NOT_FOUND, "Canal no encontrado", 404, options),
|
|
12
|
+
config: (field, options) => buildError(ErrorCode.CONFIG, field ? `Falta ${field} para la integración SUNAT` : "Falta un campo para la integración SUNAT", 500, options),
|
|
13
|
+
conflict: (options) => buildError(ErrorCode.CONFLICT, "El teléfono ya existe", 409, options),
|
|
14
|
+
courierDocumentTaken: (options) => buildError(ErrorCode.COURIER_DOCUMENT_TAKEN, "El documento ya existe", 409, options),
|
|
15
|
+
courierNotFound: (options) => buildError(ErrorCode.COURIER_NOT_FOUND, "Courier not found", 404, options),
|
|
16
|
+
courierPhoneTaken: (options) => buildError(ErrorCode.COURIER_PHONE_TAKEN, "El teléfono ya existe", 409, options),
|
|
17
|
+
customerNotFound: (options) => buildError(ErrorCode.CUSTOMER_NOT_FOUND, "Cliente no encontrado", 404, options),
|
|
18
|
+
forbidden: (options) => buildError(ErrorCode.FORBIDDEN, "Actor no autorizado", 403, options),
|
|
19
|
+
invalidJson: (options) => buildError(ErrorCode.INVALID_JSON, "Body debe ser JSON", 400, options),
|
|
20
|
+
invalidReport: (options) => buildError(ErrorCode.INVALID_REPORT, "El archivo ZIP está vacío", 409, options),
|
|
21
|
+
invalidState: (options) => buildError(ErrorCode.INVALID_STATE, "No se pudo resolver tenantId/recipientUid desde Firestore", 500, options),
|
|
22
|
+
inviteRevoked: (options) => buildError(ErrorCode.INVITE_REVOKED, "La invitación fue revocada", 409, options),
|
|
23
|
+
loginStrategiesMissing: (options) => buildError(ErrorCode.LOGIN_STRATEGIES_MISSING, "No hay estrategias de autenticación configuradas", 500, options),
|
|
24
|
+
loginUnsupportedProvider: (provider, options) => buildError(ErrorCode.LOGIN_UNSUPPORTED_PROVIDER, provider
|
|
25
|
+
? `Proveedor de login no soportado: ${provider}`
|
|
26
|
+
: "Proveedor de login no soportado", 400, options),
|
|
27
|
+
noItems: (options) => buildError(ErrorCode.NO_ITEMS, "La orden debe tener al menos un item", 400, options),
|
|
28
|
+
notFound: (options) => buildError(ErrorCode.NOT_FOUND, "User not found", 404, options),
|
|
29
|
+
notificationNotFound: (options) => buildError(ErrorCode.NOTIFICATION_NOT_FOUND, "Notificación no encontrada", 404, options),
|
|
30
|
+
parameterExists: (options) => buildError(ErrorCode.PARAMETER_EXISTS, "El código de parámetro ya existe", 409, options),
|
|
31
|
+
parameterNotFound: (field, options) => buildError(ErrorCode.PARAMETER_NOT_FOUND, field ? `El parámetro ${field} es inválido` : "El parámetro es inválido", 400, options),
|
|
32
|
+
productExists: (options) => buildError(ErrorCode.PRODUCT_EXISTS, "El SKU ya existe", 409, options),
|
|
33
|
+
productNotFound: (options) => buildError(ErrorCode.PRODUCT_NOT_FOUND, "Producto no encontrado", 404, options),
|
|
34
|
+
serviceExists: (options) => buildError(ErrorCode.SERVICE_EXISTS, "El servicio ya existe", 409, options),
|
|
35
|
+
serviceNotFound: (options) => buildError(ErrorCode.SERVICE_NOT_FOUND, "Servicio no encontrado", 404, options),
|
|
36
|
+
sunatApiError: (options) => buildError(ErrorCode.SUNAT_API_ERROR, "Error inesperado en API de SIRE Compras", 502, options),
|
|
37
|
+
sunatAuthError: (options) => buildError(ErrorCode.SUNAT_AUTH_ERROR, "Error inesperado al obtener token SUNAT", 502, options),
|
|
38
|
+
sunatAuthFailed: (message, options) => buildError(ErrorCode.SUNAT_AUTH_FAILED, message ?? "Error de autenticación SUNAT", 500, options),
|
|
39
|
+
sunatAuthInvalidResponse: (options) => buildError(ErrorCode.SUNAT_AUTH_INVALID_RESPONSE, "SUNAT retornó un formato de token inválido", 502, options),
|
|
40
|
+
sunatAuthTimeout: (options) => buildError(ErrorCode.SUNAT_AUTH_TIMEOUT, "Tiempo de espera agotado al obtener token SUNAT", 504, options),
|
|
41
|
+
sunatSettingsNotConfigured: (options) => buildError(ErrorCode.SUNAT_SETTINGS_NOT_CONFIGURED, "Configura las credenciales SUNAT", 404, options),
|
|
42
|
+
sunatSettingsNotFound: (options) => buildError(ErrorCode.SUNAT_SETTINGS_NOT_FOUND, "Configura las credenciales SUNAT", 404, options),
|
|
43
|
+
sunatTimeout: (options) => buildError(ErrorCode.SUNAT_TIMEOUT, "Tiempo de espera agotado en API de SIRE Compras", 504, options),
|
|
44
|
+
systemNavItemNotFound: (itemId, options) => buildError(ErrorCode.SYSTEM_NAV_ITEM_NOT_FOUND, itemId ? `Item de navegación no encontrado: ${itemId}` : "Item de navegación no encontrado", 404, options),
|
|
45
|
+
tenantContextConflict: (options) => buildError(ErrorCode.TENANT_CONTEXT_CONFLICT, "El contexto del tenant no es válido", 400, options),
|
|
46
|
+
tenantContextRequired: (options) => buildError(ErrorCode.TENANT_CONTEXT_REQUIRED, "Falta el contexto de empresa", 403, options),
|
|
47
|
+
tenantContextUnavailable: (options) => buildError(ErrorCode.TENANT_CONTEXT_UNAVAILABLE, "No se pudo resolver el contexto del tenant", 500, options),
|
|
48
|
+
tenantForbidden: (options) => buildError(ErrorCode.TENANT_FORBIDDEN, "No tienes acceso a esta empresa", 403, options),
|
|
49
|
+
tenantMemberAlreadyExists: (options) => buildError(ErrorCode.TENANT_MEMBER_ALREADY_EXISTS, "El usuario ya es miembro de la empresa", 409, options),
|
|
50
|
+
tenantMemberNotFound: (options) => buildError(ErrorCode.TENANT_MEMBER_NOT_FOUND, "Miembro no encontrado", 404, options),
|
|
51
|
+
tenantMismatch: (options) => buildError(ErrorCode.TENANT_MISMATCH, "El archivo descargado no corresponde al RUC del tenant", 409, options),
|
|
52
|
+
tenantNotFound: (options) => buildError(ErrorCode.TENANT_NOT_FOUND, "Empresa no encontrada", 404, options),
|
|
53
|
+
tenantRoleImmutable: (options) => buildError(ErrorCode.TENANT_ROLE_IMMUTABLE, "Los roles del sistema no pueden desactivarse", 409, options),
|
|
54
|
+
tenantRoleNotFound: (options) => buildError(ErrorCode.TENANT_ROLE_NOT_FOUND, "El rol indicado no existe", 404, options),
|
|
55
|
+
tenantSeatsExceeded: (options) => buildError(ErrorCode.TENANT_SEATS_EXCEEDED, "Capacidad de asientos excedida", 409, options),
|
|
56
|
+
tenantSlugInUse: (options) => buildError(ErrorCode.TENANT_SLUG_IN_USE, "El slug ya está en uso", 409, options),
|
|
57
|
+
ubigeoExists: (options) => buildError(ErrorCode.UBIGEO_EXISTS, "El código ubigeo ya está registrado", 409, options),
|
|
58
|
+
ubigeoNotFound: (options) => buildError(ErrorCode.UBIGEO_NOT_FOUND, "Ubigeo no encontrado", 404, options),
|
|
59
|
+
unauthorized: (options) => buildError(ErrorCode.UNAUTHORIZED, "Usuario no autorizado", 401, options),
|
|
60
|
+
userExists: (options) => buildError(ErrorCode.USER_EXISTS, "El correo ya está registrado", 409, options),
|
|
61
|
+
userInactive: (options) => buildError(ErrorCode.USER_INACTIVE, "Usuario inactivo", 403, options),
|
|
62
|
+
userNotFound: (options) => buildError(ErrorCode.USER_NOT_FOUND, "Usuario no encontrado", 404, options),
|
|
63
|
+
validation: (field, options) => buildError(ErrorCode.VALIDATION, field ? `${field} inválido` : "metadata inválido", 400, options),
|
|
64
|
+
internal: (options) => buildError(ErrorCode.INTERNAL, "internal error", 500, options),
|
|
65
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export declare const ErrorCode: {
|
|
2
|
+
readonly AUTH_REQUIRED: "AUTH_REQUIRED";
|
|
3
|
+
readonly BAD_REQUEST: "BAD_REQUEST";
|
|
4
|
+
readonly CHANNEL_EXISTS: "CHANNEL_EXISTS";
|
|
5
|
+
readonly CHANNEL_NOT_FOUND: "CHANNEL_NOT_FOUND";
|
|
6
|
+
readonly CONFIG: "CONFIG";
|
|
7
|
+
readonly CONFLICT: "CONFLICT";
|
|
8
|
+
readonly COURIER_DOCUMENT_TAKEN: "COURIER_DOCUMENT_TAKEN";
|
|
9
|
+
readonly COURIER_NOT_FOUND: "COURIER_NOT_FOUND";
|
|
10
|
+
readonly COURIER_PHONE_TAKEN: "COURIER_PHONE_TAKEN";
|
|
11
|
+
readonly CUSTOMER_NOT_FOUND: "CUSTOMER_NOT_FOUND";
|
|
12
|
+
readonly FORBIDDEN: "FORBIDDEN";
|
|
13
|
+
readonly INVALID_JSON: "INVALID_JSON";
|
|
14
|
+
readonly INVALID_REPORT: "INVALID_REPORT";
|
|
15
|
+
readonly INVALID_STATE: "INVALID_STATE";
|
|
16
|
+
readonly INVITE_REVOKED: "INVITE_REVOKED";
|
|
17
|
+
readonly LOGIN_STRATEGIES_MISSING: "LOGIN_STRATEGIES_MISSING";
|
|
18
|
+
readonly LOGIN_UNSUPPORTED_PROVIDER: "LOGIN_UNSUPPORTED_PROVIDER";
|
|
19
|
+
readonly NO_ITEMS: "NO_ITEMS";
|
|
20
|
+
readonly NOT_FOUND: "NOT_FOUND";
|
|
21
|
+
readonly NOTIFICATION_NOT_FOUND: "NOTIFICATION_NOT_FOUND";
|
|
22
|
+
readonly PARAMETER_EXISTS: "PARAMETER_EXISTS";
|
|
23
|
+
readonly PARAMETER_NOT_FOUND: "PARAMETER_NOT_FOUND";
|
|
24
|
+
readonly PRODUCT_EXISTS: "PRODUCT_EXISTS";
|
|
25
|
+
readonly PRODUCT_NOT_FOUND: "PRODUCT_NOT_FOUND";
|
|
26
|
+
readonly SERVICE_EXISTS: "SERVICE_EXISTS";
|
|
27
|
+
readonly SERVICE_NOT_FOUND: "SERVICE_NOT_FOUND";
|
|
28
|
+
readonly SUNAT_API_ERROR: "SUNAT_API_ERROR";
|
|
29
|
+
readonly SUNAT_AUTH_ERROR: "SUNAT_AUTH_ERROR";
|
|
30
|
+
readonly SUNAT_AUTH_FAILED: "SUNAT_AUTH_FAILED";
|
|
31
|
+
readonly SUNAT_AUTH_INVALID_RESPONSE: "SUNAT_AUTH_INVALID_RESPONSE";
|
|
32
|
+
readonly SUNAT_AUTH_TIMEOUT: "SUNAT_AUTH_TIMEOUT";
|
|
33
|
+
readonly SUNAT_SETTINGS_NOT_CONFIGURED: "SUNAT_SETTINGS_NOT_CONFIGURED";
|
|
34
|
+
readonly SUNAT_SETTINGS_NOT_FOUND: "SUNAT_SETTINGS_NOT_FOUND";
|
|
35
|
+
readonly SUNAT_TIMEOUT: "SUNAT_TIMEOUT";
|
|
36
|
+
readonly SYSTEM_NAV_ITEM_NOT_FOUND: "SYSTEM_NAV_ITEM_NOT_FOUND";
|
|
37
|
+
readonly TENANT_CONTEXT_CONFLICT: "TENANT_CONTEXT_CONFLICT";
|
|
38
|
+
readonly TENANT_CONTEXT_REQUIRED: "TENANT_CONTEXT_REQUIRED";
|
|
39
|
+
readonly TENANT_CONTEXT_UNAVAILABLE: "TENANT_CONTEXT_UNAVAILABLE";
|
|
40
|
+
readonly TENANT_FORBIDDEN: "TENANT_FORBIDDEN";
|
|
41
|
+
readonly TENANT_MEMBER_ALREADY_EXISTS: "TENANT_MEMBER_ALREADY_EXISTS";
|
|
42
|
+
readonly TENANT_MEMBER_NOT_FOUND: "TENANT_MEMBER_NOT_FOUND";
|
|
43
|
+
readonly TENANT_MISMATCH: "TENANT_MISMATCH";
|
|
44
|
+
readonly TENANT_NOT_FOUND: "TENANT_NOT_FOUND";
|
|
45
|
+
readonly TENANT_ROLE_IMMUTABLE: "TENANT_ROLE_IMMUTABLE";
|
|
46
|
+
readonly TENANT_ROLE_NOT_FOUND: "TENANT_ROLE_NOT_FOUND";
|
|
47
|
+
readonly TENANT_SEATS_EXCEEDED: "TENANT_SEATS_EXCEEDED";
|
|
48
|
+
readonly TENANT_SLUG_IN_USE: "TENANT_SLUG_IN_USE";
|
|
49
|
+
readonly UBIGEO_EXISTS: "UBIGEO_EXISTS";
|
|
50
|
+
readonly UBIGEO_NOT_FOUND: "UBIGEO_NOT_FOUND";
|
|
51
|
+
readonly UNAUTHORIZED: "UNAUTHORIZED";
|
|
52
|
+
readonly USER_EXISTS: "USER_EXISTS";
|
|
53
|
+
readonly USER_INACTIVE: "USER_INACTIVE";
|
|
54
|
+
readonly USER_NOT_FOUND: "USER_NOT_FOUND";
|
|
55
|
+
readonly VALIDATION: "VALIDATION";
|
|
56
|
+
readonly INTERNAL: "INTERNAL";
|
|
57
|
+
};
|
|
58
|
+
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export const ErrorCode = {
|
|
2
|
+
AUTH_REQUIRED: "AUTH_REQUIRED",
|
|
3
|
+
BAD_REQUEST: "BAD_REQUEST",
|
|
4
|
+
CHANNEL_EXISTS: "CHANNEL_EXISTS",
|
|
5
|
+
CHANNEL_NOT_FOUND: "CHANNEL_NOT_FOUND",
|
|
6
|
+
CONFIG: "CONFIG",
|
|
7
|
+
CONFLICT: "CONFLICT",
|
|
8
|
+
COURIER_DOCUMENT_TAKEN: "COURIER_DOCUMENT_TAKEN",
|
|
9
|
+
COURIER_NOT_FOUND: "COURIER_NOT_FOUND",
|
|
10
|
+
COURIER_PHONE_TAKEN: "COURIER_PHONE_TAKEN",
|
|
11
|
+
CUSTOMER_NOT_FOUND: "CUSTOMER_NOT_FOUND",
|
|
12
|
+
FORBIDDEN: "FORBIDDEN",
|
|
13
|
+
INVALID_JSON: "INVALID_JSON",
|
|
14
|
+
INVALID_REPORT: "INVALID_REPORT",
|
|
15
|
+
INVALID_STATE: "INVALID_STATE",
|
|
16
|
+
INVITE_REVOKED: "INVITE_REVOKED",
|
|
17
|
+
LOGIN_STRATEGIES_MISSING: "LOGIN_STRATEGIES_MISSING",
|
|
18
|
+
LOGIN_UNSUPPORTED_PROVIDER: "LOGIN_UNSUPPORTED_PROVIDER",
|
|
19
|
+
NO_ITEMS: "NO_ITEMS",
|
|
20
|
+
NOT_FOUND: "NOT_FOUND",
|
|
21
|
+
NOTIFICATION_NOT_FOUND: "NOTIFICATION_NOT_FOUND",
|
|
22
|
+
PARAMETER_EXISTS: "PARAMETER_EXISTS",
|
|
23
|
+
PARAMETER_NOT_FOUND: "PARAMETER_NOT_FOUND",
|
|
24
|
+
PRODUCT_EXISTS: "PRODUCT_EXISTS",
|
|
25
|
+
PRODUCT_NOT_FOUND: "PRODUCT_NOT_FOUND",
|
|
26
|
+
SERVICE_EXISTS: "SERVICE_EXISTS",
|
|
27
|
+
SERVICE_NOT_FOUND: "SERVICE_NOT_FOUND",
|
|
28
|
+
SUNAT_API_ERROR: "SUNAT_API_ERROR",
|
|
29
|
+
SUNAT_AUTH_ERROR: "SUNAT_AUTH_ERROR",
|
|
30
|
+
SUNAT_AUTH_FAILED: "SUNAT_AUTH_FAILED",
|
|
31
|
+
SUNAT_AUTH_INVALID_RESPONSE: "SUNAT_AUTH_INVALID_RESPONSE",
|
|
32
|
+
SUNAT_AUTH_TIMEOUT: "SUNAT_AUTH_TIMEOUT",
|
|
33
|
+
SUNAT_SETTINGS_NOT_CONFIGURED: "SUNAT_SETTINGS_NOT_CONFIGURED",
|
|
34
|
+
SUNAT_SETTINGS_NOT_FOUND: "SUNAT_SETTINGS_NOT_FOUND",
|
|
35
|
+
SUNAT_TIMEOUT: "SUNAT_TIMEOUT",
|
|
36
|
+
SYSTEM_NAV_ITEM_NOT_FOUND: "SYSTEM_NAV_ITEM_NOT_FOUND",
|
|
37
|
+
TENANT_CONTEXT_CONFLICT: "TENANT_CONTEXT_CONFLICT",
|
|
38
|
+
TENANT_CONTEXT_REQUIRED: "TENANT_CONTEXT_REQUIRED",
|
|
39
|
+
TENANT_CONTEXT_UNAVAILABLE: "TENANT_CONTEXT_UNAVAILABLE",
|
|
40
|
+
TENANT_FORBIDDEN: "TENANT_FORBIDDEN",
|
|
41
|
+
TENANT_MEMBER_ALREADY_EXISTS: "TENANT_MEMBER_ALREADY_EXISTS",
|
|
42
|
+
TENANT_MEMBER_NOT_FOUND: "TENANT_MEMBER_NOT_FOUND",
|
|
43
|
+
TENANT_MISMATCH: "TENANT_MISMATCH",
|
|
44
|
+
TENANT_NOT_FOUND: "TENANT_NOT_FOUND",
|
|
45
|
+
TENANT_ROLE_IMMUTABLE: "TENANT_ROLE_IMMUTABLE",
|
|
46
|
+
TENANT_ROLE_NOT_FOUND: "TENANT_ROLE_NOT_FOUND",
|
|
47
|
+
TENANT_SEATS_EXCEEDED: "TENANT_SEATS_EXCEEDED",
|
|
48
|
+
TENANT_SLUG_IN_USE: "TENANT_SLUG_IN_USE",
|
|
49
|
+
UBIGEO_EXISTS: "UBIGEO_EXISTS",
|
|
50
|
+
UBIGEO_NOT_FOUND: "UBIGEO_NOT_FOUND",
|
|
51
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
52
|
+
USER_EXISTS: "USER_EXISTS",
|
|
53
|
+
USER_INACTIVE: "USER_INACTIVE",
|
|
54
|
+
USER_NOT_FOUND: "USER_NOT_FOUND",
|
|
55
|
+
VALIDATION: "VALIDATION",
|
|
56
|
+
INTERNAL: "INTERNAL",
|
|
57
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AppError } from "../errors/app-error";
|
|
2
|
+
import { ErrorCode } from "../errors/error-codes";
|
|
3
|
+
export function toApiError(err, traceId) {
|
|
4
|
+
if (err instanceof AppError) {
|
|
5
|
+
return {
|
|
6
|
+
status: err.status,
|
|
7
|
+
body: {
|
|
8
|
+
code: err.code,
|
|
9
|
+
message: err.message,
|
|
10
|
+
errors: err.errors,
|
|
11
|
+
traceId,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
status: 500,
|
|
17
|
+
body: { code: ErrorCode.INTERNAL, message: "internal error", traceId },
|
|
18
|
+
};
|
|
19
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type SearchIndex<T = unknown> = Record<string, T>;
|
|
2
|
+
export declare function createEmptySearchIndex<T = unknown>(): SearchIndex<T>;
|
|
3
|
+
export declare function normalizeSearchText(value: string): string;
|
|
4
|
+
export declare function buildSearchPrefixes(tokens: string[], maxLength?: number): string[];
|
|
5
|
+
export declare function toSearchPrefix(query: string): string;
|
|
6
|
+
export declare function normalizePhoneDigits(value: string): string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const DEFAULT_MAX_PREFIX_LENGTH = 30;
|
|
2
|
+
export function createEmptySearchIndex() {
|
|
3
|
+
return {};
|
|
4
|
+
}
|
|
5
|
+
export function normalizeSearchText(value) {
|
|
6
|
+
const normalized = (value ?? "")
|
|
7
|
+
.normalize("NFD")
|
|
8
|
+
.replaceAll(/\p{Diacritic}/gu, "")
|
|
9
|
+
.replaceAll(/["'`,.:;!?(){}]/g, " ")
|
|
10
|
+
.replaceAll(/[-_]/g, " ")
|
|
11
|
+
.toLowerCase();
|
|
12
|
+
return normalized.replaceAll(/\s+/g, " ").trim();
|
|
13
|
+
}
|
|
14
|
+
export function buildSearchPrefixes(tokens, maxLength = DEFAULT_MAX_PREFIX_LENGTH) {
|
|
15
|
+
const prefixSet = new Set();
|
|
16
|
+
for (const token of tokens) {
|
|
17
|
+
const safeToken = token.slice(0, maxLength);
|
|
18
|
+
if (!safeToken) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
addPrefixes(prefixSet, safeToken);
|
|
22
|
+
const segments = safeToken.split(/\s+/g).filter(Boolean);
|
|
23
|
+
for (const segment of segments) {
|
|
24
|
+
addPrefixes(prefixSet, segment.slice(0, maxLength));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return Array.from(prefixSet);
|
|
28
|
+
}
|
|
29
|
+
function addPrefixes(prefixSet, value) {
|
|
30
|
+
for (let index = 1; index <= value.length; index += 1) {
|
|
31
|
+
prefixSet.add(value.slice(0, index));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function toSearchPrefix(query) {
|
|
35
|
+
return normalizeSearchText(query).slice(0, DEFAULT_MAX_PREFIX_LENGTH);
|
|
36
|
+
}
|
|
37
|
+
export function normalizePhoneDigits(value) {
|
|
38
|
+
return (value ?? "").replaceAll(/\D+/g, "");
|
|
39
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { AppError } from "./app-error";
|
|
2
|
+
import { ErrorCode } from "./error-codes";
|
|
3
|
+
|
|
4
|
+
export type AppErrorFactoryOptions = {
|
|
5
|
+
message?: string;
|
|
6
|
+
status?: number;
|
|
7
|
+
errors?: Record<string, string>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const DEFAULT_ERRORS: Record<string, string> = {};
|
|
11
|
+
|
|
12
|
+
function buildError(
|
|
13
|
+
code: ErrorCode,
|
|
14
|
+
defaultMessage: string,
|
|
15
|
+
defaultStatus: number,
|
|
16
|
+
options?: AppErrorFactoryOptions,
|
|
17
|
+
): AppError {
|
|
18
|
+
return new AppError(
|
|
19
|
+
code,
|
|
20
|
+
options?.message ?? defaultMessage,
|
|
21
|
+
options?.status ?? defaultStatus,
|
|
22
|
+
options?.errors ?? DEFAULT_ERRORS,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const AppErrorFactory = {
|
|
27
|
+
authRequired: (options?: AppErrorFactoryOptions) =>
|
|
28
|
+
buildError(ErrorCode.AUTH_REQUIRED, "El idToken de Google es requerido", 401, options),
|
|
29
|
+
badRequest: (options?: AppErrorFactoryOptions) =>
|
|
30
|
+
buildError(ErrorCode.BAD_REQUEST, "Faltan parámetros", 400, options),
|
|
31
|
+
channelExists: (options?: AppErrorFactoryOptions) =>
|
|
32
|
+
buildError(ErrorCode.CHANNEL_EXISTS, "El canal ya existe", 409, options),
|
|
33
|
+
channelNotFound: (options?: AppErrorFactoryOptions) =>
|
|
34
|
+
buildError(ErrorCode.CHANNEL_NOT_FOUND, "Canal no encontrado", 404, options),
|
|
35
|
+
config: (field?: string, options?: AppErrorFactoryOptions) =>
|
|
36
|
+
buildError(
|
|
37
|
+
ErrorCode.CONFIG,
|
|
38
|
+
field ? `Falta ${field} para la integración SUNAT` : "Falta un campo para la integración SUNAT",
|
|
39
|
+
500,
|
|
40
|
+
options,
|
|
41
|
+
),
|
|
42
|
+
conflict: (options?: AppErrorFactoryOptions) =>
|
|
43
|
+
buildError(ErrorCode.CONFLICT, "El teléfono ya existe", 409, options),
|
|
44
|
+
courierDocumentTaken: (options?: AppErrorFactoryOptions) =>
|
|
45
|
+
buildError(ErrorCode.COURIER_DOCUMENT_TAKEN, "El documento ya existe", 409, options),
|
|
46
|
+
courierNotFound: (options?: AppErrorFactoryOptions) =>
|
|
47
|
+
buildError(ErrorCode.COURIER_NOT_FOUND, "Courier not found", 404, options),
|
|
48
|
+
courierPhoneTaken: (options?: AppErrorFactoryOptions) =>
|
|
49
|
+
buildError(ErrorCode.COURIER_PHONE_TAKEN, "El teléfono ya existe", 409, options),
|
|
50
|
+
customerNotFound: (options?: AppErrorFactoryOptions) =>
|
|
51
|
+
buildError(ErrorCode.CUSTOMER_NOT_FOUND, "Cliente no encontrado", 404, options),
|
|
52
|
+
forbidden: (options?: AppErrorFactoryOptions) =>
|
|
53
|
+
buildError(ErrorCode.FORBIDDEN, "Actor no autorizado", 403, options),
|
|
54
|
+
invalidJson: (options?: AppErrorFactoryOptions) =>
|
|
55
|
+
buildError(ErrorCode.INVALID_JSON, "Body debe ser JSON", 400, options),
|
|
56
|
+
invalidReport: (options?: AppErrorFactoryOptions) =>
|
|
57
|
+
buildError(ErrorCode.INVALID_REPORT, "El archivo ZIP está vacío", 409, options),
|
|
58
|
+
invalidState: (options?: AppErrorFactoryOptions) =>
|
|
59
|
+
buildError(
|
|
60
|
+
ErrorCode.INVALID_STATE,
|
|
61
|
+
"No se pudo resolver tenantId/recipientUid desde Firestore",
|
|
62
|
+
500,
|
|
63
|
+
options,
|
|
64
|
+
),
|
|
65
|
+
inviteRevoked: (options?: AppErrorFactoryOptions) =>
|
|
66
|
+
buildError(ErrorCode.INVITE_REVOKED, "La invitación fue revocada", 409, options),
|
|
67
|
+
loginStrategiesMissing: (options?: AppErrorFactoryOptions) =>
|
|
68
|
+
buildError(ErrorCode.LOGIN_STRATEGIES_MISSING, "No hay estrategias de autenticación configuradas", 500, options),
|
|
69
|
+
loginUnsupportedProvider: (provider?: string, options?: AppErrorFactoryOptions) =>
|
|
70
|
+
buildError(
|
|
71
|
+
ErrorCode.LOGIN_UNSUPPORTED_PROVIDER,
|
|
72
|
+
provider
|
|
73
|
+
? `Proveedor de login no soportado: ${provider}`
|
|
74
|
+
: "Proveedor de login no soportado",
|
|
75
|
+
400,
|
|
76
|
+
options,
|
|
77
|
+
),
|
|
78
|
+
noItems: (options?: AppErrorFactoryOptions) =>
|
|
79
|
+
buildError(ErrorCode.NO_ITEMS, "La orden debe tener al menos un item", 400, options),
|
|
80
|
+
notFound: (options?: AppErrorFactoryOptions) =>
|
|
81
|
+
buildError(ErrorCode.NOT_FOUND, "User not found", 404, options),
|
|
82
|
+
notificationNotFound: (options?: AppErrorFactoryOptions) =>
|
|
83
|
+
buildError(ErrorCode.NOTIFICATION_NOT_FOUND, "Notificación no encontrada", 404, options),
|
|
84
|
+
parameterExists: (options?: AppErrorFactoryOptions) =>
|
|
85
|
+
buildError(ErrorCode.PARAMETER_EXISTS, "El código de parámetro ya existe", 409, options),
|
|
86
|
+
parameterNotFound: (field?: string, options?: AppErrorFactoryOptions) =>
|
|
87
|
+
buildError(
|
|
88
|
+
ErrorCode.PARAMETER_NOT_FOUND,
|
|
89
|
+
field ? `El parámetro ${field} es inválido` : "El parámetro es inválido",
|
|
90
|
+
400,
|
|
91
|
+
options,
|
|
92
|
+
),
|
|
93
|
+
productExists: (options?: AppErrorFactoryOptions) =>
|
|
94
|
+
buildError(ErrorCode.PRODUCT_EXISTS, "El SKU ya existe", 409, options),
|
|
95
|
+
productNotFound: (options?: AppErrorFactoryOptions) =>
|
|
96
|
+
buildError(ErrorCode.PRODUCT_NOT_FOUND, "Producto no encontrado", 404, options),
|
|
97
|
+
serviceExists: (options?: AppErrorFactoryOptions) =>
|
|
98
|
+
buildError(ErrorCode.SERVICE_EXISTS, "El servicio ya existe", 409, options),
|
|
99
|
+
serviceNotFound: (options?: AppErrorFactoryOptions) =>
|
|
100
|
+
buildError(ErrorCode.SERVICE_NOT_FOUND, "Servicio no encontrado", 404, options),
|
|
101
|
+
sunatApiError: (options?: AppErrorFactoryOptions) =>
|
|
102
|
+
buildError(ErrorCode.SUNAT_API_ERROR, "Error inesperado en API de SIRE Compras", 502, options),
|
|
103
|
+
sunatAuthError: (options?: AppErrorFactoryOptions) =>
|
|
104
|
+
buildError(ErrorCode.SUNAT_AUTH_ERROR, "Error inesperado al obtener token SUNAT", 502, options),
|
|
105
|
+
sunatAuthFailed: (message?: string, options?: AppErrorFactoryOptions) =>
|
|
106
|
+
buildError(ErrorCode.SUNAT_AUTH_FAILED, message ?? "Error de autenticación SUNAT", 500, options),
|
|
107
|
+
sunatAuthInvalidResponse: (options?: AppErrorFactoryOptions) =>
|
|
108
|
+
buildError(
|
|
109
|
+
ErrorCode.SUNAT_AUTH_INVALID_RESPONSE,
|
|
110
|
+
"SUNAT retornó un formato de token inválido",
|
|
111
|
+
502,
|
|
112
|
+
options,
|
|
113
|
+
),
|
|
114
|
+
sunatAuthTimeout: (options?: AppErrorFactoryOptions) =>
|
|
115
|
+
buildError(ErrorCode.SUNAT_AUTH_TIMEOUT, "Tiempo de espera agotado al obtener token SUNAT", 504, options),
|
|
116
|
+
sunatSettingsNotConfigured: (options?: AppErrorFactoryOptions) =>
|
|
117
|
+
buildError(ErrorCode.SUNAT_SETTINGS_NOT_CONFIGURED, "Configura las credenciales SUNAT", 404, options),
|
|
118
|
+
sunatSettingsNotFound: (options?: AppErrorFactoryOptions) =>
|
|
119
|
+
buildError(ErrorCode.SUNAT_SETTINGS_NOT_FOUND, "Configura las credenciales SUNAT", 404, options),
|
|
120
|
+
sunatTimeout: (options?: AppErrorFactoryOptions) =>
|
|
121
|
+
buildError(ErrorCode.SUNAT_TIMEOUT, "Tiempo de espera agotado en API de SIRE Compras", 504, options),
|
|
122
|
+
systemNavItemNotFound: (itemId?: string, options?: AppErrorFactoryOptions) =>
|
|
123
|
+
buildError(
|
|
124
|
+
ErrorCode.SYSTEM_NAV_ITEM_NOT_FOUND,
|
|
125
|
+
itemId ? `Item de navegación no encontrado: ${itemId}` : "Item de navegación no encontrado",
|
|
126
|
+
404,
|
|
127
|
+
options,
|
|
128
|
+
),
|
|
129
|
+
tenantContextConflict: (options?: AppErrorFactoryOptions) =>
|
|
130
|
+
buildError(ErrorCode.TENANT_CONTEXT_CONFLICT, "El contexto del tenant no es válido", 400, options),
|
|
131
|
+
tenantContextRequired: (options?: AppErrorFactoryOptions) =>
|
|
132
|
+
buildError(ErrorCode.TENANT_CONTEXT_REQUIRED, "Falta el contexto de empresa", 403, options),
|
|
133
|
+
tenantContextUnavailable: (options?: AppErrorFactoryOptions) =>
|
|
134
|
+
buildError(ErrorCode.TENANT_CONTEXT_UNAVAILABLE, "No se pudo resolver el contexto del tenant", 500, options),
|
|
135
|
+
tenantForbidden: (options?: AppErrorFactoryOptions) =>
|
|
136
|
+
buildError(ErrorCode.TENANT_FORBIDDEN, "No tienes acceso a esta empresa", 403, options),
|
|
137
|
+
tenantMemberAlreadyExists: (options?: AppErrorFactoryOptions) =>
|
|
138
|
+
buildError(
|
|
139
|
+
ErrorCode.TENANT_MEMBER_ALREADY_EXISTS,
|
|
140
|
+
"El usuario ya es miembro de la empresa",
|
|
141
|
+
409,
|
|
142
|
+
options,
|
|
143
|
+
),
|
|
144
|
+
tenantMemberNotFound: (options?: AppErrorFactoryOptions) =>
|
|
145
|
+
buildError(ErrorCode.TENANT_MEMBER_NOT_FOUND, "Miembro no encontrado", 404, options),
|
|
146
|
+
tenantMismatch: (options?: AppErrorFactoryOptions) =>
|
|
147
|
+
buildError(
|
|
148
|
+
ErrorCode.TENANT_MISMATCH,
|
|
149
|
+
"El archivo descargado no corresponde al RUC del tenant",
|
|
150
|
+
409,
|
|
151
|
+
options,
|
|
152
|
+
),
|
|
153
|
+
tenantNotFound: (options?: AppErrorFactoryOptions) =>
|
|
154
|
+
buildError(ErrorCode.TENANT_NOT_FOUND, "Empresa no encontrada", 404, options),
|
|
155
|
+
tenantRoleImmutable: (options?: AppErrorFactoryOptions) =>
|
|
156
|
+
buildError(ErrorCode.TENANT_ROLE_IMMUTABLE, "Los roles del sistema no pueden desactivarse", 409, options),
|
|
157
|
+
tenantRoleNotFound: (options?: AppErrorFactoryOptions) =>
|
|
158
|
+
buildError(ErrorCode.TENANT_ROLE_NOT_FOUND, "El rol indicado no existe", 404, options),
|
|
159
|
+
tenantSeatsExceeded: (options?: AppErrorFactoryOptions) =>
|
|
160
|
+
buildError(ErrorCode.TENANT_SEATS_EXCEEDED, "Capacidad de asientos excedida", 409, options),
|
|
161
|
+
tenantSlugInUse: (options?: AppErrorFactoryOptions) =>
|
|
162
|
+
buildError(ErrorCode.TENANT_SLUG_IN_USE, "El slug ya está en uso", 409, options),
|
|
163
|
+
ubigeoExists: (options?: AppErrorFactoryOptions) =>
|
|
164
|
+
buildError(ErrorCode.UBIGEO_EXISTS, "El código ubigeo ya está registrado", 409, options),
|
|
165
|
+
ubigeoNotFound: (options?: AppErrorFactoryOptions) =>
|
|
166
|
+
buildError(ErrorCode.UBIGEO_NOT_FOUND, "Ubigeo no encontrado", 404, options),
|
|
167
|
+
unauthorized: (options?: AppErrorFactoryOptions) =>
|
|
168
|
+
buildError(ErrorCode.UNAUTHORIZED, "Usuario no autorizado", 401, options),
|
|
169
|
+
userExists: (options?: AppErrorFactoryOptions) =>
|
|
170
|
+
buildError(ErrorCode.USER_EXISTS, "El correo ya está registrado", 409, options),
|
|
171
|
+
userInactive: (options?: AppErrorFactoryOptions) =>
|
|
172
|
+
buildError(ErrorCode.USER_INACTIVE, "Usuario inactivo", 403, options),
|
|
173
|
+
userNotFound: (options?: AppErrorFactoryOptions) =>
|
|
174
|
+
buildError(ErrorCode.USER_NOT_FOUND, "Usuario no encontrado", 404, options),
|
|
175
|
+
validation: (field?: string, options?: AppErrorFactoryOptions) =>
|
|
176
|
+
buildError(
|
|
177
|
+
ErrorCode.VALIDATION,
|
|
178
|
+
field ? `${field} inválido` : "metadata inválido",
|
|
179
|
+
400,
|
|
180
|
+
options,
|
|
181
|
+
),
|
|
182
|
+
internal: (options?: AppErrorFactoryOptions) =>
|
|
183
|
+
buildError(ErrorCode.INTERNAL, "internal error", 500, options),
|
|
184
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const ErrorCode = {
|
|
2
|
+
AUTH_REQUIRED: "AUTH_REQUIRED",
|
|
3
|
+
BAD_REQUEST: "BAD_REQUEST",
|
|
4
|
+
CHANNEL_EXISTS: "CHANNEL_EXISTS",
|
|
5
|
+
CHANNEL_NOT_FOUND: "CHANNEL_NOT_FOUND",
|
|
6
|
+
CONFIG: "CONFIG",
|
|
7
|
+
CONFLICT: "CONFLICT",
|
|
8
|
+
COURIER_DOCUMENT_TAKEN: "COURIER_DOCUMENT_TAKEN",
|
|
9
|
+
COURIER_NOT_FOUND: "COURIER_NOT_FOUND",
|
|
10
|
+
COURIER_PHONE_TAKEN: "COURIER_PHONE_TAKEN",
|
|
11
|
+
CUSTOMER_NOT_FOUND: "CUSTOMER_NOT_FOUND",
|
|
12
|
+
FORBIDDEN: "FORBIDDEN",
|
|
13
|
+
INVALID_JSON: "INVALID_JSON",
|
|
14
|
+
INVALID_REPORT: "INVALID_REPORT",
|
|
15
|
+
INVALID_STATE: "INVALID_STATE",
|
|
16
|
+
INVITE_REVOKED: "INVITE_REVOKED",
|
|
17
|
+
LOGIN_STRATEGIES_MISSING: "LOGIN_STRATEGIES_MISSING",
|
|
18
|
+
LOGIN_UNSUPPORTED_PROVIDER: "LOGIN_UNSUPPORTED_PROVIDER",
|
|
19
|
+
NO_ITEMS: "NO_ITEMS",
|
|
20
|
+
NOT_FOUND: "NOT_FOUND",
|
|
21
|
+
NOTIFICATION_NOT_FOUND: "NOTIFICATION_NOT_FOUND",
|
|
22
|
+
PARAMETER_EXISTS: "PARAMETER_EXISTS",
|
|
23
|
+
PARAMETER_NOT_FOUND: "PARAMETER_NOT_FOUND",
|
|
24
|
+
PRODUCT_EXISTS: "PRODUCT_EXISTS",
|
|
25
|
+
PRODUCT_NOT_FOUND: "PRODUCT_NOT_FOUND",
|
|
26
|
+
SERVICE_EXISTS: "SERVICE_EXISTS",
|
|
27
|
+
SERVICE_NOT_FOUND: "SERVICE_NOT_FOUND",
|
|
28
|
+
SUNAT_API_ERROR: "SUNAT_API_ERROR",
|
|
29
|
+
SUNAT_AUTH_ERROR: "SUNAT_AUTH_ERROR",
|
|
30
|
+
SUNAT_AUTH_FAILED: "SUNAT_AUTH_FAILED",
|
|
31
|
+
SUNAT_AUTH_INVALID_RESPONSE: "SUNAT_AUTH_INVALID_RESPONSE",
|
|
32
|
+
SUNAT_AUTH_TIMEOUT: "SUNAT_AUTH_TIMEOUT",
|
|
33
|
+
SUNAT_SETTINGS_NOT_CONFIGURED: "SUNAT_SETTINGS_NOT_CONFIGURED",
|
|
34
|
+
SUNAT_SETTINGS_NOT_FOUND: "SUNAT_SETTINGS_NOT_FOUND",
|
|
35
|
+
SUNAT_TIMEOUT: "SUNAT_TIMEOUT",
|
|
36
|
+
SYSTEM_NAV_ITEM_NOT_FOUND: "SYSTEM_NAV_ITEM_NOT_FOUND",
|
|
37
|
+
TENANT_CONTEXT_CONFLICT: "TENANT_CONTEXT_CONFLICT",
|
|
38
|
+
TENANT_CONTEXT_REQUIRED: "TENANT_CONTEXT_REQUIRED",
|
|
39
|
+
TENANT_CONTEXT_UNAVAILABLE: "TENANT_CONTEXT_UNAVAILABLE",
|
|
40
|
+
TENANT_FORBIDDEN: "TENANT_FORBIDDEN",
|
|
41
|
+
TENANT_MEMBER_ALREADY_EXISTS: "TENANT_MEMBER_ALREADY_EXISTS",
|
|
42
|
+
TENANT_MEMBER_NOT_FOUND: "TENANT_MEMBER_NOT_FOUND",
|
|
43
|
+
TENANT_MISMATCH: "TENANT_MISMATCH",
|
|
44
|
+
TENANT_NOT_FOUND: "TENANT_NOT_FOUND",
|
|
45
|
+
TENANT_ROLE_IMMUTABLE: "TENANT_ROLE_IMMUTABLE",
|
|
46
|
+
TENANT_ROLE_NOT_FOUND: "TENANT_ROLE_NOT_FOUND",
|
|
47
|
+
TENANT_SEATS_EXCEEDED: "TENANT_SEATS_EXCEEDED",
|
|
48
|
+
TENANT_SLUG_IN_USE: "TENANT_SLUG_IN_USE",
|
|
49
|
+
UBIGEO_EXISTS: "UBIGEO_EXISTS",
|
|
50
|
+
UBIGEO_NOT_FOUND: "UBIGEO_NOT_FOUND",
|
|
51
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
52
|
+
USER_EXISTS: "USER_EXISTS",
|
|
53
|
+
USER_INACTIVE: "USER_INACTIVE",
|
|
54
|
+
USER_NOT_FOUND: "USER_NOT_FOUND",
|
|
55
|
+
VALIDATION: "VALIDATION",
|
|
56
|
+
INTERNAL: "INTERNAL",
|
|
57
|
+
} as const;
|
|
58
|
+
|
|
59
|
+
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { AppError } from "../errors/app-error";
|
|
2
|
+
import { ErrorCode } from "../errors/error-codes";
|
|
3
|
+
|
|
4
|
+
export type ApiErrorResponse = {
|
|
5
|
+
code: string;
|
|
6
|
+
message: string;
|
|
7
|
+
errors?: Record<string, string>;
|
|
8
|
+
traceId?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function toApiError(
|
|
12
|
+
err: unknown,
|
|
13
|
+
traceId?: string,
|
|
14
|
+
): { status: number; body: ApiErrorResponse } {
|
|
15
|
+
if (err instanceof AppError) {
|
|
16
|
+
return {
|
|
17
|
+
status: err.status,
|
|
18
|
+
body: {
|
|
19
|
+
code: err.code,
|
|
20
|
+
message: err.message,
|
|
21
|
+
errors: err.errors,
|
|
22
|
+
traceId,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
status: 500,
|
|
29
|
+
body: { code: ErrorCode.INTERNAL, message: "internal error", traceId },
|
|
30
|
+
};
|
|
31
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type SearchIndex<T = unknown> = Record<string, T>;
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_PREFIX_LENGTH = 30;
|
|
4
|
+
|
|
5
|
+
export function createEmptySearchIndex<T = unknown>(): SearchIndex<T> {
|
|
6
|
+
return {};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function normalizeSearchText(value: string): string {
|
|
10
|
+
const normalized = (value ?? "")
|
|
11
|
+
.normalize("NFD")
|
|
12
|
+
.replaceAll(/\p{Diacritic}/gu, "")
|
|
13
|
+
.replaceAll(/["'`,.:;!?(){}]/g, " ")
|
|
14
|
+
.replaceAll(/[-_]/g, " ")
|
|
15
|
+
.toLowerCase();
|
|
16
|
+
return normalized.replaceAll(/\s+/g, " ").trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildSearchPrefixes(
|
|
20
|
+
tokens: string[],
|
|
21
|
+
maxLength: number = DEFAULT_MAX_PREFIX_LENGTH,
|
|
22
|
+
): string[] {
|
|
23
|
+
const prefixSet = new Set<string>();
|
|
24
|
+
for (const token of tokens) {
|
|
25
|
+
const safeToken = token.slice(0, maxLength);
|
|
26
|
+
if (!safeToken) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
addPrefixes(prefixSet, safeToken);
|
|
31
|
+
|
|
32
|
+
const segments = safeToken.split(/\s+/g).filter(Boolean);
|
|
33
|
+
for (const segment of segments) {
|
|
34
|
+
addPrefixes(prefixSet, segment.slice(0, maxLength));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return Array.from(prefixSet);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function addPrefixes(prefixSet: Set<string>, value: string): void {
|
|
41
|
+
for (let index = 1; index <= value.length; index += 1) {
|
|
42
|
+
prefixSet.add(value.slice(0, index));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function toSearchPrefix(query: string): string {
|
|
47
|
+
return normalizeSearchText(query).slice(0, DEFAULT_MAX_PREFIX_LENGTH);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function normalizePhoneDigits(value: string): string {
|
|
51
|
+
return (value ?? "").replaceAll(/\D+/g, "");
|
|
52
|
+
}
|