nodejs-quickstart-structure 1.18.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -4
- package/README.md +2 -1
- package/bin/index.js +93 -92
- package/lib/generator.js +1 -1
- package/lib/modules/caching-setup.js +76 -73
- package/lib/modules/config-files.js +4 -0
- package/lib/modules/kafka-setup.js +249 -191
- package/lib/modules/project-setup.js +1 -0
- package/package.json +13 -2
- package/templates/clean-architecture/js/src/errors/BadRequestError.js +11 -10
- package/templates/clean-architecture/js/src/errors/BadRequestError.spec.js.ejs +22 -21
- package/templates/clean-architecture/js/src/errors/NotFoundError.js +11 -10
- package/templates/clean-architecture/js/src/errors/NotFoundError.spec.js.ejs +22 -21
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +69 -39
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +142 -81
- package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +1 -1
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +156 -75
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs +234 -138
- package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.js.ejs +27 -21
- package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.spec.js.ejs +66 -49
- package/templates/clean-architecture/js/src/interfaces/graphql/typeDefs/user.types.js.ejs +19 -17
- package/templates/clean-architecture/js/src/interfaces/routes/api.js +12 -10
- package/templates/clean-architecture/js/src/usecases/DeleteUser.js +11 -0
- package/templates/clean-architecture/js/src/usecases/DeleteUser.spec.js.ejs +47 -0
- package/templates/clean-architecture/js/src/usecases/UpdateUser.js +11 -0
- package/templates/clean-architecture/js/src/usecases/UpdateUser.spec.js.ejs +48 -0
- package/templates/clean-architecture/js/src/utils/errorMessages.js +14 -0
- package/templates/clean-architecture/ts/src/errors/BadRequestError.spec.ts.ejs +22 -21
- package/templates/clean-architecture/ts/src/errors/BadRequestError.ts +9 -8
- package/templates/clean-architecture/ts/src/errors/NotFoundError.spec.ts.ejs +22 -21
- package/templates/clean-architecture/ts/src/errors/NotFoundError.ts +9 -8
- package/templates/clean-architecture/ts/src/index.ts.ejs +1 -1
- package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +175 -85
- package/templates/clean-architecture/ts/src/infrastructure/repositories/userRepository.ts.ejs +74 -0
- package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +331 -185
- package/templates/clean-architecture/ts/src/interfaces/controllers/userController.ts.ejs +173 -84
- package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.spec.ts.ejs +68 -51
- package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +29 -21
- package/templates/clean-architecture/ts/src/interfaces/graphql/typeDefs/user.types.ts.ejs +17 -15
- package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.ts +13 -11
- package/templates/clean-architecture/ts/src/usecases/deleteUser.spec.ts.ejs +47 -0
- package/templates/clean-architecture/ts/src/usecases/deleteUser.ts +9 -0
- package/templates/clean-architecture/ts/src/usecases/updateUser.spec.ts.ejs +48 -0
- package/templates/clean-architecture/ts/src/usecases/updateUser.ts +9 -0
- package/templates/clean-architecture/ts/src/utils/errorMessages.ts +12 -0
- package/templates/common/.gitattributes +46 -0
- package/templates/common/.snyk.ejs +45 -0
- package/templates/common/Dockerfile +17 -9
- package/templates/common/README.md.ejs +295 -263
- package/templates/common/caching/clean/js/DeleteUser.js.ejs +27 -0
- package/templates/common/caching/clean/js/UpdateUser.js.ejs +27 -0
- package/templates/common/caching/clean/ts/deleteUser.ts.ejs +24 -0
- package/templates/common/caching/clean/ts/updateUser.ts.ejs +25 -0
- package/templates/common/caching/ts/memoryCache.ts.ejs +73 -64
- package/templates/common/caching/ts/redisClient.ts.ejs +89 -80
- package/templates/common/database/js/models/User.js.ejs +79 -53
- package/templates/common/database/js/models/User.js.mongoose.ejs +23 -19
- package/templates/common/database/js/models/User.spec.js.ejs +94 -84
- package/templates/common/database/ts/models/User.spec.ts.ejs +100 -84
- package/templates/common/database/ts/models/User.ts.ejs +87 -61
- package/templates/common/database/ts/models/User.ts.mongoose.ejs +30 -25
- package/templates/common/health/js/healthRoute.js.ejs +50 -47
- package/templates/common/health/ts/healthRoute.ts.ejs +49 -46
- package/templates/common/jest.e2e.config.js.ejs +8 -8
- package/templates/common/kafka/js/messaging/baseConsumer.js.ejs +30 -30
- package/templates/common/kafka/js/messaging/userEventSchema.js.ejs +12 -11
- package/templates/common/kafka/js/messaging/welcomeEmailConsumer.js.ejs +44 -31
- package/templates/common/kafka/js/messaging/welcomeEmailConsumer.spec.js.ejs +86 -49
- package/templates/common/kafka/js/services/kafkaService.js.ejs +93 -93
- package/templates/common/kafka/js/utils/kafkaEvents.js.ejs +7 -0
- package/templates/common/kafka/ts/messaging/userEventSchema.spec.ts.ejs +51 -51
- package/templates/common/kafka/ts/messaging/userEventSchema.ts.ejs +12 -11
- package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.spec.ts.ejs +86 -49
- package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.ts.ejs +38 -25
- package/templates/common/kafka/ts/services/kafkaService.ts.ejs +95 -95
- package/templates/common/kafka/ts/utils/kafkaEvents.ts.ejs +5 -0
- package/templates/common/package.json.ejs +10 -2
- package/templates/common/shutdown/js/gracefulShutdown.js.ejs +65 -61
- package/templates/common/shutdown/js/gracefulShutdown.spec.js.ejs +149 -160
- package/templates/common/shutdown/ts/gracefulShutdown.spec.ts.ejs +179 -158
- package/templates/common/shutdown/ts/gracefulShutdown.ts.ejs +59 -55
- package/templates/common/src/tests/e2e/e2e.users.test.js.ejs +120 -49
- package/templates/common/src/tests/e2e/e2e.users.test.ts.ejs +120 -49
- package/templates/common/swagger.yml.ejs +118 -66
- package/templates/db/mysql/V1__Initial_Setup.sql.ejs +10 -9
- package/templates/db/postgres/V1__Initial_Setup.sql.ejs +10 -9
- package/templates/mvc/js/src/controllers/userController.js.ejs +246 -105
- package/templates/mvc/js/src/controllers/userController.spec.js.ejs +481 -209
- package/templates/mvc/js/src/errors/BadRequestError.js +11 -10
- package/templates/mvc/js/src/errors/BadRequestError.spec.js.ejs +22 -21
- package/templates/mvc/js/src/errors/NotFoundError.js +11 -10
- package/templates/mvc/js/src/errors/NotFoundError.spec.js.ejs +22 -21
- package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +25 -19
- package/templates/mvc/js/src/graphql/resolvers/user.resolvers.spec.js.ejs +64 -47
- package/templates/mvc/js/src/graphql/typeDefs/user.types.js.ejs +19 -17
- package/templates/mvc/js/src/index.js.ejs +1 -1
- package/templates/mvc/js/src/routes/api.js +10 -8
- package/templates/mvc/js/src/routes/api.spec.js.ejs +41 -36
- package/templates/mvc/js/src/utils/errorMessages.js +14 -0
- package/templates/mvc/ts/src/controllers/userController.spec.ts.ejs +481 -203
- package/templates/mvc/ts/src/controllers/userController.ts.ejs +248 -107
- package/templates/mvc/ts/src/errors/BadRequestError.spec.ts.ejs +22 -21
- package/templates/mvc/ts/src/errors/BadRequestError.ts +9 -8
- package/templates/mvc/ts/src/errors/NotFoundError.spec.ts.ejs +27 -21
- package/templates/mvc/ts/src/errors/NotFoundError.ts +9 -8
- package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.spec.ts.ejs +68 -51
- package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +29 -21
- package/templates/mvc/ts/src/graphql/typeDefs/user.types.ts.ejs +17 -15
- package/templates/mvc/ts/src/index.ts.ejs +156 -153
- package/templates/mvc/ts/src/routes/api.spec.ts.ejs +59 -40
- package/templates/mvc/ts/src/routes/api.ts +12 -10
- package/templates/mvc/ts/src/utils/errorMessages.ts +12 -0
- package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.ts.ejs +0 -37
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
const { kafka } = require('../config/kafka');
|
|
2
|
-
const logger = require('<% if (architecture === "Clean Architecture") { %>../log/logger<% } else { %>../utils/logger<% } %>');
|
|
3
|
-
|
|
4
|
-
let producer = null;
|
|
5
|
-
let consumer = null;
|
|
6
|
-
let isConnected = false;
|
|
7
|
-
let connectionPromise = null;
|
|
8
|
-
|
|
9
|
-
const connectKafka = async (retries = 10) => {
|
|
10
|
-
if (connectionPromise) return connectionPromise;
|
|
11
|
-
|
|
12
|
-
connectionPromise = (async () => {
|
|
13
|
-
if (!producer) producer = kafka.producer();
|
|
14
|
-
if (!consumer) consumer = kafka.consumer({ groupId: 'test-group' });
|
|
15
|
-
|
|
16
|
-
let attempt = 0;
|
|
17
|
-
// Auto-register WelcomeEmailConsumer if it exists
|
|
18
|
-
// Note: Dynamic import used here for simplicity and to avoid startup crashes.
|
|
19
|
-
// In enterprise production, consider using Dependency Injection.
|
|
20
|
-
const WelcomeEmailConsumer = require('<% if (architecture === "Clean Architecture") { %>../../interfaces/messaging/consumers/instances/welcomeEmailConsumer<% } else { %>../messaging/consumers/instances/welcomeEmailConsumer<% } %>');
|
|
21
|
-
while (attempt < retries) {
|
|
22
|
-
try {
|
|
23
|
-
await producer.connect();
|
|
24
|
-
await consumer.connect();
|
|
25
|
-
logger.info('[Kafka] Producer connected successfully');
|
|
26
|
-
logger.info('[Kafka] Consumer connected successfully');
|
|
27
|
-
isConnected = true;
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const welcomeConsumer = new WelcomeEmailConsumer();
|
|
31
|
-
await consumer.subscribe({ topic: welcomeConsumer.topic, fromBeginning: true });
|
|
32
|
-
logger.info(`[Kafka] Registered consumer for topic: ${welcomeConsumer.topic}`);
|
|
33
|
-
|
|
34
|
-
await consumer.run({
|
|
35
|
-
eachMessage: async (payload) => welcomeConsumer.onMessage(payload),
|
|
36
|
-
});
|
|
37
|
-
} catch (error) {
|
|
38
|
-
// Fallback or no consumers found
|
|
39
|
-
await consumer.subscribe({ topic: 'user-topic', fromBeginning: true });
|
|
40
|
-
await consumer.run({
|
|
41
|
-
eachMessage: async ({ message }) => {
|
|
42
|
-
logger.info({ value: message.value.toString() });
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
return; // Success
|
|
47
|
-
} catch (error) {
|
|
48
|
-
attempt++;
|
|
49
|
-
logger.error(`[Kafka] Connection attempt ${attempt} failed:`, error.message);
|
|
50
|
-
if (attempt >= retries) {
|
|
51
|
-
throw error; // Rethrow after final attempt
|
|
52
|
-
}
|
|
53
|
-
await new Promise(res => setTimeout(res, 3000)); // Wait 3s between retries
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
})();
|
|
57
|
-
|
|
58
|
-
return connectionPromise;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const sendMessage = async (topic, message) => {
|
|
62
|
-
if (connectionPromise) {
|
|
63
|
-
await connectionPromise;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (!isConnected) {
|
|
67
|
-
throw new Error('[Kafka] Producer not connected. Check logs for connection errors.');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
await producer.send({
|
|
72
|
-
topic,
|
|
73
|
-
messages: [{ value: message }],
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
const parsed = JSON.parse(message);
|
|
78
|
-
logger.info(`[Kafka] Producer: Sent ${parsed.action} event for '${parsed.payload?.email || 'unknown'}'`);
|
|
79
|
-
} catch (error) {
|
|
80
|
-
logger.info(`[Kafka] Producer: Sent message to ${topic}`, error);
|
|
81
|
-
}
|
|
82
|
-
} catch (error) {
|
|
83
|
-
logger.error(`[Kafka] Failed to send message to ${topic}:`, error.message);
|
|
84
|
-
throw error;
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const disconnectKafka = async () => {
|
|
89
|
-
if (producer) await producer.disconnect();
|
|
90
|
-
if (consumer) await consumer.disconnect();
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
module.exports = { connectKafka, sendMessage, disconnectKafka };
|
|
1
|
+
const { kafka } = require('../config/kafka');
|
|
2
|
+
const logger = require('<% if (architecture === "Clean Architecture") { %>../log/logger<% } else { %>../utils/logger<% } %>');
|
|
3
|
+
|
|
4
|
+
let producer = null;
|
|
5
|
+
let consumer = null;
|
|
6
|
+
let isConnected = false;
|
|
7
|
+
let connectionPromise = null;
|
|
8
|
+
|
|
9
|
+
const connectKafka = async (retries = 10) => {
|
|
10
|
+
if (connectionPromise) return connectionPromise;
|
|
11
|
+
|
|
12
|
+
connectionPromise = (async () => {
|
|
13
|
+
if (!producer) producer = kafka.producer();
|
|
14
|
+
if (!consumer) consumer = kafka.consumer({ groupId: 'test-group' });
|
|
15
|
+
|
|
16
|
+
let attempt = 0;
|
|
17
|
+
// Auto-register WelcomeEmailConsumer if it exists
|
|
18
|
+
// Note: Dynamic import used here for simplicity and to avoid startup crashes.
|
|
19
|
+
// In enterprise production, consider using Dependency Injection.
|
|
20
|
+
const WelcomeEmailConsumer = require('<% if (architecture === "Clean Architecture") { %>../../interfaces/messaging/consumers/instances/welcomeEmailConsumer<% } else { %>../messaging/consumers/instances/welcomeEmailConsumer<% } %>');
|
|
21
|
+
while (attempt < retries) {
|
|
22
|
+
try {
|
|
23
|
+
await producer.connect();
|
|
24
|
+
await consumer.connect();
|
|
25
|
+
logger.info('[Kafka] Producer connected successfully');
|
|
26
|
+
logger.info('[Kafka] Consumer connected successfully');
|
|
27
|
+
isConnected = true;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const welcomeConsumer = new WelcomeEmailConsumer();
|
|
31
|
+
await consumer.subscribe({ topic: welcomeConsumer.topic, fromBeginning: true });
|
|
32
|
+
logger.info(`[Kafka] Registered consumer for topic: ${welcomeConsumer.topic}`);
|
|
33
|
+
|
|
34
|
+
await consumer.run({
|
|
35
|
+
eachMessage: async (payload) => welcomeConsumer.onMessage(payload),
|
|
36
|
+
});
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// Fallback or no consumers found
|
|
39
|
+
await consumer.subscribe({ topic: 'user-topic', fromBeginning: true });
|
|
40
|
+
await consumer.run({
|
|
41
|
+
eachMessage: async ({ message }) => {
|
|
42
|
+
logger.info({ value: message.value.toString() });
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return; // Success
|
|
47
|
+
} catch (error) {
|
|
48
|
+
attempt++;
|
|
49
|
+
logger.error(`[Kafka] Connection attempt ${attempt} failed:`, error.message);
|
|
50
|
+
if (attempt >= retries) {
|
|
51
|
+
throw error; // Rethrow after final attempt
|
|
52
|
+
}
|
|
53
|
+
await new Promise(res => setTimeout(res, 3000)); // Wait 3s between retries
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
})();
|
|
57
|
+
|
|
58
|
+
return connectionPromise;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const sendMessage = async (topic, message, key) => {
|
|
62
|
+
if (connectionPromise) {
|
|
63
|
+
await connectionPromise;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!isConnected) {
|
|
67
|
+
throw new Error('[Kafka] Producer not connected. Check logs for connection errors.');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await producer.send({
|
|
72
|
+
topic,
|
|
73
|
+
messages: [{ key, value: message }],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(message);
|
|
78
|
+
logger.info(`[Kafka] Producer: Sent ${parsed.action} event for '${parsed.payload?.email || 'unknown'}'`);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
logger.info(`[Kafka] Producer: Sent message to ${topic}`, error);
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
logger.error(`[Kafka] Failed to send message to ${topic}:`, error.message);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const disconnectKafka = async () => {
|
|
89
|
+
if (producer) await producer.disconnect();
|
|
90
|
+
if (consumer) await consumer.disconnect();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
module.exports = { connectKafka, sendMessage, disconnectKafka };
|
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
import { UserEventSchema } from '<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/schemas/userEventSchema<% } else { %>@/messaging/schemas/userEventSchema<% } %>';
|
|
2
|
-
|
|
3
|
-
describe('UserEventSchema', () => {
|
|
4
|
-
it('should validate a correct USER_CREATED event', () => {
|
|
5
|
-
const validEvent = {
|
|
6
|
-
action: 'USER_CREATED',
|
|
7
|
-
payload: {
|
|
8
|
-
id: 1,
|
|
9
|
-
email: 'test@example.com'
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
const result = UserEventSchema.safeParse(validEvent);
|
|
13
|
-
expect(result.success).toBe(true);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('should validate a correct UPDATE_USER event', () => {
|
|
17
|
-
const validEvent = {
|
|
18
|
-
action: '
|
|
19
|
-
payload: {
|
|
20
|
-
id: 'abc-123',
|
|
21
|
-
email: 'test@example.com'
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
const result = UserEventSchema.safeParse(validEvent);
|
|
25
|
-
expect(result.success).toBe(true);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('should fail validation for invalid email', () => {
|
|
29
|
-
const invalidEvent = {
|
|
30
|
-
action: 'USER_CREATED',
|
|
31
|
-
payload: {
|
|
32
|
-
id: 1,
|
|
33
|
-
email: 'invalid-email'
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
const result = UserEventSchema.safeParse(invalidEvent);
|
|
37
|
-
expect(result.success).toBe(false);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should fail validation for invalid action', () => {
|
|
41
|
-
const invalidEvent = {
|
|
42
|
-
action: 'INVALID_ACTION',
|
|
43
|
-
payload: {
|
|
44
|
-
id: 1,
|
|
45
|
-
email: 'test@example.com'
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
const result = UserEventSchema.safeParse(invalidEvent);
|
|
49
|
-
expect(result.success).toBe(false);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
1
|
+
import { UserEventSchema } from '<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/schemas/userEventSchema<% } else { %>@/messaging/schemas/userEventSchema<% } %>';
|
|
2
|
+
|
|
3
|
+
describe('UserEventSchema', () => {
|
|
4
|
+
it('should validate a correct USER_CREATED event', () => {
|
|
5
|
+
const validEvent = {
|
|
6
|
+
action: 'USER_CREATED',
|
|
7
|
+
payload: {
|
|
8
|
+
id: 1,
|
|
9
|
+
email: 'test@example.com'
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const result = UserEventSchema.safeParse(validEvent);
|
|
13
|
+
expect(result.success).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should validate a correct UPDATE_USER event', () => {
|
|
17
|
+
const validEvent = {
|
|
18
|
+
action: 'USER_UPDATED',
|
|
19
|
+
payload: {
|
|
20
|
+
id: 'abc-123',
|
|
21
|
+
email: 'test@example.com'
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const result = UserEventSchema.safeParse(validEvent);
|
|
25
|
+
expect(result.success).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should fail validation for invalid email', () => {
|
|
29
|
+
const invalidEvent = {
|
|
30
|
+
action: 'USER_CREATED',
|
|
31
|
+
payload: {
|
|
32
|
+
id: 1,
|
|
33
|
+
email: 'invalid-email'
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const result = UserEventSchema.safeParse(invalidEvent);
|
|
37
|
+
expect(result.success).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should fail validation for invalid action', () => {
|
|
41
|
+
const invalidEvent = {
|
|
42
|
+
action: 'INVALID_ACTION',
|
|
43
|
+
payload: {
|
|
44
|
+
id: 1,
|
|
45
|
+
email: 'test@example.com'
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const result = UserEventSchema.safeParse(invalidEvent);
|
|
49
|
+
expect(result.success).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { KAFKA_ACTIONS } from '@/utils/kafkaEvents';
|
|
3
|
+
|
|
4
|
+
export const UserEventSchema = z.object({
|
|
5
|
+
action: z.nativeEnum(KAFKA_ACTIONS),
|
|
6
|
+
payload: z.object({
|
|
7
|
+
id: z.union([z.string(), z.number()]),
|
|
8
|
+
email: z.string().email().optional(),
|
|
9
|
+
}),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export type UserEvent = z.infer<typeof UserEventSchema>;
|
|
@@ -1,49 +1,86 @@
|
|
|
1
|
-
import { WelcomeEmailConsumer } from '<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/consumers/instances/welcomeEmailConsumer<% } else { %>@/messaging/consumers/instances/welcomeEmailConsumer<% } %>';
|
|
2
|
-
import logger from '<%= loggerPath %>';
|
|
3
|
-
|
|
4
|
-
jest.mock('<%= loggerPath %>');
|
|
5
|
-
|
|
6
|
-
describe('WelcomeEmailConsumer', () => {
|
|
7
|
-
let consumer: WelcomeEmailConsumer;
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
jest.clearAllMocks();
|
|
11
|
-
consumer = new WelcomeEmailConsumer();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('should log welcome email simulation for USER_CREATED action', async () => {
|
|
15
|
-
const data = {
|
|
16
|
-
action: 'USER_CREATED',
|
|
17
|
-
payload: {
|
|
18
|
-
id: 1,
|
|
19
|
-
email: 'test@example.com'
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
await consumer.handle(data);
|
|
24
|
-
|
|
25
|
-
expect(logger.info).toHaveBeenCalledWith(
|
|
26
|
-
expect.stringContaining('[Kafka] Consumer: Received USER_CREATED.')
|
|
27
|
-
);
|
|
28
|
-
expect(logger.info).toHaveBeenCalledWith(
|
|
29
|
-
expect.stringContaining('📧 Sending welcome email to \'test@example.com\'... Done!')
|
|
30
|
-
);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('should log
|
|
34
|
-
const data = {
|
|
35
|
-
action: '
|
|
36
|
-
payload: {
|
|
37
|
-
id: 1,
|
|
38
|
-
email: '
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
await consumer.handle(data);
|
|
43
|
-
|
|
44
|
-
expect(logger.
|
|
45
|
-
expect.stringContaining('[Kafka]
|
|
46
|
-
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
import { WelcomeEmailConsumer } from '<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/consumers/instances/welcomeEmailConsumer<% } else { %>@/messaging/consumers/instances/welcomeEmailConsumer<% } %>';
|
|
2
|
+
import logger from '<%= loggerPath %>';
|
|
3
|
+
|
|
4
|
+
jest.mock('<%= loggerPath %>');
|
|
5
|
+
|
|
6
|
+
describe('WelcomeEmailConsumer', () => {
|
|
7
|
+
let consumer: WelcomeEmailConsumer;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
consumer = new WelcomeEmailConsumer();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should log welcome email simulation for USER_CREATED action', async () => {
|
|
15
|
+
const data = {
|
|
16
|
+
action: 'USER_CREATED',
|
|
17
|
+
payload: {
|
|
18
|
+
id: 1,
|
|
19
|
+
email: 'test@example.com'
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
await consumer.handle(data);
|
|
24
|
+
|
|
25
|
+
expect(logger.info).toHaveBeenCalledWith(
|
|
26
|
+
expect.stringContaining('[Kafka] Consumer: Received USER_CREATED.')
|
|
27
|
+
);
|
|
28
|
+
expect(logger.info).toHaveBeenCalledWith(
|
|
29
|
+
expect.stringContaining('📧 Sending welcome email to \'test@example.com\'... Done!')
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should log update simulation for USER_UPDATED action', async () => {
|
|
34
|
+
const data = {
|
|
35
|
+
action: 'USER_UPDATED',
|
|
36
|
+
payload: {
|
|
37
|
+
id: 1,
|
|
38
|
+
email: 'updated@example.com'
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
await consumer.handle(data);
|
|
43
|
+
|
|
44
|
+
expect(logger.info).toHaveBeenCalledWith(
|
|
45
|
+
expect.stringContaining('[Kafka] Consumer: Received USER_UPDATED.')
|
|
46
|
+
);
|
|
47
|
+
expect(logger.info).toHaveBeenCalledWith(
|
|
48
|
+
expect.stringContaining('🔄 Updating user records for \'1\' (Email: updated@example.com)... Done!')
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should log deletion simulation for USER_DELETED action', async () => {
|
|
53
|
+
const data = {
|
|
54
|
+
action: 'USER_DELETED',
|
|
55
|
+
payload: {
|
|
56
|
+
id: 1
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
await consumer.handle(data);
|
|
61
|
+
|
|
62
|
+
expect(logger.info).toHaveBeenCalledWith(
|
|
63
|
+
expect.stringContaining('[Kafka] Consumer: Received USER_DELETED.')
|
|
64
|
+
);
|
|
65
|
+
expect(logger.info).toHaveBeenCalledWith(
|
|
66
|
+
expect.stringContaining('🗑️ Cleaning up data for user \'1\'... Done!')
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should log error for invalid data', async () => {
|
|
71
|
+
const data = {
|
|
72
|
+
action: 'USER_CREATED',
|
|
73
|
+
payload: {
|
|
74
|
+
id: 1,
|
|
75
|
+
email: 'invalid-email'
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
await consumer.handle(data);
|
|
80
|
+
|
|
81
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
82
|
+
expect.stringContaining('[Kafka] Invalid user event data:'),
|
|
83
|
+
expect.anything()
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -1,25 +1,38 @@
|
|
|
1
|
-
import { BaseConsumer } from '<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/baseConsumer<% } else { %>@/messaging/baseConsumer<% } %>';
|
|
2
|
-
import logger from '<%= loggerPath %>';
|
|
3
|
-
import { UserEventSchema } from '<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/schemas/userEventSchema<% } else { %>@/messaging/schemas/userEventSchema<% } %>';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
1
|
+
import { BaseConsumer } from '<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/baseConsumer<% } else { %>@/messaging/baseConsumer<% } %>';
|
|
2
|
+
import logger from '<%= loggerPath %>';
|
|
3
|
+
import { UserEventSchema } from '<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/schemas/userEventSchema<% } else { %>@/messaging/schemas/userEventSchema<% } %>';
|
|
4
|
+
import { ERROR_MESSAGES } from '@/utils/errorMessages';
|
|
5
|
+
import { KAFKA_ACTIONS } from '@/utils/kafkaEvents';
|
|
6
|
+
|
|
7
|
+
export class WelcomeEmailConsumer extends BaseConsumer {
|
|
8
|
+
topic = 'user-topic';
|
|
9
|
+
groupId = 'welcome-email-group';
|
|
10
|
+
|
|
11
|
+
async handle(data: unknown) {
|
|
12
|
+
const result = UserEventSchema.safeParse(data);
|
|
13
|
+
|
|
14
|
+
if (!result.success) {
|
|
15
|
+
logger.error(`[Kafka] ${ERROR_MESSAGES.INVALID_USER_DATA}:`, result.error.format());
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { action, payload } = result.data;
|
|
20
|
+
|
|
21
|
+
switch (action) {
|
|
22
|
+
case KAFKA_ACTIONS.USER_CREATED:
|
|
23
|
+
logger.info(`[Kafka] Consumer: Received ${KAFKA_ACTIONS.USER_CREATED}.`);
|
|
24
|
+
logger.info(`[Kafka] Consumer: 📧 Sending welcome email to '${payload.email}'... Done!`);
|
|
25
|
+
break;
|
|
26
|
+
case KAFKA_ACTIONS.USER_UPDATED:
|
|
27
|
+
logger.info(`[Kafka] Consumer: Received ${KAFKA_ACTIONS.USER_UPDATED}.`);
|
|
28
|
+
logger.info(`[Kafka] Consumer: 🔄 Updating user records for '${payload.id}' (Email: ${payload.email})... Done!`);
|
|
29
|
+
break;
|
|
30
|
+
case KAFKA_ACTIONS.USER_DELETED:
|
|
31
|
+
logger.info(`[Kafka] Consumer: Received ${KAFKA_ACTIONS.USER_DELETED}.`);
|
|
32
|
+
logger.info(`[Kafka] Consumer: 🗑️ Cleaning up data for user '${payload.id}'... Done!`);
|
|
33
|
+
break;
|
|
34
|
+
default:
|
|
35
|
+
logger.warn(`[Kafka] Unknown action: ${action}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|