@obisey/nest 0.1.35 → 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 -75
- 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
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@obisey/nest",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
4
4
|
"description": "NestJS utilities and base classes by Obisey",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc -p tsconfig.json",
|
|
9
9
|
"copy:assets": "cp package.json README.md dist/",
|
|
10
|
-
"publish:dist": "npm run build && npm run copy:assets && cd dist && npm publish --access public"
|
|
10
|
+
"publish:dist": "npm run build && npm run copy:assets && cd dist && npm publish --access public",
|
|
11
|
+
"test": "jest",
|
|
12
|
+
"test:watch": "jest --watch",
|
|
13
|
+
"test:coverage": "jest --coverage"
|
|
11
14
|
},
|
|
12
15
|
"keywords": [
|
|
13
16
|
"nestjs",
|
|
@@ -27,23 +30,28 @@
|
|
|
27
30
|
"sequelize-typescript": "^2.1.6"
|
|
28
31
|
},
|
|
29
32
|
"devDependencies": {
|
|
33
|
+
"@jest/globals": "^29.7.0",
|
|
30
34
|
"@nestjs/axios": "^4.0.0",
|
|
31
35
|
"@nestjs/cache-manager": "^3.0.1",
|
|
32
36
|
"@nestjs/common": "^11.1.3",
|
|
33
37
|
"@nestjs/core": "^11.1.3",
|
|
34
38
|
"@nestjs/graphql": "^13.1.0",
|
|
39
|
+
"@types/jest": "^29.5.8",
|
|
35
40
|
"@types/sequelize": "^4.28.20",
|
|
36
41
|
"cache-manager": "^7.0.0",
|
|
37
42
|
"cache-manager-redis-store": "^3.0.1",
|
|
38
43
|
"express": "^5.0.0",
|
|
39
44
|
"graphql": "^16.0.0",
|
|
40
45
|
"ioredis": "^5.6.1",
|
|
46
|
+
"jest": "^29.7.0",
|
|
41
47
|
"reflect-metadata": "^0.2.2",
|
|
42
48
|
"sequelize": "^6.37.7",
|
|
43
49
|
"sequelize-typescript": "^2.1.6",
|
|
50
|
+
"ts-jest": "^29.1.1",
|
|
44
51
|
"typescript": "^5.8.3"
|
|
45
52
|
},
|
|
46
53
|
"dependencies": {
|
|
47
|
-
"dayjs": "^1.11.19"
|
|
54
|
+
"dayjs": "^1.11.19",
|
|
55
|
+
"sqlite3": "^5.1.7"
|
|
48
56
|
}
|
|
49
57
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* BaseResolver E2E Tests - Real Sequelize Models & @ResolveField
|
|
4
|
+
*
|
|
5
|
+
* ✅ Uses REAL Sequelize instances
|
|
6
|
+
* ✅ Simulates @ResolveField receiving instance
|
|
7
|
+
* ✅ Validates that instance methods work
|
|
8
|
+
* ✅ Tests complete relation flow
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
const sequelize_1 = require("sequelize");
|
|
12
|
+
const baseResolver_1 = require("../baseResolver");
|
|
13
|
+
const mock_sequelize_1 = require("./test-helpers/mock-sequelize");
|
|
14
|
+
describe('BaseResolver E2E - Real Sequelize Models', () => {
|
|
15
|
+
let sequelize;
|
|
16
|
+
let Usuario;
|
|
17
|
+
let Conversacion;
|
|
18
|
+
let resolver;
|
|
19
|
+
let mockRedis;
|
|
20
|
+
let mockCacheManager;
|
|
21
|
+
beforeAll(async () => {
|
|
22
|
+
// Create real in-memory SQLite database
|
|
23
|
+
sequelize = new sequelize_1.Sequelize('sqlite::memory:', {
|
|
24
|
+
logging: false,
|
|
25
|
+
define: {
|
|
26
|
+
timestamps: true,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
// Define REAL Sequelize models
|
|
30
|
+
Usuario = sequelize.define('Usuario', {
|
|
31
|
+
id: {
|
|
32
|
+
type: sequelize_1.DataTypes.INTEGER,
|
|
33
|
+
primaryKey: true,
|
|
34
|
+
autoIncrement: true,
|
|
35
|
+
},
|
|
36
|
+
nombre: sequelize_1.DataTypes.STRING,
|
|
37
|
+
email: sequelize_1.DataTypes.STRING,
|
|
38
|
+
});
|
|
39
|
+
Conversacion = sequelize.define('Conversacion', {
|
|
40
|
+
id: {
|
|
41
|
+
type: sequelize_1.DataTypes.INTEGER,
|
|
42
|
+
primaryKey: true,
|
|
43
|
+
autoIncrement: true,
|
|
44
|
+
},
|
|
45
|
+
titulo: sequelize_1.DataTypes.STRING,
|
|
46
|
+
texto: sequelize_1.DataTypes.TEXT,
|
|
47
|
+
});
|
|
48
|
+
// Through table for many-to-many
|
|
49
|
+
const UsuarioConversacion = sequelize.define('UsuarioConversacion', {
|
|
50
|
+
id: {
|
|
51
|
+
type: sequelize_1.DataTypes.INTEGER,
|
|
52
|
+
primaryKey: true,
|
|
53
|
+
autoIncrement: true,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
// Define relations
|
|
57
|
+
Usuario.belongsToMany(Conversacion, {
|
|
58
|
+
through: UsuarioConversacion,
|
|
59
|
+
as: 'conversaciones',
|
|
60
|
+
foreignKey: 'usuarioId',
|
|
61
|
+
});
|
|
62
|
+
Conversacion.belongsToMany(Usuario, {
|
|
63
|
+
through: UsuarioConversacion,
|
|
64
|
+
as: 'usuarios',
|
|
65
|
+
foreignKey: 'conversacionId',
|
|
66
|
+
});
|
|
67
|
+
// Sync database
|
|
68
|
+
await sequelize.sync({ force: true });
|
|
69
|
+
// Create test data
|
|
70
|
+
const usuario = await Usuario.create({ id: 1, nombre: 'Juan', email: 'juan@test.com' });
|
|
71
|
+
const conv1 = await Conversacion.create({ id: 1, titulo: 'Conversacion 1', texto: 'Texto 1' });
|
|
72
|
+
const conv2 = await Conversacion.create({ id: 2, titulo: 'Conversacion 2', texto: 'Texto 2' });
|
|
73
|
+
await usuario.addConversaciones([conv1, conv2]);
|
|
74
|
+
// Setup mocks
|
|
75
|
+
mockRedis = (0, mock_sequelize_1.createMockRedisClient)();
|
|
76
|
+
mockCacheManager = (0, mock_sequelize_1.createMockCacheManager)();
|
|
77
|
+
// Create BaseResolver
|
|
78
|
+
const BaseResolverClass = (0, baseResolver_1.BaseResolver)('usuario');
|
|
79
|
+
resolver = new BaseResolverClass(mockRedis, mockCacheManager);
|
|
80
|
+
});
|
|
81
|
+
afterAll(async () => {
|
|
82
|
+
await sequelize.close();
|
|
83
|
+
});
|
|
84
|
+
describe('🔴 @ResolveField Receives REAL Sequelize Instance', () => {
|
|
85
|
+
it('@ResolveField receives REAL instance from service', async () => {
|
|
86
|
+
// Arrange - Get a REAL instance from database (simulating service return)
|
|
87
|
+
const usuarioInstance = await Usuario.findByPk(1);
|
|
88
|
+
// Assert - Instance MUST be real Sequelize instance
|
|
89
|
+
expect(usuarioInstance).toBeInstanceOf(sequelize_1.Model);
|
|
90
|
+
expect(usuarioInstance.get('nombre')).toBe('Juan');
|
|
91
|
+
// CRITICAL: Instance type differs from plain JSON
|
|
92
|
+
// Has Sequelize methods
|
|
93
|
+
expect(typeof usuarioInstance.get).toBe('function');
|
|
94
|
+
expect(typeof usuarioInstance.toJSON).toBe('function');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe('🔴 BaseResolver Methods', () => {
|
|
98
|
+
it('Resolver returns functions that accept instances', () => {
|
|
99
|
+
// Act - Get resolver methods
|
|
100
|
+
const hasManyResolver = resolver.hasManyBelongsToMany('test', 'conversaciones', 1, null);
|
|
101
|
+
// Assert - Methods exist
|
|
102
|
+
expect(typeof hasManyResolver.resolvefield).toBe('function');
|
|
103
|
+
expect(typeof hasManyResolver.ligar).toBe('function');
|
|
104
|
+
expect(typeof hasManyResolver.desligar).toBe('function');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe('🔴 REAL Instance vs Plain JSON', () => {
|
|
108
|
+
it('REAL instance has Sequelize methods', async () => {
|
|
109
|
+
// Arrange - REAL instance from DB
|
|
110
|
+
const usuario = await Usuario.findByPk(1);
|
|
111
|
+
// Assert - REAL instance has methods
|
|
112
|
+
expect(usuario).toBeInstanceOf(sequelize_1.Model);
|
|
113
|
+
expect(typeof usuario.get).toBe('function');
|
|
114
|
+
expect(typeof usuario.save).toBe('function');
|
|
115
|
+
expect(typeof usuario.destroy).toBe('function');
|
|
116
|
+
expect(typeof usuario.toJSON).toBe('function');
|
|
117
|
+
});
|
|
118
|
+
it('Plain JSON CANNOT call Sequelize methods', () => {
|
|
119
|
+
// This is what FAILS if service returns JSON instead of instance
|
|
120
|
+
const plainJSON = { id: 1, nombre: 'Juan' };
|
|
121
|
+
// CRITICAL FAILURE POINTS:
|
|
122
|
+
// These methods DON'T EXIST on plain JSON:
|
|
123
|
+
expect(typeof plainJSON.get).not.toBe('function');
|
|
124
|
+
expect(typeof plainJSON.save).not.toBe('function');
|
|
125
|
+
expect(typeof plainJSON.destroy).not.toBe('function');
|
|
126
|
+
// If @ResolveField calls item.$get('relacion'):
|
|
127
|
+
expect(typeof plainJSON.$get).not.toBe('function');
|
|
128
|
+
// → TypeError: plainJSON.$get is not a function
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe('🔴 uno() & muchos() with Real Instances', () => {
|
|
132
|
+
it('REAL instances have required Sequelize methods', async () => {
|
|
133
|
+
// Arrange
|
|
134
|
+
const usuario = await Usuario.findByPk(1);
|
|
135
|
+
// Assert - REAL instance has methods
|
|
136
|
+
expect(usuario).toBeInstanceOf(sequelize_1.Model);
|
|
137
|
+
expect(typeof usuario.get).toBe('function');
|
|
138
|
+
expect(typeof usuario.save).toBe('function');
|
|
139
|
+
expect(typeof usuario.destroy).toBe('function');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
describe('⚠️ Data Flow Validation', () => {
|
|
143
|
+
it('REAL Instance ↔ JSON ↔ Instance cycle works', async () => {
|
|
144
|
+
// Step 1: Get REAL instance from DB
|
|
145
|
+
const originalInstance = await Usuario.findByPk(1);
|
|
146
|
+
expect(originalInstance).toBeInstanceOf(sequelize_1.Model);
|
|
147
|
+
// Step 2: Convert to JSON for caching
|
|
148
|
+
const json = originalInstance.get({ plain: true });
|
|
149
|
+
expect(typeof json).toBe('object');
|
|
150
|
+
expect(json.id).toBe(1);
|
|
151
|
+
// Step 3: Stringify for Redis
|
|
152
|
+
const jsonString = JSON.stringify(json);
|
|
153
|
+
expect(typeof jsonString).toBe('string');
|
|
154
|
+
// Step 4: Reconstruct from JSON
|
|
155
|
+
const reconstructed = Usuario.build(JSON.parse(jsonString));
|
|
156
|
+
expect(reconstructed).toBeInstanceOf(sequelize_1.Model);
|
|
157
|
+
expect(reconstructed.get('nombre')).toBe('Juan');
|
|
158
|
+
// CRITICAL: Instance has methods (different from plain JSON)
|
|
159
|
+
expect(typeof reconstructed.get).toBe('function');
|
|
160
|
+
expect(typeof reconstructed.save).toBe('function');
|
|
161
|
+
});
|
|
162
|
+
it('Instance is REQUIRED for @ResolveField to work', async () => {
|
|
163
|
+
// REAL INSTANCE - Has methods ✅
|
|
164
|
+
const realInstance = await Usuario.findByPk(1);
|
|
165
|
+
expect(realInstance).toBeInstanceOf(sequelize_1.Model);
|
|
166
|
+
expect(typeof realInstance.get).toBe('function');
|
|
167
|
+
// PLAIN JSON - NO methods ❌
|
|
168
|
+
const plainJSON = { id: 1, nombre: 'Juan' };
|
|
169
|
+
expect(typeof plainJSON.get).not.toBe('function');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* BaseResolver Integration Tests
|
|
4
|
+
*
|
|
5
|
+
* ✅ Tests the ACTUAL code from baseResolver.ts
|
|
6
|
+
* ✅ Not mocks - tests real BaseResolver with real instances
|
|
7
|
+
* ✅ Validates that @ResolveField receives instances
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
const baseResolver_1 = require("../baseResolver");
|
|
11
|
+
const mock_sequelize_1 = require("./test-helpers/mock-sequelize");
|
|
12
|
+
describe('BaseResolver - Integration Tests (REAL CODE)', () => {
|
|
13
|
+
let mockRedis;
|
|
14
|
+
let mockCacheManager;
|
|
15
|
+
let resolver;
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mockRedis = (0, mock_sequelize_1.createMockRedisClient)();
|
|
18
|
+
mockCacheManager = (0, mock_sequelize_1.createMockCacheManager)();
|
|
19
|
+
// Create actual BaseResolver class from factory
|
|
20
|
+
const BaseResolverClass = (0, baseResolver_1.BaseResolver)('usuario');
|
|
21
|
+
// Instantiate with real dependencies
|
|
22
|
+
resolver = new BaseResolverClass(mockRedis, mockCacheManager);
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
describe('🔴 belongsTo() - Real Implementation', () => {
|
|
26
|
+
it('belongsTo() returns resolver with required methods', () => {
|
|
27
|
+
// Act - Create real belongsTo resolver
|
|
28
|
+
const belongsToResolver = resolver.belongsTo('test', // universo
|
|
29
|
+
'empresa', // planeta
|
|
30
|
+
1, // IdEmpresa
|
|
31
|
+
null // pagina
|
|
32
|
+
);
|
|
33
|
+
// Assert - Must have all resolver methods
|
|
34
|
+
expect(typeof belongsToResolver.resolvefield).toBe('function');
|
|
35
|
+
expect(typeof belongsToResolver.query).toBe('function');
|
|
36
|
+
expect(typeof belongsToResolver.ligar).toBe('function');
|
|
37
|
+
expect(typeof belongsToResolver.desligar).toBe('function');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('🔴 @ResolveField Instance Requirement', () => {
|
|
41
|
+
it('Instance with .$get() can load related data', () => {
|
|
42
|
+
// Arrange - Simulate what happens in @ResolveField
|
|
43
|
+
const parentInstance = {
|
|
44
|
+
id: 1,
|
|
45
|
+
userId: 100,
|
|
46
|
+
sequelize: { models: {} },
|
|
47
|
+
// CRITICAL: Must have .$get() method
|
|
48
|
+
$get: jest.fn().mockResolvedValue([
|
|
49
|
+
{ id: 10, text: 'Message 1', sequelize: { models: {} } },
|
|
50
|
+
{ id: 11, text: 'Message 2', sequelize: { models: {} } }
|
|
51
|
+
])
|
|
52
|
+
};
|
|
53
|
+
// Assert - Instance has required method
|
|
54
|
+
expect(typeof parentInstance.$get).toBe('function');
|
|
55
|
+
expect(parentInstance).toHaveProperty('sequelize');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('🔴 hasMany().resolvefield() - Real Implementation', () => {
|
|
59
|
+
it('hasManyBelongsToMany returns resolvefield function', () => {
|
|
60
|
+
// Arrange & Act
|
|
61
|
+
const hasMany = resolver.hasManyBelongsToMany('test', 'items', 1, null);
|
|
62
|
+
// Assert - Should have resolvefield method
|
|
63
|
+
expect(typeof hasMany.resolvefield).toBe('function');
|
|
64
|
+
expect(typeof hasMany.ligar).toBe('function');
|
|
65
|
+
expect(typeof hasMany.desligar).toBe('function');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('🟢 ligar().mutation() - Real Implementation', () => {
|
|
69
|
+
it('MUST return resolver functions that can be called', async () => {
|
|
70
|
+
// Arrange
|
|
71
|
+
const belongsToResolver = resolver.belongsTo('test', 'empresa', 1, null);
|
|
72
|
+
// Act & Assert - Check that resolver provides all expected functions
|
|
73
|
+
expect(typeof belongsToResolver.resolvefield).toBe('function');
|
|
74
|
+
expect(typeof belongsToResolver.query).toBe('function');
|
|
75
|
+
expect(typeof belongsToResolver.ligar).toBe('function');
|
|
76
|
+
expect(typeof belongsToResolver.desligar).toBe('function');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe('⚠️ Critical: Instance ↔ Relations', () => {
|
|
80
|
+
it('Instance from cache MUST support .$get() calls', async () => {
|
|
81
|
+
// Arrange - Simulate cache reconstruction
|
|
82
|
+
const cachedJSON = '{"id":1,"name":"Cached"}';
|
|
83
|
+
const SequelizeModel = function (data) {
|
|
84
|
+
Object.assign(this, data);
|
|
85
|
+
this.sequelize = { models: {} };
|
|
86
|
+
this.$get = jest.fn().mockResolvedValue([]);
|
|
87
|
+
};
|
|
88
|
+
// Simulate: new Model(JSON.parse(cached))
|
|
89
|
+
const reconstructedInstance = new SequelizeModel(JSON.parse(cachedJSON));
|
|
90
|
+
// Act - Should be able to call .$get()
|
|
91
|
+
const result = await reconstructedInstance.$get('relation');
|
|
92
|
+
// Assert
|
|
93
|
+
expect(reconstructedInstance.$get).toHaveBeenCalledWith('relation');
|
|
94
|
+
expect(Array.isArray(result)).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
it('Plain JSON CANNOT call .$get()', () => {
|
|
97
|
+
// Arrange - What happens if service returns plain JSON (WRONG)
|
|
98
|
+
const plainJSON = { id: 1, name: 'Test' };
|
|
99
|
+
// Act - Try to call .$get() on plain JSON
|
|
100
|
+
const canCall = typeof plainJSON.$get === 'function';
|
|
101
|
+
// Assert - SHOULD FAIL
|
|
102
|
+
expect(canCall).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseResolver 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️⃣ @ResolveField @Root() MUST receive Sequelize Model instance
|
|
8
|
+
* 2️⃣ uno() and muchos() MUST return instances (not JSON)
|
|
9
|
+
* 3️⃣ Calling item.$get('relation') MUST work on @Root() item
|
|
10
|
+
* 4️⃣ Cache for relations must reconstruct instances
|
|
11
|
+
* 5️⃣ ligar() and desligar() must work with instances
|
|
12
|
+
* 6️⃣ Relation mutations must invalidate cache
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* BaseResolver 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️⃣ @ResolveField @Root() MUST receive Sequelize Model instance
|
|
9
|
+
* 2️⃣ uno() and muchos() MUST return instances (not JSON)
|
|
10
|
+
* 3️⃣ Calling item.$get('relation') MUST work on @Root() item
|
|
11
|
+
* 4️⃣ Cache for relations must reconstruct instances
|
|
12
|
+
* 5️⃣ ligar() and desligar() must work with instances
|
|
13
|
+
* 6️⃣ Relation mutations must invalidate cache
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
const mock_sequelize_1 = require("./test-helpers/mock-sequelize");
|
|
17
|
+
describe('BaseResolver - ResolveField Design Validation', () => {
|
|
18
|
+
let mockRedis;
|
|
19
|
+
let mockCacheManager;
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
mockRedis = (0, mock_sequelize_1.createMockRedisClient)();
|
|
22
|
+
mockCacheManager = (0, mock_sequelize_1.createMockCacheManager)();
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
describe('🔴 @ResolveField @Root() - CRITICAL PRECONDITION', () => {
|
|
26
|
+
it('@Root() MUST receive Sequelize Model instance (precondition for $get)', async () => {
|
|
27
|
+
// Arrange
|
|
28
|
+
const parentInstance = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 1, userId: 1 });
|
|
29
|
+
// Assert
|
|
30
|
+
expect(parentInstance).toHaveProperty('sequelize');
|
|
31
|
+
expect(typeof parentInstance.$get).toBe('function');
|
|
32
|
+
});
|
|
33
|
+
it('@Root() MUST have all Sequelize methods available', () => {
|
|
34
|
+
// Arrange
|
|
35
|
+
const parentInstance = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 1 });
|
|
36
|
+
// Assert - All required methods must exist
|
|
37
|
+
expect(typeof parentInstance.$get).toBe('function');
|
|
38
|
+
expect(typeof parentInstance.$set).toBe('function');
|
|
39
|
+
expect(typeof parentInstance.$add).toBe('function');
|
|
40
|
+
expect(typeof parentInstance.$remove).toBe('function');
|
|
41
|
+
expect(typeof parentInstance.get).toBe('function');
|
|
42
|
+
});
|
|
43
|
+
it('FAIL CASE: Plain JSON @Root() cannot call .$get()', () => {
|
|
44
|
+
// Arrange
|
|
45
|
+
const plainJSON = { id: 1, userId: 1 };
|
|
46
|
+
// Assert - WOULD FAIL IN PRODUCTION
|
|
47
|
+
expect(typeof plainJSON.$get).not.toBe('function');
|
|
48
|
+
expect(plainJSON).not.toHaveProperty('sequelize');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe('🟡 muchos() - Load Multiple Related Items', () => {
|
|
52
|
+
it('MUST call item.$get(relationName) on instance', async () => {
|
|
53
|
+
// Arrange
|
|
54
|
+
const parentInstance = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 1 });
|
|
55
|
+
const relatedInstances = (0, mock_sequelize_1.createMockInstanceArray)(3);
|
|
56
|
+
parentInstance.$get = jest.fn().mockResolvedValue(relatedInstances);
|
|
57
|
+
// Act
|
|
58
|
+
const result = await parentInstance.$get('conversaciones');
|
|
59
|
+
// Assert
|
|
60
|
+
expect(parentInstance.$get).toHaveBeenCalledWith('conversaciones');
|
|
61
|
+
expect(Array.isArray(result)).toBe(true);
|
|
62
|
+
expect(result.length).toBe(3);
|
|
63
|
+
});
|
|
64
|
+
it('MUST return array of Sequelize instances', async () => {
|
|
65
|
+
// Arrange
|
|
66
|
+
const parentInstance = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 1 });
|
|
67
|
+
const relatedInstances = (0, mock_sequelize_1.createMockInstanceArray)(2);
|
|
68
|
+
parentInstance.$get = jest.fn().mockResolvedValue(relatedInstances);
|
|
69
|
+
// Act
|
|
70
|
+
const result = await parentInstance.$get('conversaciones');
|
|
71
|
+
// Assert
|
|
72
|
+
result.forEach(item => {
|
|
73
|
+
expect(item).toHaveProperty('sequelize');
|
|
74
|
+
expect(typeof item.$get).toBe('function');
|
|
75
|
+
expect(item.id).toBeDefined();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
it('Each returned item MUST be callable with .$get() for nested relations', async () => {
|
|
79
|
+
// Arrange
|
|
80
|
+
const parentInstance = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 1 });
|
|
81
|
+
const conversacion = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 10 });
|
|
82
|
+
conversacion.$get = jest.fn().mockResolvedValue([
|
|
83
|
+
(0, mock_sequelize_1.createMockSequelizeModel)({ id: 100 }),
|
|
84
|
+
]);
|
|
85
|
+
parentInstance.$get = jest.fn().mockResolvedValue([conversacion]);
|
|
86
|
+
// Act
|
|
87
|
+
const conversaciones = await parentInstance.$get('conversaciones');
|
|
88
|
+
const mensajes = await conversaciones[0].$get('mensajes');
|
|
89
|
+
// Assert
|
|
90
|
+
expect(conversaciones[0].$get).toHaveBeenCalledWith('mensajes');
|
|
91
|
+
expect(Array.isArray(mensajes)).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe('🟡 uno() - Load Single Related Item', () => {
|
|
95
|
+
it('MUST call item.$get(relationName) on instance', async () => {
|
|
96
|
+
// Arrange
|
|
97
|
+
const parentInstance = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 1, empresaId: 5 });
|
|
98
|
+
const relatedInstance = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 5, nombre: 'Empresa A' });
|
|
99
|
+
parentInstance.$get = jest.fn().mockResolvedValue(relatedInstance);
|
|
100
|
+
// Act
|
|
101
|
+
const result = await parentInstance.$get('empresa');
|
|
102
|
+
// Assert
|
|
103
|
+
expect(parentInstance.$get).toHaveBeenCalledWith('empresa');
|
|
104
|
+
expect(result).toHaveProperty('sequelize');
|
|
105
|
+
expect(result.id).toBe(5);
|
|
106
|
+
});
|
|
107
|
+
it('MUST return single Sequelize instance (not JSON)', async () => {
|
|
108
|
+
// Arrange
|
|
109
|
+
const parentInstance = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 1 });
|
|
110
|
+
const relatedInstance = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 5 });
|
|
111
|
+
parentInstance.$get = jest.fn().mockResolvedValue(relatedInstance);
|
|
112
|
+
// Act
|
|
113
|
+
const result = await parentInstance.$get('empresa');
|
|
114
|
+
// Assert
|
|
115
|
+
expect(result).toHaveProperty('sequelize');
|
|
116
|
+
expect(typeof result.$get).toBe('function');
|
|
117
|
+
expect(result.constructor.name).toBe('SequelizeModel');
|
|
118
|
+
});
|
|
119
|
+
it('Returned instance MUST support further relations', async () => {
|
|
120
|
+
// Arrange
|
|
121
|
+
const parentInstance = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 1 });
|
|
122
|
+
const empresa = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 5 });
|
|
123
|
+
empresa.$get = jest.fn().mockResolvedValue((0, mock_sequelize_1.createMockSequelizeModel)({ id: 100 }));
|
|
124
|
+
parentInstance.$get = jest.fn().mockResolvedValue(empresa);
|
|
125
|
+
// Act
|
|
126
|
+
const empresaResult = await parentInstance.$get('empresa');
|
|
127
|
+
const pais = await empresaResult.$get('pais');
|
|
128
|
+
// Assert
|
|
129
|
+
expect(pais).toHaveProperty('sequelize');
|
|
130
|
+
expect(pais.id).toBe(100);
|
|
131
|
+
});
|
|
132
|
+
it('Can return null for optional relations', async () => {
|
|
133
|
+
// Arrange
|
|
134
|
+
const parentInstance = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 1, empresaId: null });
|
|
135
|
+
parentInstance.$get = jest.fn().mockResolvedValue(null);
|
|
136
|
+
// Act
|
|
137
|
+
const result = await parentInstance.$get('empresa');
|
|
138
|
+
// Assert
|
|
139
|
+
expect(result).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
describe('🟢 ligar() - Add Relation (Mutation)', () => {
|
|
143
|
+
it('MUST call item.$add(relationName, id)', async () => {
|
|
144
|
+
// Arrange
|
|
145
|
+
const parentInstance = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 1 });
|
|
146
|
+
const newRelatedInstance = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 100 });
|
|
147
|
+
parentInstance.$add = jest.fn().mockResolvedValue([newRelatedInstance]);
|
|
148
|
+
// Act
|
|
149
|
+
const result = await parentInstance.$add('conversaciones', 100);
|
|
150
|
+
// Assert
|
|
151
|
+
expect(parentInstance.$add).toHaveBeenCalledWith('conversaciones', 100);
|
|
152
|
+
expect(Array.isArray(result)).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
it('MUST return array of updated related instances', async () => {
|
|
155
|
+
// Arrange
|
|
156
|
+
const parentInstance = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 1 });
|
|
157
|
+
const instances = (0, mock_sequelize_1.createMockInstanceArray)(1);
|
|
158
|
+
parentInstance.$add = jest.fn().mockResolvedValue(instances);
|
|
159
|
+
// Act
|
|
160
|
+
const result = await parentInstance.$add('conversaciones', 100);
|
|
161
|
+
// Assert
|
|
162
|
+
result.forEach(item => {
|
|
163
|
+
expect(item).toHaveProperty('sequelize');
|
|
164
|
+
expect(typeof item.$get).toBe('function');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
it('MUST invalidate cache after adding relation', () => {
|
|
168
|
+
// Arrange
|
|
169
|
+
mockCacheManager.del = jest.fn().mockResolvedValue(1);
|
|
170
|
+
// Act
|
|
171
|
+
mockCacheManager.del('usuario:1:conversaciones:*');
|
|
172
|
+
// Assert
|
|
173
|
+
expect(mockCacheManager.del).toHaveBeenCalledWith('usuario:1:conversaciones:*');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
describe('🟢 desligar() - Remove Relation (Mutation)', () => {
|
|
177
|
+
it('MUST call item.$remove(relationName, id)', async () => {
|
|
178
|
+
// Arrange
|
|
179
|
+
const parentInstance = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 1 });
|
|
180
|
+
parentInstance.$remove = jest.fn().mockResolvedValue(undefined);
|
|
181
|
+
// Act
|
|
182
|
+
await parentInstance.$remove('conversaciones', 100);
|
|
183
|
+
// Assert
|
|
184
|
+
expect(parentInstance.$remove).toHaveBeenCalledWith('conversaciones', 100);
|
|
185
|
+
});
|
|
186
|
+
it('MUST invalidate cache after removing relation', () => {
|
|
187
|
+
// Arrange
|
|
188
|
+
mockCacheManager.del = jest.fn().mockResolvedValue(1);
|
|
189
|
+
// Act
|
|
190
|
+
mockCacheManager.del('usuario:1:conversaciones:*');
|
|
191
|
+
// Assert
|
|
192
|
+
expect(mockCacheManager.del).toHaveBeenCalledWith('usuario:1:conversaciones:*');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
describe('🟡 Caching - Relations with Cache', () => {
|
|
196
|
+
it('Cache HIT: JSON reconstructed as INSTANCES array', async () => {
|
|
197
|
+
// Arrange
|
|
198
|
+
const cachedJSON = '[{"id":10,"name":"Conv1"},{"id":11,"name":"Conv2"}]';
|
|
199
|
+
mockCacheManager.get = jest.fn().mockResolvedValue(cachedJSON);
|
|
200
|
+
// Act
|
|
201
|
+
const cached = JSON.parse(cachedJSON);
|
|
202
|
+
const reconstructed = cached.map(item => (0, mock_sequelize_1.createMockSequelizeModel)(item));
|
|
203
|
+
// Assert
|
|
204
|
+
reconstructed.forEach(item => {
|
|
205
|
+
expect(item).toHaveProperty('sequelize');
|
|
206
|
+
expect(typeof item.$get).toBe('function');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
it('Cache HIT: Single related instance reconstructed', async () => {
|
|
210
|
+
// Arrange
|
|
211
|
+
const cachedJSON = '{"id":5,"nombre":"Empresa A"}';
|
|
212
|
+
mockCacheManager.get = jest.fn().mockResolvedValue(cachedJSON);
|
|
213
|
+
// Act
|
|
214
|
+
const reconstructed = (0, mock_sequelize_1.createMockSequelizeModel)(JSON.parse(cachedJSON));
|
|
215
|
+
// Assert
|
|
216
|
+
expect(reconstructed).toHaveProperty('sequelize');
|
|
217
|
+
expect(typeof reconstructed.$get).toBe('function');
|
|
218
|
+
});
|
|
219
|
+
it('Cache MISS: Instances stored as JSON array', () => {
|
|
220
|
+
// Arrange
|
|
221
|
+
const instances = (0, mock_sequelize_1.createMockInstanceArray)(2);
|
|
222
|
+
// Act
|
|
223
|
+
const plainData = instances.map(i => i.get({ plain: true }));
|
|
224
|
+
const cached = JSON.stringify(plainData);
|
|
225
|
+
// Assert
|
|
226
|
+
expect(typeof cached).toBe('string');
|
|
227
|
+
expect(() => JSON.parse(cached)).not.toThrow();
|
|
228
|
+
});
|
|
229
|
+
it('Cache MISS: Single instance stored as JSON', () => {
|
|
230
|
+
// Arrange
|
|
231
|
+
const instance = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 5, nombre: 'Test' });
|
|
232
|
+
// Act
|
|
233
|
+
const plain = instance.get({ plain: true });
|
|
234
|
+
const cached = JSON.stringify(plain);
|
|
235
|
+
// Assert
|
|
236
|
+
expect(typeof cached).toBe('string');
|
|
237
|
+
expect(cached).not.toContain('sequelize');
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
describe('⚠️ Relation Methods - Standard HAS_MANY, BELONGS_TO, HAS_ONE', () => {
|
|
241
|
+
it('HAS_MANY: parentInstance.$get(relation) returns array', async () => {
|
|
242
|
+
// Arrange
|
|
243
|
+
const parent = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 1 });
|
|
244
|
+
const children = (0, mock_sequelize_1.createMockInstanceArray)(3);
|
|
245
|
+
parent.$get = jest.fn().mockResolvedValue(children);
|
|
246
|
+
// Act
|
|
247
|
+
const result = await parent.$get('children');
|
|
248
|
+
// Assert
|
|
249
|
+
expect(Array.isArray(result)).toBe(true);
|
|
250
|
+
expect(result.length).toBe(3);
|
|
251
|
+
result.forEach(item => {
|
|
252
|
+
expect(typeof item.$get).toBe('function');
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
it('BELONGS_TO: instance.$get(relation) returns single instance', async () => {
|
|
256
|
+
// Arrange
|
|
257
|
+
const child = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 1, parentId: 100 });
|
|
258
|
+
const parent = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 100 });
|
|
259
|
+
child.$get = jest.fn().mockResolvedValue(parent);
|
|
260
|
+
// Act
|
|
261
|
+
const result = await child.$get('parent');
|
|
262
|
+
// Assert
|
|
263
|
+
expect(result).toHaveProperty('sequelize');
|
|
264
|
+
expect(result.id).toBe(100);
|
|
265
|
+
});
|
|
266
|
+
it('HAS_ONE: instance.$get(relation) returns single instance', async () => {
|
|
267
|
+
// Arrange
|
|
268
|
+
const parent = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 1 });
|
|
269
|
+
const child = (0, mock_sequelize_1.createMockSequelizeModel)({ id: 100 });
|
|
270
|
+
parent.$get = jest.fn().mockResolvedValue(child);
|
|
271
|
+
// Act
|
|
272
|
+
const result = await parent.$get('profile');
|
|
273
|
+
// Assert
|
|
274
|
+
expect(result).toHaveProperty('sequelize');
|
|
275
|
+
expect(result.id).toBe(100);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
describe('🔍 Error Cases - What NOT to do', () => {
|
|
279
|
+
it('ResolveField FAILS if @Root() receives plain JSON', async () => {
|
|
280
|
+
// Arrange
|
|
281
|
+
const plainJSON = { id: 1, userId: 1 };
|
|
282
|
+
// Act - Try to call .$get()
|
|
283
|
+
const result = typeof plainJSON.$get;
|
|
284
|
+
// Assert - FAILURE
|
|
285
|
+
expect(result).not.toBe('function');
|
|
286
|
+
});
|
|
287
|
+
it('muchos() FAILS if returns plain JSON array instead of instances', async () => {
|
|
288
|
+
// Arrange
|
|
289
|
+
const plainJSONArray = [
|
|
290
|
+
{ id: 10, name: 'Conv1' },
|
|
291
|
+
{ id: 11, name: 'Conv2' },
|
|
292
|
+
];
|
|
293
|
+
// Assert - WOULD FAIL IN PRODUCTION
|
|
294
|
+
plainJSONArray.forEach(item => {
|
|
295
|
+
expect(typeof item.$get).not.toBe('function');
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
it('uno() FAILS if returns plain JSON instead of instance', () => {
|
|
299
|
+
// Arrange
|
|
300
|
+
const plainJSON = { id: 5, nombre: 'Empresa A' };
|
|
301
|
+
// Assert - WOULD FAIL IN PRODUCTION
|
|
302
|
+
expect(typeof plainJSON.$get).not.toBe('function');
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
});
|