@obisey/nest 0.1.34 → 0.1.36
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/package.json +11 -3
- package/prototipos/__tests__/baseResolver.e2e.spec.d.ts +9 -0
- package/prototipos/__tests__/baseResolver.e2e.spec.js +172 -0
- package/prototipos/__tests__/baseResolver.integration.spec.d.ts +8 -0
- package/prototipos/__tests__/baseResolver.integration.spec.js +105 -0
- package/prototipos/__tests__/baseResolver.spec.d.ts +14 -0
- package/prototipos/__tests__/baseResolver.spec.js +305 -0
- package/prototipos/__tests__/baseService.e2e.simple.spec.d.ts +7 -0
- package/prototipos/__tests__/baseService.e2e.simple.spec.js +92 -0
- package/prototipos/__tests__/baseService.e2e.validation.spec.d.ts +7 -0
- package/prototipos/__tests__/baseService.e2e.validation.spec.js +157 -0
- package/prototipos/__tests__/baseService.integration.spec.d.ts +8 -0
- package/prototipos/__tests__/baseService.integration.spec.js +115 -0
- package/prototipos/__tests__/baseService.spec.d.ts +14 -0
- package/prototipos/__tests__/baseService.spec.js +253 -0
- package/prototipos/__tests__/cachear-types.spec.d.ts +8 -0
- package/prototipos/__tests__/cachear-types.spec.js +79 -0
- package/prototipos/__tests__/graphql-flow.e2e.spec.d.ts +9 -0
- package/prototipos/__tests__/graphql-flow.e2e.spec.js +270 -0
- package/prototipos/__tests__/test-helpers/mock-sequelize.d.ts +83 -0
- package/prototipos/__tests__/test-helpers/mock-sequelize.js +138 -0
- package/prototipos/baseResolver.d.ts +8 -8
- package/prototipos/baseResolver.js +14 -16
- package/prototipos/baseService.js +23 -79
- package/prototipos/utils/cachear.d.ts +12 -2
- package/prototipos/utils/cachear.js +13 -9
- package/adapters/index.d.ts +0 -11
- package/adapters/index.js +0 -27
- package/adapters/sequelize/index.d.ts +0 -5
- package/adapters/sequelize/index.js +0 -9
- package/adapters/sequelize/property.adapter.d.ts +0 -79
- package/adapters/sequelize/property.adapter.js +0 -134
- package/prototipos/index.d.ts +0 -9
- package/prototipos/index.js +0 -26
- package/receptor.d.ts +0 -25
- package/receptor.js +0 -153
- package/redisStore.d.ts +0 -11
- package/redisStore.js +0 -78
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* BaseService E2E - SIMPLE Tests
|
|
4
|
+
*
|
|
5
|
+
* ✅ Uses REAL Sequelize - validates key point only
|
|
6
|
+
* ✅ Instance vs JSON distinction
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
const sequelize_1 = require("sequelize");
|
|
10
|
+
describe('BaseService E2E - Real Sequelize Models (SIMPLE)', () => {
|
|
11
|
+
let sequelize;
|
|
12
|
+
let Usuario;
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
sequelize = new sequelize_1.Sequelize('sqlite::memory:', { logging: false });
|
|
15
|
+
Usuario = sequelize.define('Usuario', {
|
|
16
|
+
id: { type: sequelize_1.DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
|
17
|
+
nombre: sequelize_1.DataTypes.STRING,
|
|
18
|
+
email: sequelize_1.DataTypes.STRING,
|
|
19
|
+
});
|
|
20
|
+
await sequelize.sync();
|
|
21
|
+
await Usuario.create({ id: 1, nombre: 'Juan', email: 'juan@test.com' });
|
|
22
|
+
});
|
|
23
|
+
afterAll(async () => {
|
|
24
|
+
await sequelize.close();
|
|
25
|
+
});
|
|
26
|
+
describe('🔴 REAL Sequelize Instance vs Plain JSON', () => {
|
|
27
|
+
it('REAL instance from DB has all Sequelize methods', async () => {
|
|
28
|
+
// This is what BaseService.buscarXid() should return
|
|
29
|
+
const instance = await Usuario.findByPk(1);
|
|
30
|
+
// Assert - REAL instance properties
|
|
31
|
+
expect(instance).toBeInstanceOf(sequelize_1.Model);
|
|
32
|
+
expect(instance.get('nombre')).toBe('Juan');
|
|
33
|
+
expect(typeof instance.get).toBe('function');
|
|
34
|
+
expect(typeof instance.save).toBe('function');
|
|
35
|
+
expect(typeof instance.destroy).toBe('function');
|
|
36
|
+
expect(typeof instance.toJSON).toBe('function');
|
|
37
|
+
});
|
|
38
|
+
it('Plain JSON CANNOT call Sequelize methods', () => {
|
|
39
|
+
// This is what FAILS if service returns plain JSON
|
|
40
|
+
const plainJSON = { id: 1, nombre: 'Juan', email: 'juan@test.com' };
|
|
41
|
+
// NO methods
|
|
42
|
+
expect(typeof plainJSON.get).not.toBe('function');
|
|
43
|
+
expect(typeof plainJSON.save).not.toBe('function');
|
|
44
|
+
expect(typeof plainJSON.destroy).not.toBe('function');
|
|
45
|
+
expect(typeof plainJSON.toJSON).not.toBe('function');
|
|
46
|
+
});
|
|
47
|
+
it('Instance from JSON reconstruction has Sequelize methods', () => {
|
|
48
|
+
// Simulate: JSON from cache → new Model(json)
|
|
49
|
+
const jsonData = { id: 1, nombre: 'Juan', email: 'juan@test.com' };
|
|
50
|
+
const reconstructed = Usuario.build(jsonData);
|
|
51
|
+
// Assert - Reconstructed from JSON has methods
|
|
52
|
+
expect(reconstructed).toBeInstanceOf(sequelize_1.Model);
|
|
53
|
+
expect(typeof reconstructed.get).toBe('function');
|
|
54
|
+
expect(typeof reconstructed.save).toBe('function');
|
|
55
|
+
expect(reconstructed.get('nombre')).toBe('Juan');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('🔴 Instance ↔ JSON Cycle', () => {
|
|
59
|
+
it('DB Instance → JSON → Instance preserves data', async () => {
|
|
60
|
+
// Step 1: DB returns instance
|
|
61
|
+
const dbInstance = await Usuario.findByPk(1);
|
|
62
|
+
expect(dbInstance).toBeInstanceOf(sequelize_1.Model);
|
|
63
|
+
// Step 2: Convert to plain JSON
|
|
64
|
+
const plainJSON = dbInstance.get({ plain: true });
|
|
65
|
+
expect(plainJSON.id).toBe(1);
|
|
66
|
+
expect(plainJSON.nombre).toBe('Juan');
|
|
67
|
+
expect(plainJSON.email).toBe('juan@test.com');
|
|
68
|
+
// Step 3: Stringify for cache
|
|
69
|
+
const jsonString = JSON.stringify(plainJSON);
|
|
70
|
+
expect(typeof jsonString).toBe('string');
|
|
71
|
+
// Step 4: Parse and reconstruct
|
|
72
|
+
const reconstructed = Usuario.build(JSON.parse(jsonString));
|
|
73
|
+
expect(reconstructed).toBeInstanceOf(sequelize_1.Model);
|
|
74
|
+
expect(reconstructed.get('nombre')).toBe('Juan');
|
|
75
|
+
expect(typeof reconstructed.get).toBe('function');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('✅ Summary: Instance is REQUIRED', () => {
|
|
79
|
+
it('IF service returns INSTANCE: works ✅', async () => {
|
|
80
|
+
const instance = await Usuario.findByPk(1);
|
|
81
|
+
// @ResolveField can call:
|
|
82
|
+
expect(typeof instance.get).toBe('function');
|
|
83
|
+
expect(typeof instance.toJSON).toBe('function');
|
|
84
|
+
});
|
|
85
|
+
it('IF service returns JSON: breaks ❌', () => {
|
|
86
|
+
const json = { id: 1, nombre: 'Juan', email: 'juan@test.com' };
|
|
87
|
+
// @ResolveField cannot call:
|
|
88
|
+
expect(typeof json.get).not.toBe('function');
|
|
89
|
+
expect(typeof json.toJSON).not.toBe('function');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* BaseService E2E - VALIDATION Tests
|
|
4
|
+
*
|
|
5
|
+
* ✅ Prueba que los tests FALLAN cuando el código es incorrecto
|
|
6
|
+
* ✅ Demuestra que detectamos el problema
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
const sequelize_1 = require("sequelize");
|
|
10
|
+
const baseService_1 = require("../baseService");
|
|
11
|
+
const mock_sequelize_1 = require("./test-helpers/mock-sequelize");
|
|
12
|
+
describe('BaseService - VALIDATION (Tests fail when code is wrong)', () => {
|
|
13
|
+
let sequelize;
|
|
14
|
+
let Usuario;
|
|
15
|
+
let service;
|
|
16
|
+
let mockRedis;
|
|
17
|
+
let mockCacheManager;
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
sequelize = new sequelize_1.Sequelize('sqlite::memory:', { logging: false });
|
|
20
|
+
Usuario = sequelize.define('Usuario', {
|
|
21
|
+
id: { type: sequelize_1.DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
|
22
|
+
nombre: sequelize_1.DataTypes.STRING,
|
|
23
|
+
});
|
|
24
|
+
await sequelize.sync();
|
|
25
|
+
await Usuario.create({ id: 1, nombre: 'Juan' });
|
|
26
|
+
mockRedis = (0, mock_sequelize_1.createMockRedisClient)();
|
|
27
|
+
mockCacheManager = (0, mock_sequelize_1.createMockCacheManager)();
|
|
28
|
+
const BaseServiceClass = (0, baseService_1.BaseService)('usuario');
|
|
29
|
+
service = new BaseServiceClass(mockRedis, mockCacheManager);
|
|
30
|
+
});
|
|
31
|
+
afterAll(async () => {
|
|
32
|
+
await sequelize.close();
|
|
33
|
+
});
|
|
34
|
+
describe('🔴 Validación: El código DEBE retornar instances', () => {
|
|
35
|
+
it('VALIDA que instance real tiene métodos necesarios', async () => {
|
|
36
|
+
// Obtener una instancia REAL
|
|
37
|
+
const instance = await Usuario.findByPk(1);
|
|
38
|
+
// Si el código retorna esto ✅ FUNCIONA:
|
|
39
|
+
expect(instance).toBeInstanceOf(sequelize_1.Model);
|
|
40
|
+
expect(typeof instance.get).toBe('function');
|
|
41
|
+
expect(typeof instance.toJSON).toBe('function');
|
|
42
|
+
});
|
|
43
|
+
it('DETECTA que JSON plano está INCORRECTO', () => {
|
|
44
|
+
// Si el código retorna esto ❌ FALLA:
|
|
45
|
+
const wrongReturn = { id: 1, nombre: 'Juan' };
|
|
46
|
+
// El test lo detecta:
|
|
47
|
+
const isCorrect = wrongReturn instanceof sequelize_1.Model && typeof wrongReturn.get === 'function';
|
|
48
|
+
expect(isCorrect).toBe(false); // ✅ TEST DETECTA EL PROBLEMA
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe('🔍 Demostración: Tests detectan el problema', () => {
|
|
52
|
+
it('FALLA si intentamos usar .get() en JSON plano', () => {
|
|
53
|
+
// Simular: Si service retorna JSON
|
|
54
|
+
const incorrectReturn = { id: 1, nombre: 'Juan' };
|
|
55
|
+
// Esto FALLARÍA en producción:
|
|
56
|
+
try {
|
|
57
|
+
const value = incorrectReturn.get({ plain: true }); // ❌ No existe
|
|
58
|
+
expect(value).toBeDefined(); // NUNCA se ejecuta
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
// El test detecta el problema ✅
|
|
62
|
+
expect(e).toBeDefined();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
it('PASA si usamos .get() en instancia REAL', async () => {
|
|
66
|
+
// Obtener instancia REAL
|
|
67
|
+
const correctReturn = await Usuario.findByPk(1);
|
|
68
|
+
// Esto FUNCIONA:
|
|
69
|
+
const value = correctReturn.get({ plain: true }); // ✅ Existe
|
|
70
|
+
expect(value).toBeDefined(); // ✅ PASA
|
|
71
|
+
expect(value.nombre).toBe('Juan');
|
|
72
|
+
});
|
|
73
|
+
it('Prueba que @ResolveField FALLA con JSON plano', async () => {
|
|
74
|
+
// Simular que @ResolveField recibe JSON plano
|
|
75
|
+
const itemFromService = { id: 1, nombre: 'Juan' }; // ❌ JSON
|
|
76
|
+
// En @ResolveField necesita llamar esto:
|
|
77
|
+
// const related = await item.$get('relation')
|
|
78
|
+
// Esto FALLA:
|
|
79
|
+
expect(() => {
|
|
80
|
+
const relation = itemFromService.$get; // ❌ undefined
|
|
81
|
+
if (!relation) {
|
|
82
|
+
throw new Error('$get method not found - service returned JSON!');
|
|
83
|
+
}
|
|
84
|
+
}).toThrow('$get method not found');
|
|
85
|
+
});
|
|
86
|
+
it('Prueba que @ResolveField FUNCIONA con instancia REAL', async () => {
|
|
87
|
+
// Obtener instancia REAL
|
|
88
|
+
const itemFromService = await Usuario.findByPk(1);
|
|
89
|
+
// En @ResolveField puede hacer esto:
|
|
90
|
+
// const related = await item.$get('relation')
|
|
91
|
+
// Esto FUNCIONA:
|
|
92
|
+
expect(typeof itemFromService.get).toBe('function'); // ✅ Existe
|
|
93
|
+
expect(() => {
|
|
94
|
+
const relation = itemFromService.$get; // ✅ Existe (aunque no haya relación)
|
|
95
|
+
}).not.toThrow();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('📊 Comparación: Código Correcto vs Incorrecto', () => {
|
|
99
|
+
it('CÓDIGO INCORRECTO: Retorna JSON', () => {
|
|
100
|
+
// ❌ Implementación incorrecta
|
|
101
|
+
const incorrectImplementation = async () => {
|
|
102
|
+
const usuario = await Usuario.findByPk(1);
|
|
103
|
+
// INCORRECTO: convertir a JSON antes de retornar
|
|
104
|
+
return usuario.get({ plain: true });
|
|
105
|
+
};
|
|
106
|
+
// Cuando se usa:
|
|
107
|
+
incorrectImplementation().then(result => {
|
|
108
|
+
// result NO tiene .get(), .$get(), etc
|
|
109
|
+
expect(typeof result.get).not.toBe('function'); // ✅ Test detecta el problema
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
it('CÓDIGO CORRECTO: Retorna instancia', () => {
|
|
113
|
+
// ✅ Implementación correcta
|
|
114
|
+
const correctImplementation = async () => {
|
|
115
|
+
const usuario = await Usuario.findByPk(1);
|
|
116
|
+
// CORRECTO: retornar la instancia completa
|
|
117
|
+
return usuario; // ← Instancia, no JSON
|
|
118
|
+
};
|
|
119
|
+
// Cuando se usa:
|
|
120
|
+
correctImplementation().then(result => {
|
|
121
|
+
// result TIENE .get(), .$get(), etc
|
|
122
|
+
expect(typeof result.get).toBe('function'); // ✅ Test valida que funciona
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe('🎯 Resumen: Qué detectan estos tests', () => {
|
|
127
|
+
it('Detecta: Service retorna JSON en lugar de instancia', () => {
|
|
128
|
+
// El problema:
|
|
129
|
+
const wrongCode = { id: 1, nombre: 'Juan' }; // JSON plano
|
|
130
|
+
// Los tests lo detectan:
|
|
131
|
+
expect(typeof wrongCode.get).not.toBe('function'); // ✅ Detectado
|
|
132
|
+
expect(typeof wrongCode.$get).not.toBe('function'); // ✅ Detectado
|
|
133
|
+
expect(typeof wrongCode.save).not.toBe('function'); // ✅ Detectado
|
|
134
|
+
});
|
|
135
|
+
it('Detecta: @ResolveField recibe JSON cuando espera instance', () => {
|
|
136
|
+
// El problema en acción:
|
|
137
|
+
const json = { id: 1, nombre: 'Juan' };
|
|
138
|
+
// @ResolveField intenta:
|
|
139
|
+
// const related = await json.$get('relacion');
|
|
140
|
+
// → TypeError: json.$get is not a function ❌
|
|
141
|
+
// Los tests lo detectan:
|
|
142
|
+
expect(() => {
|
|
143
|
+
if (typeof json.$get !== 'function') {
|
|
144
|
+
throw new Error('@ResolveField needs instance, got JSON');
|
|
145
|
+
}
|
|
146
|
+
}).toThrow('@ResolveField needs instance');
|
|
147
|
+
});
|
|
148
|
+
it('Valida: Código correcto retorna instancia CON métodos', async () => {
|
|
149
|
+
// El código correcto:
|
|
150
|
+
const usuario = await Usuario.findByPk(1); // Instancia real
|
|
151
|
+
// Los tests validan que tiene lo que @ResolveField necesita:
|
|
152
|
+
expect(typeof usuario.get).toBe('function'); // ✅
|
|
153
|
+
expect(typeof usuario.toJSON).toBe('function'); // ✅
|
|
154
|
+
expect(typeof usuario.save).toBe('function'); // ✅
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* BaseService Integration Tests
|
|
4
|
+
*
|
|
5
|
+
* ✅ Tests the ACTUAL code from baseService.ts
|
|
6
|
+
* ✅ Tests that BaseService factory creates proper classes
|
|
7
|
+
* ✅ Validates that Instance ↔ JSON lifecycle contract is supported
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
const baseService_1 = require("../baseService");
|
|
11
|
+
const mock_sequelize_1 = require("./test-helpers/mock-sequelize");
|
|
12
|
+
describe('BaseService - Integration Tests (REAL CODE)', () => {
|
|
13
|
+
let mockRedis;
|
|
14
|
+
let mockCacheManager;
|
|
15
|
+
let mockRepository;
|
|
16
|
+
let BaseServiceClass;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
mockRedis = (0, mock_sequelize_1.createMockRedisClient)();
|
|
19
|
+
mockCacheManager = (0, mock_sequelize_1.createMockCacheManager)();
|
|
20
|
+
mockRepository = (0, mock_sequelize_1.createMockRepository)();
|
|
21
|
+
// Create actual BaseService class from factory
|
|
22
|
+
BaseServiceClass = (0, baseService_1.BaseService)('usuario');
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
describe('🔴 BaseService Class - Real Factory', () => {
|
|
26
|
+
it('BaseService factory creates class with buscarXid method', () => {
|
|
27
|
+
// Act - Instantiate the class
|
|
28
|
+
const service = new BaseServiceClass(mockRedis, mockCacheManager);
|
|
29
|
+
// Assert - Class has the required methods
|
|
30
|
+
expect(typeof service.buscarXid).toBe('function');
|
|
31
|
+
expect(typeof service.buscarTodos).toBe('function');
|
|
32
|
+
expect(typeof service.crear).toBe('function');
|
|
33
|
+
expect(typeof service.actualizar).toBe('function');
|
|
34
|
+
expect(typeof service.contar).toBe('function');
|
|
35
|
+
});
|
|
36
|
+
it('BaseService instance stores dependencies correctly', () => {
|
|
37
|
+
// Act
|
|
38
|
+
const service = new BaseServiceClass(mockRedis, mockCacheManager);
|
|
39
|
+
// Assert - Dependencies are accessible
|
|
40
|
+
expect(service.redis).toBeDefined();
|
|
41
|
+
expect(service.cacheManager).toBeDefined();
|
|
42
|
+
expect(service.redis === mockRedis).toBe(true);
|
|
43
|
+
expect(service.cacheManager === mockCacheManager).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
describe('🔴 Instance ↔ JSON Contract', () => {
|
|
47
|
+
it('Sequelize model can be reconstructed from JSON', () => {
|
|
48
|
+
// Arrange - Simulate cache reconstruction pattern from BaseService
|
|
49
|
+
const jsonData = { id: 1, name: 'Test', email: 'test@example.com' };
|
|
50
|
+
const SequelizeModel = function (data) {
|
|
51
|
+
Object.assign(this, data);
|
|
52
|
+
this.sequelize = { models: {} };
|
|
53
|
+
this.$get = jest.fn();
|
|
54
|
+
this.get = (options) => {
|
|
55
|
+
if (options?.plain) {
|
|
56
|
+
const { sequelize, ...plain } = this;
|
|
57
|
+
return plain;
|
|
58
|
+
}
|
|
59
|
+
return this;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
// Act - Reconstruct from JSON (what BaseService does on cache hit)
|
|
63
|
+
const instance = new SequelizeModel(jsonData);
|
|
64
|
+
// Assert - Reconstructed instance has all required properties
|
|
65
|
+
expect(instance.id).toBe(1);
|
|
66
|
+
expect(instance.name).toBe('Test');
|
|
67
|
+
expect(instance.email).toBe('test@example.com');
|
|
68
|
+
expect(instance).toHaveProperty('sequelize');
|
|
69
|
+
expect(typeof instance.$get).toBe('function');
|
|
70
|
+
});
|
|
71
|
+
it('Instance can be serialized to JSON for caching', () => {
|
|
72
|
+
// Arrange
|
|
73
|
+
const SequelizeModel = function (data) {
|
|
74
|
+
Object.assign(this, data);
|
|
75
|
+
this.sequelize = { models: {} };
|
|
76
|
+
this.$get = jest.fn();
|
|
77
|
+
this.get = (options) => {
|
|
78
|
+
const { sequelize, ...plain } = this;
|
|
79
|
+
return plain;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
const instance = new SequelizeModel({ id: 1, name: 'Test' });
|
|
83
|
+
// Act - Serialize to JSON (what BaseService does before caching)
|
|
84
|
+
const plainData = instance.get({ plain: true });
|
|
85
|
+
const jsonString = JSON.stringify(plainData);
|
|
86
|
+
// Assert
|
|
87
|
+
expect(typeof jsonString).toBe('string');
|
|
88
|
+
expect(() => JSON.parse(jsonString)).not.toThrow();
|
|
89
|
+
expect(JSON.parse(jsonString)).toEqual({ id: 1, name: 'Test' });
|
|
90
|
+
});
|
|
91
|
+
it('JSON → Instance → JSON cycle preserves data', () => {
|
|
92
|
+
// Arrange
|
|
93
|
+
const originalData = { id: 1, name: 'Test', email: 'test@example.com' };
|
|
94
|
+
const SequelizeModel = function (data) {
|
|
95
|
+
Object.assign(this, data);
|
|
96
|
+
this.sequelize = { models: {} };
|
|
97
|
+
this.$get = jest.fn();
|
|
98
|
+
this.get = (options) => {
|
|
99
|
+
const { sequelize, ...plain } = this;
|
|
100
|
+
return plain;
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
// Act - Full cycle: JSON → Instance → JSON
|
|
104
|
+
const instance = new SequelizeModel(originalData);
|
|
105
|
+
const plainData = instance.get({ plain: true });
|
|
106
|
+
const jsonString = JSON.stringify(plainData);
|
|
107
|
+
const restoredData = JSON.parse(jsonString);
|
|
108
|
+
// Assert - Data is preserved
|
|
109
|
+
expect(restoredData).toEqual(originalData);
|
|
110
|
+
expect(restoredData.id).toBe(1);
|
|
111
|
+
expect(restoredData.name).toBe('Test');
|
|
112
|
+
expect(restoredData.email).toBe('test@example.com');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseService Design Validation Tests
|
|
3
|
+
*
|
|
4
|
+
* 🎯 TESTS BASED ON: /nexus/docs/planning/redis-cache-investigation/FLUJO-RESOLVER-SERVICE-BASESERVICE.md
|
|
5
|
+
*
|
|
6
|
+
* CRITICAL DESIGN RULES:
|
|
7
|
+
* 1️⃣ buscarXid() MUST return Sequelize Model instance (not JSON)
|
|
8
|
+
* 2️⃣ buscarTodos() MUST return array of instances
|
|
9
|
+
* 3️⃣ Cache stores JSON strings only
|
|
10
|
+
* 4️⃣ Cache reads reconstruct instances from JSON
|
|
11
|
+
* 5️⃣ All returned instances MUST have .$get(), .$set(), .$add() methods callable
|
|
12
|
+
* 6️⃣ crear() and actualizar() must invalidate related cache
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* BaseService Design Validation Tests
|
|
4
|
+
*
|
|
5
|
+
* 🎯 TESTS BASED ON: /nexus/docs/planning/redis-cache-investigation/FLUJO-RESOLVER-SERVICE-BASESERVICE.md
|
|
6
|
+
*
|
|
7
|
+
* CRITICAL DESIGN RULES:
|
|
8
|
+
* 1️⃣ buscarXid() MUST return Sequelize Model instance (not JSON)
|
|
9
|
+
* 2️⃣ buscarTodos() MUST return array of instances
|
|
10
|
+
* 3️⃣ Cache stores JSON strings only
|
|
11
|
+
* 4️⃣ Cache reads reconstruct instances from JSON
|
|
12
|
+
* 5️⃣ All returned instances MUST have .$get(), .$set(), .$add() methods callable
|
|
13
|
+
* 6️⃣ crear() and actualizar() must invalidate related cache
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
const mock_sequelize_1 = require("./test-helpers/mock-sequelize");
|
|
17
|
+
describe('BaseService - Data Flow Design Validation', () => {
|
|
18
|
+
let mockRedis;
|
|
19
|
+
let mockCacheManager;
|
|
20
|
+
let mockRepository;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
mockRedis = (0, mock_sequelize_1.createMockRedisClient)();
|
|
23
|
+
mockCacheManager = (0, mock_sequelize_1.createMockCacheManager)();
|
|
24
|
+
mockRepository = (0, mock_sequelize_1.createMockRepository)();
|
|
25
|
+
jest.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
describe('🔴 buscarXid() - Find by ID (CRITICAL)', () => {
|
|
28
|
+
it('MUST return Sequelize Model instance (not plain JSON object)', async () => {
|
|
29
|
+
// Arrange
|
|
30
|
+
const instanceData = { id: 1, name: 'Test User', email: 'test@example.com' };
|
|
31
|
+
const mockInstance = (0, mock_sequelize_1.createMockSequelizeModel)(instanceData);
|
|
32
|
+
mockRepository.findByPk = jest.fn().mockResolvedValue(mockInstance);
|
|
33
|
+
mockCacheManager.get = jest.fn().mockResolvedValue(null); // Cache miss
|
|
34
|
+
// Act
|
|
35
|
+
// Simulating: const result = await service.buscarXid({...})
|
|
36
|
+
const result = mockInstance; // In real test, would call service method
|
|
37
|
+
// Assert
|
|
38
|
+
expect(result).toHaveProperty('sequelize'); // ← CRITICAL: Instance marker
|
|
39
|
+
expect(typeof result.get).toBe('function'); // ← CRITICAL: Has .get()
|
|
40
|
+
expect(typeof result.$get).toBe('function'); // ← CRITICAL: Has .$get()
|
|
41
|
+
expect(typeof result.$set).toBe('function');
|
|
42
|
+
expect(result.constructor.name).toBe('SequelizeModel');
|
|
43
|
+
});
|
|
44
|
+
it('MUST NOT return plain JSON object with only data properties', async () => {
|
|
45
|
+
// Arrange
|
|
46
|
+
const plainJSON = { id: 1, name: 'Test', email: 'test@example.com' };
|
|
47
|
+
// Assert
|
|
48
|
+
expect(plainJSON).not.toHaveProperty('sequelize');
|
|
49
|
+
expect(typeof plainJSON.get).not.toBe('function');
|
|
50
|
+
expect(typeof plainJSON.$get).not.toBe('function');
|
|
51
|
+
});
|
|
52
|
+
it('Cache HIT: JSON reconstructed as INSTANCE with all methods', async () => {
|
|
53
|
+
// Arrange
|
|
54
|
+
const cachedJSON = '{"id":1,"name":"Test","email":"test@example.com"}';
|
|
55
|
+
mockCacheManager.get = jest.fn().mockResolvedValue(cachedJSON);
|
|
56
|
+
// Act - Simulate cache reconstruction
|
|
57
|
+
const parsed = JSON.parse(cachedJSON);
|
|
58
|
+
const reconstructed = (0, mock_sequelize_1.createMockSequelizeModel)(parsed);
|
|
59
|
+
// Assert
|
|
60
|
+
expect(reconstructed).toHaveProperty('sequelize');
|
|
61
|
+
expect(typeof reconstructed.$get).toBe('function');
|
|
62
|
+
expect(reconstructed.id).toBe(1);
|
|
63
|
+
expect(reconstructed.name).toBe('Test');
|
|
64
|
+
});
|
|
65
|
+
it('Cache MISS: DB instance stored in cache as JSON string', async () => {
|
|
66
|
+
// Arrange
|
|
67
|
+
const dbInstance = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 1, name: 'Test' });
|
|
68
|
+
mockRepository.findByPk = jest.fn().mockResolvedValue(dbInstance);
|
|
69
|
+
mockCacheManager.get = jest.fn().mockResolvedValue(null); // Cache miss
|
|
70
|
+
mockCacheManager.set = jest.fn();
|
|
71
|
+
// Act
|
|
72
|
+
const cachedValue = JSON.stringify(dbInstance.get({ plain: true }));
|
|
73
|
+
// Assert
|
|
74
|
+
expect(typeof cachedValue).toBe('string');
|
|
75
|
+
expect(() => JSON.parse(cachedValue)).not.toThrow();
|
|
76
|
+
const parsed = JSON.parse(cachedValue);
|
|
77
|
+
expect(parsed).toHaveProperty('id', 1);
|
|
78
|
+
expect(parsed).toHaveProperty('name', 'Test');
|
|
79
|
+
expect(parsed).not.toHaveProperty('sequelize'); // sequelize should be removed
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('🟡 buscarTodos() - Find All with Pagination (CRITICAL)', () => {
|
|
83
|
+
it('MUST return array of Sequelize Model instances', async () => {
|
|
84
|
+
// Arrange
|
|
85
|
+
const mockInstances = (0, mock_sequelize_1.createMockInstanceArray)(3);
|
|
86
|
+
mockRepository.findAll = jest.fn().mockResolvedValue(mockInstances);
|
|
87
|
+
mockCacheManager.get = jest.fn().mockResolvedValue(null);
|
|
88
|
+
// Act
|
|
89
|
+
const result = { items: mockInstances, count: 3 };
|
|
90
|
+
// Assert
|
|
91
|
+
expect(Array.isArray(result.items)).toBe(true);
|
|
92
|
+
result.items.forEach(item => {
|
|
93
|
+
expect(item).toHaveProperty('sequelize');
|
|
94
|
+
expect(typeof item.$get).toBe('function');
|
|
95
|
+
expect(item.id).toBeDefined();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
it('Each item MUST be callable with .$get() for relations', async () => {
|
|
99
|
+
// Arrange
|
|
100
|
+
const mockInstances = (0, mock_sequelize_1.createMockInstanceArray)(2);
|
|
101
|
+
mockInstances[0].$get = jest.fn().mockResolvedValue([
|
|
102
|
+
(0, mock_sequelize_1.createMockSequelizeModel)({ id: 100 }),
|
|
103
|
+
]);
|
|
104
|
+
// Act
|
|
105
|
+
const relatedData = await mockInstances[0].$get('relatedEntity');
|
|
106
|
+
// Assert
|
|
107
|
+
expect(mockInstances[0].$get).toHaveBeenCalledWith('relatedEntity');
|
|
108
|
+
expect(Array.isArray(relatedData)).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
it('3-Level Cache: ID list stored separately from items', async () => {
|
|
111
|
+
// Arrange
|
|
112
|
+
const mockInstances = (0, mock_sequelize_1.createMockInstanceArray)(2);
|
|
113
|
+
mockRepository.findAll = jest.fn().mockResolvedValue(mockInstances);
|
|
114
|
+
mockCacheManager.hget = jest.fn();
|
|
115
|
+
mockCacheManager.hset = jest.fn();
|
|
116
|
+
// Act
|
|
117
|
+
const ids = mockInstances.map(i => i.id);
|
|
118
|
+
// Assert - Should have multiple cache levels
|
|
119
|
+
expect(ids.length).toBe(2);
|
|
120
|
+
expect(Array.isArray(ids)).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
it('Cache retrieve: All items reconstructed as INSTANCES from JSON', async () => {
|
|
123
|
+
// Arrange
|
|
124
|
+
const itemsJSON = [
|
|
125
|
+
'{"id":1,"name":"Item1"}',
|
|
126
|
+
'{"id":2,"name":"Item2"}',
|
|
127
|
+
];
|
|
128
|
+
// Act
|
|
129
|
+
const reconstructed = itemsJSON.map(json => (0, mock_sequelize_1.createMockSequelizeModel)(JSON.parse(json)));
|
|
130
|
+
// Assert
|
|
131
|
+
reconstructed.forEach(item => {
|
|
132
|
+
expect(typeof item.$get).toBe('function');
|
|
133
|
+
expect(item.sequelize).toBeDefined();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
describe('🟢 crear() - Create with Cache Invalidation', () => {
|
|
138
|
+
it('MUST return created instance (not JSON)', async () => {
|
|
139
|
+
// Arrange
|
|
140
|
+
const newData = { name: 'New User', email: 'new@example.com' };
|
|
141
|
+
const createdInstance = (0, mock_sequelize_1.createMockSequelizeModel)({
|
|
142
|
+
id: 99,
|
|
143
|
+
...newData,
|
|
144
|
+
});
|
|
145
|
+
mockRepository.create = jest.fn().mockResolvedValue(createdInstance);
|
|
146
|
+
// Act
|
|
147
|
+
const result = createdInstance;
|
|
148
|
+
// Assert
|
|
149
|
+
expect(result).toHaveProperty('sequelize');
|
|
150
|
+
expect(typeof result.$get).toBe('function');
|
|
151
|
+
expect(result.id).toBe(99);
|
|
152
|
+
});
|
|
153
|
+
it('MUST invalidate cache for list operations (buscarTodos, contar)', async () => {
|
|
154
|
+
// Arrange
|
|
155
|
+
mockCacheManager.del = jest.fn().mockResolvedValue(1);
|
|
156
|
+
// Act
|
|
157
|
+
const keysToInvalidate = ['usuario:buscarTodos:*', 'usuario:contar:*'];
|
|
158
|
+
keysToInvalidate.forEach(key => mockCacheManager.del(key));
|
|
159
|
+
// Assert
|
|
160
|
+
expect(mockCacheManager.del).toHaveBeenCalledTimes(2);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
describe('🟢 actualizar() - Update with Cache Invalidation', () => {
|
|
164
|
+
it('MUST return updated instance', async () => {
|
|
165
|
+
// Arrange
|
|
166
|
+
const updateData = { name: 'Updated Name' };
|
|
167
|
+
const updatedInstance = (0, mock_sequelize_1.createMockSequelizeModel)({
|
|
168
|
+
id: 1,
|
|
169
|
+
name: 'Updated Name',
|
|
170
|
+
});
|
|
171
|
+
mockRepository.update = jest.fn().mockResolvedValue([1]);
|
|
172
|
+
// Act
|
|
173
|
+
const result = updatedInstance;
|
|
174
|
+
// Assert
|
|
175
|
+
expect(result).toHaveProperty('sequelize');
|
|
176
|
+
expect(result.name).toBe('Updated Name');
|
|
177
|
+
});
|
|
178
|
+
it('MUST invalidate entity cache', async () => {
|
|
179
|
+
// Arrange
|
|
180
|
+
mockCacheManager.del = jest.fn().mockResolvedValue(1);
|
|
181
|
+
// Act
|
|
182
|
+
mockCacheManager.del('usuario:1'); // Invalidate specific entity
|
|
183
|
+
// Assert
|
|
184
|
+
expect(mockCacheManager.del).toHaveBeenCalledWith('usuario:1');
|
|
185
|
+
});
|
|
186
|
+
it('MUST invalidate list cache', async () => {
|
|
187
|
+
// Arrange
|
|
188
|
+
mockCacheManager.del = jest.fn().mockResolvedValue(1);
|
|
189
|
+
// Act
|
|
190
|
+
mockCacheManager.del('usuario:list:*');
|
|
191
|
+
// Assert
|
|
192
|
+
expect(mockCacheManager.del).toHaveBeenCalledWith('usuario:list:*');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
describe('⚠️ Instance ↔ JSON Lifecycle', () => {
|
|
196
|
+
it('Database returns: Sequelize Instance', () => {
|
|
197
|
+
// Arrange
|
|
198
|
+
const dbResult = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 1, name: 'Test' });
|
|
199
|
+
// Assert
|
|
200
|
+
expect(dbResult).toHaveProperty('sequelize');
|
|
201
|
+
expect(typeof dbResult.get).toBe('function');
|
|
202
|
+
});
|
|
203
|
+
it('Cache stores: JSON string only', () => {
|
|
204
|
+
// Arrange
|
|
205
|
+
const instance = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 1, name: 'Test' });
|
|
206
|
+
const plainData = instance.get({ plain: true });
|
|
207
|
+
// Act
|
|
208
|
+
const cached = JSON.stringify(plainData);
|
|
209
|
+
// Assert
|
|
210
|
+
expect(typeof cached).toBe('string');
|
|
211
|
+
expect(() => JSON.parse(cached)).not.toThrow();
|
|
212
|
+
expect(cached).not.toContain('sequelize');
|
|
213
|
+
});
|
|
214
|
+
it('Cache hit: JSON reconstructed as Instance', () => {
|
|
215
|
+
// Arrange
|
|
216
|
+
const cachedJSON = '{"id":1,"name":"Test"}';
|
|
217
|
+
// Act
|
|
218
|
+
const reconstructed = (0, mock_sequelize_1.createMockSequelizeModel)(JSON.parse(cachedJSON));
|
|
219
|
+
// Assert
|
|
220
|
+
expect(reconstructed).toHaveProperty('sequelize');
|
|
221
|
+
expect(typeof reconstructed.$get).toBe('function');
|
|
222
|
+
});
|
|
223
|
+
it('GraphQL response: Instance serialized to JSON', () => {
|
|
224
|
+
// Arrange
|
|
225
|
+
const instance = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 1, name: 'Test' });
|
|
226
|
+
// Act
|
|
227
|
+
const json = instance.get({ plain: true });
|
|
228
|
+
// Assert
|
|
229
|
+
expect(typeof json).toBe('object');
|
|
230
|
+
expect(json).not.toHaveProperty('sequelize');
|
|
231
|
+
expect(json.id).toBe(1);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
describe('🔍 Error Cases - What NOT to do', () => {
|
|
235
|
+
it('Service MUST NOT return plain JSON from cache without reconstruction', () => {
|
|
236
|
+
// This is a FAILURE case - service that returns JSON directly
|
|
237
|
+
const badCacheHit = '{"id":1,"name":"Test"}';
|
|
238
|
+
// If service returns this directly (WRONG):
|
|
239
|
+
const wrongResult = badCacheHit;
|
|
240
|
+
// This would fail ResolveField that expects instance:
|
|
241
|
+
expect(typeof wrongResult.$get).not.toBe('function');
|
|
242
|
+
expect(wrongResult).not.toHaveProperty('sequelize');
|
|
243
|
+
});
|
|
244
|
+
it('ResolveField MUST fail if @Root() receives plain JSON instead of instance', async () => {
|
|
245
|
+
// Arrange
|
|
246
|
+
const plainJSON = { id: 1, name: 'Test' };
|
|
247
|
+
// Act - Try to call .$get() on plain JSON
|
|
248
|
+
const hasMethod = typeof plainJSON.$get === 'function';
|
|
249
|
+
// Assert
|
|
250
|
+
expect(hasMethod).toBe(false); // WOULD FAIL IN PRODUCTION
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|