@atlas-id/contracts 0.1.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/LICENSE +28 -0
- package/README.md +289 -0
- package/package.json +56 -0
- package/src/contracts-validation.test.ts +136 -0
- package/src/events.test.ts +48 -0
- package/src/events.ts +85 -0
- package/src/index.ts +6 -0
- package/src/notifications.test.ts +80 -0
- package/src/notifications.ts +298 -0
- package/src/oauth-connections.test.ts +80 -0
- package/src/oauth-connections.ts +220 -0
- package/src/schemas/index.ts +138 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/validation.test.ts +96 -0
- package/src/utils/validation.ts +136 -0
- package/src/versioning.test.ts +112 -0
- package/src/versioning.ts +156 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Esquemas de validación en runtime usando Zod.
|
|
3
|
+
* Estos esquemas permiten validar datos en tiempo de ejecución,
|
|
4
|
+
* no solo en tiempo de compilación con TypeScript.
|
|
5
|
+
*
|
|
6
|
+
* Nota: Requiere instalar zod como dependencia.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { isValidSemanticVersion } from '../versioning';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Esquema para validar versiones semánticas.
|
|
14
|
+
*/
|
|
15
|
+
export const semanticVersionSchema = z.string().refine(
|
|
16
|
+
isValidSemanticVersion,
|
|
17
|
+
{ message: 'Debe ser una versión semántica válida (MAJOR.MINOR.PATCH)' }
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Esquema para validar metadata de eventos.
|
|
22
|
+
*/
|
|
23
|
+
export const eventMetaSchema = z.object({
|
|
24
|
+
traceId: z.string().optional(),
|
|
25
|
+
spanId: z.string().optional(),
|
|
26
|
+
correlationId: z.string().optional(),
|
|
27
|
+
causationId: z.string().optional(),
|
|
28
|
+
tenantId: z.string().optional(),
|
|
29
|
+
projectId: z.string().optional(),
|
|
30
|
+
source: z.string().min(1),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Esquema genérico para validar envelopes de eventos.
|
|
35
|
+
*
|
|
36
|
+
* @template TPayload - Tipo del payload (debe ser un esquema Zod)
|
|
37
|
+
*/
|
|
38
|
+
export function createEventEnvelopeSchema<TPayload extends z.ZodTypeAny>(
|
|
39
|
+
payloadSchema: TPayload
|
|
40
|
+
) {
|
|
41
|
+
return z.object({
|
|
42
|
+
id: z.string().min(1),
|
|
43
|
+
type: z.string().min(1),
|
|
44
|
+
version: semanticVersionSchema,
|
|
45
|
+
occurredAt: z.string().datetime(),
|
|
46
|
+
payload: payloadSchema,
|
|
47
|
+
meta: eventMetaSchema,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Esquema para validar solicitudes de notificación por email.
|
|
53
|
+
*/
|
|
54
|
+
export const emailNotificationRequestSchema = z.object({
|
|
55
|
+
channel: z.literal('email'),
|
|
56
|
+
projectId: z.string().min(1),
|
|
57
|
+
tenantId: z.string().optional(),
|
|
58
|
+
locale: z.string().optional(),
|
|
59
|
+
deduplicationKey: z.string().optional(),
|
|
60
|
+
expiresAt: z.string().datetime().optional(),
|
|
61
|
+
metadata: z.record(z.string()).optional(),
|
|
62
|
+
to: z.string().email(),
|
|
63
|
+
templateId: z.string().min(1),
|
|
64
|
+
variables: z.record(z.union([z.string(), z.number(), z.boolean(), z.null()])).optional(),
|
|
65
|
+
cc: z.array(z.string().email()).optional(),
|
|
66
|
+
bcc: z.array(z.string().email()).optional(),
|
|
67
|
+
replyTo: z.string().email().optional(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Esquema para validar solicitudes de notificación por SMS.
|
|
72
|
+
*/
|
|
73
|
+
export const smsNotificationRequestSchema = z.object({
|
|
74
|
+
channel: z.literal('sms'),
|
|
75
|
+
projectId: z.string().min(1),
|
|
76
|
+
tenantId: z.string().optional(),
|
|
77
|
+
locale: z.string().optional(),
|
|
78
|
+
deduplicationKey: z.string().optional(),
|
|
79
|
+
expiresAt: z.string().datetime().optional(),
|
|
80
|
+
metadata: z.record(z.string()).optional(),
|
|
81
|
+
to: z.string().min(1),
|
|
82
|
+
templateId: z.string().min(1),
|
|
83
|
+
variables: z.record(z.union([z.string(), z.number(), z.boolean(), z.null()])).optional(),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Esquema para validar solicitudes de notificación por webhook.
|
|
88
|
+
*/
|
|
89
|
+
export const webhookNotificationRequestSchema = z.object({
|
|
90
|
+
channel: z.literal('webhook'),
|
|
91
|
+
projectId: z.string().min(1),
|
|
92
|
+
tenantId: z.string().optional(),
|
|
93
|
+
locale: z.string().optional(),
|
|
94
|
+
deduplicationKey: z.string().optional(),
|
|
95
|
+
expiresAt: z.string().datetime().optional(),
|
|
96
|
+
metadata: z.record(z.string()).optional(),
|
|
97
|
+
url: z.string().url(),
|
|
98
|
+
signatureVersion: semanticVersionSchema,
|
|
99
|
+
body: z.unknown(),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Esquema para validar cualquier solicitud de notificación.
|
|
104
|
+
*/
|
|
105
|
+
export const notificationRequestSchema = z.discriminatedUnion('channel', [
|
|
106
|
+
emailNotificationRequestSchema,
|
|
107
|
+
smsNotificationRequestSchema,
|
|
108
|
+
webhookNotificationRequestSchema,
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Esquema para validar conexiones OAuth.
|
|
113
|
+
*/
|
|
114
|
+
export const oauthConnectionSchema = z.object({
|
|
115
|
+
id: z.string().min(1),
|
|
116
|
+
providerId: z.string().min(1),
|
|
117
|
+
projectId: z.string().min(1),
|
|
118
|
+
tenantId: z.string().optional(),
|
|
119
|
+
userId: z.string().min(1),
|
|
120
|
+
scope: z.array(z.string()),
|
|
121
|
+
expiresAt: z.string().datetime().optional(),
|
|
122
|
+
createdAt: z.string().datetime(),
|
|
123
|
+
updatedAt: z.string().datetime(),
|
|
124
|
+
status: z.enum(['active', 'refreshing', 'revoked', 'expired']),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Esquema para validar conjuntos de tokens OAuth.
|
|
129
|
+
*/
|
|
130
|
+
export const tokenSetSchema = z.object({
|
|
131
|
+
accessToken: z.string().min(1),
|
|
132
|
+
refreshToken: z.string().optional(),
|
|
133
|
+
expiresIn: z.number().positive().optional(),
|
|
134
|
+
tokenType: z.string().optional(),
|
|
135
|
+
issuedAt: z.string().datetime(),
|
|
136
|
+
expiresAt: z.string().datetime().optional(),
|
|
137
|
+
idToken: z.string().optional(),
|
|
138
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './validation';
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
isValidUuid,
|
|
4
|
+
isValidEmail,
|
|
5
|
+
isValidUrl,
|
|
6
|
+
isValidE164Phone,
|
|
7
|
+
isValidIso8601,
|
|
8
|
+
hasRequiredKeys,
|
|
9
|
+
} from './validation';
|
|
10
|
+
|
|
11
|
+
describe('validation utils', () => {
|
|
12
|
+
describe('isValidUuid', () => {
|
|
13
|
+
it('debe validar UUIDs v4 correctos', () => {
|
|
14
|
+
expect(isValidUuid('550e8400-e29b-41d4-a716-446655440000')).toBe(true);
|
|
15
|
+
// UUID v4 válido (el segundo ejemplo es UUID v1, no v4)
|
|
16
|
+
expect(isValidUuid('f47ac10b-58cc-4372-a567-0e02b2c3d479')).toBe(true);
|
|
17
|
+
expect(isValidUuid('a1b2c3d4-e5f6-4789-a012-3456789abcde')).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('debe rechazar UUIDs inválidos', () => {
|
|
21
|
+
expect(isValidUuid('invalid')).toBe(false);
|
|
22
|
+
expect(isValidUuid('550e8400-e29b-41d4-a716')).toBe(false);
|
|
23
|
+
expect(isValidUuid('')).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('isValidEmail', () => {
|
|
28
|
+
it('debe validar emails correctos', () => {
|
|
29
|
+
expect(isValidEmail('user@example.com')).toBe(true);
|
|
30
|
+
expect(isValidEmail('test.user+tag@example.co.uk')).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('debe rechazar emails inválidos', () => {
|
|
34
|
+
expect(isValidEmail('invalid')).toBe(false);
|
|
35
|
+
expect(isValidEmail('@example.com')).toBe(false);
|
|
36
|
+
expect(isValidEmail('user@')).toBe(false);
|
|
37
|
+
expect(isValidEmail('')).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('isValidUrl', () => {
|
|
42
|
+
it('debe validar URLs correctas', () => {
|
|
43
|
+
expect(isValidUrl('https://example.com')).toBe(true);
|
|
44
|
+
expect(isValidUrl('http://example.com/path?query=1')).toBe(true);
|
|
45
|
+
expect(isValidUrl('https://subdomain.example.com:8080/path')).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('debe rechazar URLs inválidas', () => {
|
|
49
|
+
expect(isValidUrl('not-a-url')).toBe(false);
|
|
50
|
+
expect(isValidUrl('://invalid')).toBe(false);
|
|
51
|
+
expect(isValidUrl('')).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('isValidE164Phone', () => {
|
|
56
|
+
it('debe validar números E.164 correctos', () => {
|
|
57
|
+
expect(isValidE164Phone('+1234567890')).toBe(true);
|
|
58
|
+
expect(isValidE164Phone('+34612345678')).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('debe rechazar números inválidos', () => {
|
|
62
|
+
expect(isValidE164Phone('1234567890')).toBe(false);
|
|
63
|
+
expect(isValidE164Phone('+123')).toBe(false);
|
|
64
|
+
expect(isValidE164Phone('')).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('isValidIso8601', () => {
|
|
69
|
+
it('debe validar timestamps ISO 8601 correctos', () => {
|
|
70
|
+
expect(isValidIso8601('2024-01-01T00:00:00.000Z')).toBe(true);
|
|
71
|
+
expect(isValidIso8601('2024-12-31T23:59:59.999Z')).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('debe rechazar timestamps inválidos', () => {
|
|
75
|
+
expect(isValidIso8601('2024-01-01')).toBe(false);
|
|
76
|
+
expect(isValidIso8601('invalid')).toBe(false);
|
|
77
|
+
expect(isValidIso8601('')).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('hasRequiredKeys', () => {
|
|
82
|
+
it('debe validar que un objeto tiene todas las claves requeridas', () => {
|
|
83
|
+
const obj1: Record<string, number> = { a: 1, b: 2 };
|
|
84
|
+
const obj2: Record<string, number> = { a: 1, b: 2, c: 3 };
|
|
85
|
+
expect(hasRequiredKeys(obj1, ['a', 'b'])).toBe(true);
|
|
86
|
+
expect(hasRequiredKeys(obj2, ['a', 'b'])).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('debe retornar false si faltan claves requeridas', () => {
|
|
90
|
+
const obj1: Record<string, number> = { a: 1 };
|
|
91
|
+
const obj2: Record<string, unknown> = {};
|
|
92
|
+
expect(hasRequiredKeys(obj1, ['a', 'b'])).toBe(false);
|
|
93
|
+
expect(hasRequiredKeys(obj2, ['a'])).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilidades de validación para formatos comunes utilizados en los contratos.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Expresión regular para validar formato UUID v4.
|
|
7
|
+
*/
|
|
8
|
+
const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Expresión regular básica para validar formato de email.
|
|
12
|
+
* Nota: Esta es una validación básica. Para validación estricta, usar una biblioteca especializada.
|
|
13
|
+
*/
|
|
14
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Expresión regular para validar formato de teléfono E.164.
|
|
18
|
+
* E.164 requiere: + seguido de 1-3 dígitos del código de país, luego 1-14 dígitos adicionales.
|
|
19
|
+
* Mínimo total: + seguido de al menos 7 dígitos (código de país + número local).
|
|
20
|
+
*/
|
|
21
|
+
const E164_PHONE_REGEX = /^\+[1-9]\d{6,14}$/;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Valida si una cadena tiene formato UUID v4.
|
|
25
|
+
*
|
|
26
|
+
* @param value - Cadena a validar
|
|
27
|
+
* @returns true si es un UUID v4 válido
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* isValidUuid('550e8400-e29b-41d4-a716-446655440000'); // true
|
|
32
|
+
* isValidUuid('invalid'); // false
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function isValidUuid(value: string): boolean {
|
|
36
|
+
return UUID_V4_REGEX.test(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Valida si una cadena tiene formato de email básico.
|
|
41
|
+
*
|
|
42
|
+
* @param value - Cadena a validar
|
|
43
|
+
* @returns true si tiene formato de email válido
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* isValidEmail('user@example.com'); // true
|
|
48
|
+
* isValidEmail('invalid'); // false
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function isValidEmail(value: string): boolean {
|
|
52
|
+
return EMAIL_REGEX.test(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Valida si una cadena tiene formato de URL válido.
|
|
57
|
+
*
|
|
58
|
+
* @param value - Cadena a validar
|
|
59
|
+
* @returns true si es una URL válida
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* isValidUrl('https://example.com'); // true
|
|
64
|
+
* isValidUrl('not-a-url'); // false
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function isValidUrl(value: string): boolean {
|
|
68
|
+
try {
|
|
69
|
+
// URL está disponible en Node.js 18+ y navegadores modernos
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
|
|
71
|
+
const URLConstructor = (globalThis as unknown as { URL?: new (url: string) => unknown }).URL;
|
|
72
|
+
|
|
73
|
+
if (URLConstructor) {
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
75
|
+
new URLConstructor(value);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
// Fallback para entornos sin URL global
|
|
79
|
+
return /^https?:\/\/.+\..+/.test(value);
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Valida si una cadena tiene formato de teléfono E.164.
|
|
87
|
+
*
|
|
88
|
+
* @param value - Cadena a validar
|
|
89
|
+
* @returns true si tiene formato E.164 válido
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* isValidE164Phone('+1234567890'); // true
|
|
94
|
+
* isValidE164Phone('1234567890'); // false
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export function isValidE164Phone(value: string): boolean {
|
|
98
|
+
return E164_PHONE_REGEX.test(value);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Valida si una cadena es un timestamp ISO 8601 válido.
|
|
103
|
+
*
|
|
104
|
+
* @param value - Cadena a validar
|
|
105
|
+
* @returns true si es un timestamp ISO 8601 válido
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* isValidIso8601('2024-01-01T00:00:00.000Z'); // true
|
|
110
|
+
* isValidIso8601('invalid'); // false
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export function isValidIso8601(value: string): boolean {
|
|
114
|
+
const date = new Date(value);
|
|
115
|
+
return !isNaN(date.getTime()) && value === date.toISOString();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Valida si un objeto tiene todas las propiedades requeridas.
|
|
120
|
+
*
|
|
121
|
+
* @param obj - Objeto a validar
|
|
122
|
+
* @param requiredKeys - Array de claves requeridas
|
|
123
|
+
* @returns true si todas las claves están presentes
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* hasRequiredKeys({ a: 1, b: 2 }, ['a', 'b']); // true
|
|
128
|
+
* hasRequiredKeys({ a: 1 }, ['a', 'b']); // false
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export function hasRequiredKeys<T extends Record<string, unknown>>(
|
|
132
|
+
obj: T,
|
|
133
|
+
requiredKeys: (keyof T)[]
|
|
134
|
+
): boolean {
|
|
135
|
+
return requiredKeys.every((key) => key in obj);
|
|
136
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
isValidSemanticVersion,
|
|
4
|
+
parseSemanticVersion,
|
|
5
|
+
compareSemanticVersions,
|
|
6
|
+
isVersionGreaterThan,
|
|
7
|
+
isVersionLessThan,
|
|
8
|
+
isVersionGreaterOrEqual,
|
|
9
|
+
isVersionLessOrEqual,
|
|
10
|
+
type SemanticVersion,
|
|
11
|
+
} from './versioning';
|
|
12
|
+
|
|
13
|
+
describe('versioning', () => {
|
|
14
|
+
describe('isValidSemanticVersion', () => {
|
|
15
|
+
it('debe validar versiones semánticas correctas', () => {
|
|
16
|
+
expect(isValidSemanticVersion('1.0.0')).toBe(true);
|
|
17
|
+
expect(isValidSemanticVersion('0.1.0')).toBe(true);
|
|
18
|
+
expect(isValidSemanticVersion('10.20.30')).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('debe rechazar versiones inválidas', () => {
|
|
22
|
+
expect(isValidSemanticVersion('1.0')).toBe(false);
|
|
23
|
+
expect(isValidSemanticVersion('1.0.0.0')).toBe(false);
|
|
24
|
+
expect(isValidSemanticVersion('1.0.0-beta')).toBe(false);
|
|
25
|
+
expect(isValidSemanticVersion('invalid')).toBe(false);
|
|
26
|
+
expect(isValidSemanticVersion('')).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('parseSemanticVersion', () => {
|
|
31
|
+
it('debe parsear versiones válidas correctamente', () => {
|
|
32
|
+
expect(parseSemanticVersion('1.2.3')).toEqual({
|
|
33
|
+
major: 1,
|
|
34
|
+
minor: 2,
|
|
35
|
+
patch: 3,
|
|
36
|
+
});
|
|
37
|
+
expect(parseSemanticVersion('10.20.30')).toEqual({
|
|
38
|
+
major: 10,
|
|
39
|
+
minor: 20,
|
|
40
|
+
patch: 30,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('debe retornar null para versiones inválidas', () => {
|
|
45
|
+
expect(parseSemanticVersion('1.0')).toBeNull();
|
|
46
|
+
expect(parseSemanticVersion('invalid')).toBeNull();
|
|
47
|
+
expect(parseSemanticVersion('')).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('compareSemanticVersions', () => {
|
|
52
|
+
it('debe comparar versiones correctamente', () => {
|
|
53
|
+
expect(compareSemanticVersions('1.0.0', '1.0.0')).toBe(0);
|
|
54
|
+
expect(compareSemanticVersions('1.0.0', '1.0.1')).toBe(-1);
|
|
55
|
+
expect(compareSemanticVersions('1.0.1', '1.0.0')).toBe(1);
|
|
56
|
+
expect(compareSemanticVersions('1.1.0', '1.0.0')).toBe(1);
|
|
57
|
+
expect(compareSemanticVersions('2.0.0', '1.9.9')).toBe(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('debe lanzar error para versiones inválidas', () => {
|
|
61
|
+
expect(() => compareSemanticVersions('invalid', '1.0.0')).toThrow();
|
|
62
|
+
expect(() => compareSemanticVersions('1.0.0', 'invalid')).toThrow();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('isVersionGreaterThan', () => {
|
|
67
|
+
it('debe retornar true cuando version1 > version2', () => {
|
|
68
|
+
expect(isVersionGreaterThan('2.0.0', '1.9.9')).toBe(true);
|
|
69
|
+
expect(isVersionGreaterThan('1.1.0', '1.0.9')).toBe(true);
|
|
70
|
+
expect(isVersionGreaterThan('1.0.1', '1.0.0')).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('debe retornar false cuando version1 <= version2', () => {
|
|
74
|
+
expect(isVersionGreaterThan('1.0.0', '1.0.0')).toBe(false);
|
|
75
|
+
expect(isVersionGreaterThan('1.0.0', '2.0.0')).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('isVersionLessThan', () => {
|
|
80
|
+
it('debe retornar true cuando version1 < version2', () => {
|
|
81
|
+
expect(isVersionLessThan('1.0.0', '2.0.0')).toBe(true);
|
|
82
|
+
expect(isVersionLessThan('1.0.0', '1.1.0')).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('debe retornar false cuando version1 >= version2', () => {
|
|
86
|
+
expect(isVersionLessThan('1.0.0', '1.0.0')).toBe(false);
|
|
87
|
+
expect(isVersionLessThan('2.0.0', '1.0.0')).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('isVersionGreaterOrEqual', () => {
|
|
92
|
+
it('debe retornar true cuando version1 >= version2', () => {
|
|
93
|
+
expect(isVersionGreaterOrEqual('2.0.0', '1.0.0')).toBe(true);
|
|
94
|
+
expect(isVersionGreaterOrEqual('1.0.0', '1.0.0')).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('debe retornar false cuando version1 < version2', () => {
|
|
98
|
+
expect(isVersionGreaterOrEqual('1.0.0', '2.0.0')).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('isVersionLessOrEqual', () => {
|
|
103
|
+
it('debe retornar true cuando version1 <= version2', () => {
|
|
104
|
+
expect(isVersionLessOrEqual('1.0.0', '2.0.0')).toBe(true);
|
|
105
|
+
expect(isVersionLessOrEqual('1.0.0', '1.0.0')).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('debe retornar false cuando version1 > version2', () => {
|
|
109
|
+
expect(isVersionLessOrEqual('2.0.0', '1.0.0')).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Representa una versión semántica en formato MAJOR.MINOR.PATCH
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* const version: SemanticVersion = '1.2.3';
|
|
7
|
+
* ```
|
|
8
|
+
*/
|
|
9
|
+
export type SemanticVersion = `${number}.${number}.${number}`;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Estructura que contiene un payload con su versión asociada.
|
|
13
|
+
* Útil para versionado de datos y compatibilidad entre versiones.
|
|
14
|
+
*
|
|
15
|
+
* @template TPayload - Tipo del payload versionado
|
|
16
|
+
*/
|
|
17
|
+
export type Versioned<TPayload> = {
|
|
18
|
+
version: SemanticVersion;
|
|
19
|
+
payload: TPayload;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Valida si una cadena de texto cumple con el formato de versión semántica.
|
|
24
|
+
*
|
|
25
|
+
* @param version - Cadena a validar
|
|
26
|
+
* @returns true si es una versión semántica válida, false en caso contrario
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* isValidSemanticVersion('1.2.3'); // true
|
|
31
|
+
* isValidSemanticVersion('1.2'); // false
|
|
32
|
+
* isValidSemanticVersion('invalid'); // false
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function isValidSemanticVersion(version: string): version is SemanticVersion {
|
|
36
|
+
return /^\d+\.\d+\.\d+$/.test(version);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parsea una versión semántica y retorna sus componentes numéricos.
|
|
41
|
+
*
|
|
42
|
+
* @param version - Versión semántica a parsear
|
|
43
|
+
* @returns Objeto con major, minor y patch, o null si la versión es inválida
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* parseSemanticVersion('1.2.3'); // { major: 1, minor: 2, patch: 3 }
|
|
48
|
+
* parseSemanticVersion('invalid'); // null
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function parseSemanticVersion(version: string): {
|
|
52
|
+
major: number;
|
|
53
|
+
minor: number;
|
|
54
|
+
patch: number;
|
|
55
|
+
} | null {
|
|
56
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
57
|
+
if (!match) return null;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
major: parseInt(match[1], 10),
|
|
61
|
+
minor: parseInt(match[2], 10),
|
|
62
|
+
patch: parseInt(match[3], 10),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Compara dos versiones semánticas.
|
|
68
|
+
*
|
|
69
|
+
* @param version1 - Primera versión a comparar
|
|
70
|
+
* @param version2 - Segunda versión a comparar
|
|
71
|
+
* @returns -1 si version1 < version2, 0 si son iguales, 1 si version1 > version2
|
|
72
|
+
* @throws Error si alguna de las versiones es inválida
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* compareSemanticVersions('1.2.3', '1.2.4'); // -1
|
|
77
|
+
* compareSemanticVersions('2.0.0', '1.9.9'); // 1
|
|
78
|
+
* compareSemanticVersions('1.0.0', '1.0.0'); // 0
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export function compareSemanticVersions(
|
|
82
|
+
version1: string,
|
|
83
|
+
version2: string
|
|
84
|
+
): number {
|
|
85
|
+
const v1 = parseSemanticVersion(version1);
|
|
86
|
+
const v2 = parseSemanticVersion(version2);
|
|
87
|
+
|
|
88
|
+
if (!v1 || !v2) {
|
|
89
|
+
throw new Error(`Invalid semantic version: ${!v1 ? version1 : version2}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (v1.major !== v2.major) {
|
|
93
|
+
return v1.major - v2.major;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (v1.minor !== v2.minor) {
|
|
97
|
+
return v1.minor - v2.minor;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return v1.patch - v2.patch;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Verifica si una versión es mayor que otra.
|
|
105
|
+
*
|
|
106
|
+
* @param version1 - Versión a comparar
|
|
107
|
+
* @param version2 - Versión de referencia
|
|
108
|
+
* @returns true si version1 > version2
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* isVersionGreaterThan('2.0.0', '1.9.9'); // true
|
|
113
|
+
* isVersionGreaterThan('1.0.0', '1.0.0'); // false
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export function isVersionGreaterThan(version1: string, version2: string): boolean {
|
|
117
|
+
return compareSemanticVersions(version1, version2) > 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Verifica si una versión es menor que otra.
|
|
122
|
+
*
|
|
123
|
+
* @param version1 - Versión a comparar
|
|
124
|
+
* @param version2 - Versión de referencia
|
|
125
|
+
* @returns true si version1 < version2
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* isVersionLessThan('1.0.0', '2.0.0'); // true
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export function isVersionLessThan(version1: string, version2: string): boolean {
|
|
133
|
+
return compareSemanticVersions(version1, version2) < 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Verifica si una versión es mayor o igual que otra.
|
|
138
|
+
*
|
|
139
|
+
* @param version1 - Versión a comparar
|
|
140
|
+
* @param version2 - Versión de referencia
|
|
141
|
+
* @returns true si version1 >= version2
|
|
142
|
+
*/
|
|
143
|
+
export function isVersionGreaterOrEqual(version1: string, version2: string): boolean {
|
|
144
|
+
return compareSemanticVersions(version1, version2) >= 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Verifica si una versión es menor o igual que otra.
|
|
149
|
+
*
|
|
150
|
+
* @param version1 - Versión a comparar
|
|
151
|
+
* @param version2 - Versión de referencia
|
|
152
|
+
* @returns true si version1 <= version2
|
|
153
|
+
*/
|
|
154
|
+
export function isVersionLessOrEqual(version1: string, version2: string): boolean {
|
|
155
|
+
return compareSemanticVersions(version1, version2) <= 0;
|
|
156
|
+
}
|