@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.
Files changed (38) hide show
  1. package/package.json +11 -3
  2. package/prototipos/__tests__/baseResolver.e2e.spec.d.ts +9 -0
  3. package/prototipos/__tests__/baseResolver.e2e.spec.js +172 -0
  4. package/prototipos/__tests__/baseResolver.integration.spec.d.ts +8 -0
  5. package/prototipos/__tests__/baseResolver.integration.spec.js +105 -0
  6. package/prototipos/__tests__/baseResolver.spec.d.ts +14 -0
  7. package/prototipos/__tests__/baseResolver.spec.js +305 -0
  8. package/prototipos/__tests__/baseService.e2e.simple.spec.d.ts +7 -0
  9. package/prototipos/__tests__/baseService.e2e.simple.spec.js +92 -0
  10. package/prototipos/__tests__/baseService.e2e.validation.spec.d.ts +7 -0
  11. package/prototipos/__tests__/baseService.e2e.validation.spec.js +157 -0
  12. package/prototipos/__tests__/baseService.integration.spec.d.ts +8 -0
  13. package/prototipos/__tests__/baseService.integration.spec.js +115 -0
  14. package/prototipos/__tests__/baseService.spec.d.ts +14 -0
  15. package/prototipos/__tests__/baseService.spec.js +253 -0
  16. package/prototipos/__tests__/cachear-types.spec.d.ts +8 -0
  17. package/prototipos/__tests__/cachear-types.spec.js +79 -0
  18. package/prototipos/__tests__/graphql-flow.e2e.spec.d.ts +9 -0
  19. package/prototipos/__tests__/graphql-flow.e2e.spec.js +270 -0
  20. package/prototipos/__tests__/test-helpers/mock-sequelize.d.ts +83 -0
  21. package/prototipos/__tests__/test-helpers/mock-sequelize.js +138 -0
  22. package/prototipos/baseResolver.d.ts +8 -8
  23. package/prototipos/baseResolver.js +14 -16
  24. package/prototipos/baseService.js +23 -79
  25. package/prototipos/utils/cachear.d.ts +12 -2
  26. package/prototipos/utils/cachear.js +13 -9
  27. package/adapters/index.d.ts +0 -11
  28. package/adapters/index.js +0 -27
  29. package/adapters/sequelize/index.d.ts +0 -5
  30. package/adapters/sequelize/index.js +0 -9
  31. package/adapters/sequelize/property.adapter.d.ts +0 -79
  32. package/adapters/sequelize/property.adapter.js +0 -134
  33. package/prototipos/index.d.ts +0 -9
  34. package/prototipos/index.js +0 -26
  35. package/receptor.d.ts +0 -25
  36. package/receptor.js +0 -153
  37. package/redisStore.d.ts +0 -11
  38. package/redisStore.js +0 -78
package/package.json CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "name": "@obisey/nest",
3
- "version": "0.1.34",
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,9 @@
1
+ /**
2
+ * BaseResolver E2E Tests - Real Sequelize Models & @ResolveField
3
+ *
4
+ * ✅ Uses REAL Sequelize instances
5
+ * ✅ Simulates @ResolveField receiving instance
6
+ * ✅ Validates that instance methods work
7
+ * ✅ Tests complete relation flow
8
+ */
9
+ export {};
@@ -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,8 @@
1
+ /**
2
+ * BaseResolver Integration Tests
3
+ *
4
+ * ✅ Tests the ACTUAL code from baseResolver.ts
5
+ * ✅ Not mocks - tests real BaseResolver with real instances
6
+ * ✅ Validates that @ResolveField receives instances
7
+ */
8
+ export {};
@@ -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
+ });