@solutionspool/node-micro-contracts 1.0.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/ARCHITECTURE.md +302 -0
- package/QUICKSTART.md +204 -0
- package/README.md +95 -0
- package/USAGE_GUIDE.md +434 -0
- package/examples/auth-routes.example.js +38 -0
- package/examples/event-consumption.example.js +109 -0
- package/examples/event-publishing.example.js +82 -0
- package/index.js +25 -0
- package/middleware/validate.js +75 -0
- package/package.json +19 -0
- package/schemas/common.schema.js +33 -0
- package/schemas/events.schema.js +55 -0
- package/schemas/user.schema.js +91 -0
- package/test-contracts.js +229 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod Validation Middleware Factory
|
|
3
|
+
*
|
|
4
|
+
* Creates middleware to validate request body against Zod schemas
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function validateRequest(schema) {
|
|
8
|
+
return (req, res, next) => {
|
|
9
|
+
const result = schema.safeParse(req.body);
|
|
10
|
+
|
|
11
|
+
if (!result.success) {
|
|
12
|
+
return res.status(400).json({
|
|
13
|
+
error: "Validation failed",
|
|
14
|
+
details: result.error.errors.map((err) => ({
|
|
15
|
+
path: err.path.join("."),
|
|
16
|
+
message: err.message,
|
|
17
|
+
})),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Attach validated data to request
|
|
22
|
+
req.validatedData = result.data;
|
|
23
|
+
next();
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validate query parameters
|
|
29
|
+
*/
|
|
30
|
+
function validateQuery(schema) {
|
|
31
|
+
return (req, res, next) => {
|
|
32
|
+
const result = schema.safeParse(req.query);
|
|
33
|
+
|
|
34
|
+
if (!result.success) {
|
|
35
|
+
return res.status(400).json({
|
|
36
|
+
error: "Query validation failed",
|
|
37
|
+
details: result.error.errors.map((err) => ({
|
|
38
|
+
path: err.path.join("."),
|
|
39
|
+
message: err.message,
|
|
40
|
+
})),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
req.validatedQuery = result.data;
|
|
45
|
+
next();
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Validate request parameters
|
|
51
|
+
*/
|
|
52
|
+
function validateParams(schema) {
|
|
53
|
+
return (req, res, next) => {
|
|
54
|
+
const result = schema.safeParse(req.params);
|
|
55
|
+
|
|
56
|
+
if (!result.success) {
|
|
57
|
+
return res.status(400).json({
|
|
58
|
+
error: "Parameter validation failed",
|
|
59
|
+
details: result.error.errors.map((err) => ({
|
|
60
|
+
path: err.path.join("."),
|
|
61
|
+
message: err.message,
|
|
62
|
+
})),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
req.validatedParams = result.data;
|
|
67
|
+
next();
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
validateRequest,
|
|
73
|
+
validateQuery,
|
|
74
|
+
validateParams,
|
|
75
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@solutionspool/node-micro-contracts",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared contracts and schemas for node-micro services",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"contracts",
|
|
11
|
+
"schemas",
|
|
12
|
+
"zod",
|
|
13
|
+
"microservices"
|
|
14
|
+
],
|
|
15
|
+
"license": "ISC",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"zod": "^3.22.4"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const { z } = require("zod");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Param Validation Schemas
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// UUID parameter validation
|
|
8
|
+
const UUIDParamSchema = z.object({
|
|
9
|
+
id: z.string().uuid("Invalid user ID"),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Query Validation Schemas
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// User search query validation
|
|
17
|
+
const UserSearchQuerySchema = z.object({
|
|
18
|
+
name: z.string().trim().min(1, "Name must be a non-empty string").optional(),
|
|
19
|
+
email: z.string().email("Invalid email format").optional(),
|
|
20
|
+
mobile: z
|
|
21
|
+
.string()
|
|
22
|
+
.trim()
|
|
23
|
+
.min(1, "Mobile must be a non-empty string")
|
|
24
|
+
.optional(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
// Param Schemas
|
|
29
|
+
UUIDParamSchema,
|
|
30
|
+
|
|
31
|
+
// Query Schemas
|
|
32
|
+
UserSearchQuerySchema,
|
|
33
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const { z } = require("zod");
|
|
2
|
+
const { UserRole } = require("./user.schema");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* User Registered Event Schema
|
|
6
|
+
* Published by auth-service when a user registers
|
|
7
|
+
* Consumed by user-service to create user profile
|
|
8
|
+
*/
|
|
9
|
+
const UserRegisteredEventSchema = z.object({
|
|
10
|
+
userId: z.string().uuid(),
|
|
11
|
+
name: z.string(),
|
|
12
|
+
email: z.string().email(),
|
|
13
|
+
mobile: z.string().optional(),
|
|
14
|
+
role: UserRole,
|
|
15
|
+
createdAt: z.union([z.string(), z.date()]),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* User Verified Event Schema
|
|
20
|
+
* Published when a user verifies their email
|
|
21
|
+
*/
|
|
22
|
+
const UserVerifiedEventSchema = z.object({
|
|
23
|
+
userId: z.string().uuid(),
|
|
24
|
+
email: z.string().email(),
|
|
25
|
+
verifiedAt: z.union([z.string(), z.date()]),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* User Updated Event Schema
|
|
30
|
+
* Published when a user's profile is updated
|
|
31
|
+
*/
|
|
32
|
+
const UserUpdatedEventSchema = z.object({
|
|
33
|
+
userId: z.string().uuid(),
|
|
34
|
+
name: z.string().optional(),
|
|
35
|
+
email: z.string().email().optional(),
|
|
36
|
+
role: UserRole.optional(),
|
|
37
|
+
updatedAt: z.union([z.string(), z.date()]),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* User Deleted Event Schema
|
|
42
|
+
* Published when a user account is deleted
|
|
43
|
+
*/
|
|
44
|
+
const UserDeletedEventSchema = z.object({
|
|
45
|
+
userId: z.string().uuid(),
|
|
46
|
+
email: z.string().email(),
|
|
47
|
+
deletedAt: z.union([z.string(), z.date()]),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
UserRegisteredEventSchema,
|
|
52
|
+
UserVerifiedEventSchema,
|
|
53
|
+
UserUpdatedEventSchema,
|
|
54
|
+
UserDeletedEventSchema,
|
|
55
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const { z } = require("zod");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* User Role Enum
|
|
5
|
+
*/
|
|
6
|
+
const UserRole = z.enum(["user", "provider", "admin"]);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* User Base Schema - Common fields
|
|
10
|
+
*/
|
|
11
|
+
const UserBaseSchema = z.object({
|
|
12
|
+
id: z.string().uuid(),
|
|
13
|
+
name: z.string().min(1, "Name is required"),
|
|
14
|
+
email: z.string().email("Invalid email format"),
|
|
15
|
+
role: UserRole,
|
|
16
|
+
createdAt: z.union([z.string(), z.date()]).optional(),
|
|
17
|
+
updatedAt: z.union([z.string(), z.date()]).optional(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* User Registration Request Schema
|
|
22
|
+
*/
|
|
23
|
+
const UserRegistrationSchema = z.object({
|
|
24
|
+
name: z.string().min(1, "Name is required"),
|
|
25
|
+
email: z.string().email("Invalid email format"),
|
|
26
|
+
mobile: z.string().optional(),
|
|
27
|
+
password: z.string().min(6, "Password must be at least 6 characters"),
|
|
28
|
+
role: UserRole.optional().default("user"),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* User Login Request Schema
|
|
33
|
+
*/
|
|
34
|
+
const UserLoginSchema = z.object({
|
|
35
|
+
email: z.string().email("Invalid email format"),
|
|
36
|
+
password: z.string().min(1, "Password is required"),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* User Profile Update Schema
|
|
41
|
+
*/
|
|
42
|
+
const UserProfileUpdateSchema = z.object({
|
|
43
|
+
name: z.string().min(1, "Name is required").optional(),
|
|
44
|
+
email: z.string().email("Invalid email format").optional(),
|
|
45
|
+
role: UserRole.optional(),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* User Response Schema (without password)
|
|
50
|
+
*/
|
|
51
|
+
const UserResponseSchema = UserBaseSchema.extend({
|
|
52
|
+
mobile: z.string().optional(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* JWT Token Payload Schema
|
|
57
|
+
*/
|
|
58
|
+
const JWTPayloadSchema = z.object({
|
|
59
|
+
iss: z.string(), // Issuer
|
|
60
|
+
sub: z.string().uuid(), // Subject (user ID)
|
|
61
|
+
id: z.string().uuid(),
|
|
62
|
+
email: z.string().email(),
|
|
63
|
+
role: UserRole,
|
|
64
|
+
iat: z.number().optional(), // Issued at
|
|
65
|
+
exp: z.number().optional(), // Expiration
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Auth Response Schema
|
|
70
|
+
*/
|
|
71
|
+
const AuthResponseSchema = z.object({
|
|
72
|
+
message: z.string(),
|
|
73
|
+
token: z.string(),
|
|
74
|
+
user: UserResponseSchema,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
// Enums
|
|
79
|
+
UserRole,
|
|
80
|
+
|
|
81
|
+
// User Schemas
|
|
82
|
+
UserBaseSchema,
|
|
83
|
+
UserRegistrationSchema,
|
|
84
|
+
UserLoginSchema,
|
|
85
|
+
UserProfileUpdateSchema,
|
|
86
|
+
UserResponseSchema,
|
|
87
|
+
|
|
88
|
+
// JWT Schemas
|
|
89
|
+
JWTPayloadSchema,
|
|
90
|
+
AuthResponseSchema,
|
|
91
|
+
};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test file to verify contracts are working correctly
|
|
3
|
+
* Run with: node test-contracts.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
// Schemas
|
|
8
|
+
UserRegistrationSchema,
|
|
9
|
+
UserLoginSchema,
|
|
10
|
+
UserProfileUpdateSchema,
|
|
11
|
+
UserRegisteredEventSchema,
|
|
12
|
+
UserRole,
|
|
13
|
+
|
|
14
|
+
// Middleware
|
|
15
|
+
validateRequest,
|
|
16
|
+
} = require("./index");
|
|
17
|
+
|
|
18
|
+
console.log("🧪 Testing @node-micro/contracts\n");
|
|
19
|
+
|
|
20
|
+
// Test 1: Valid User Registration
|
|
21
|
+
console.log("Test 1: Valid User Registration");
|
|
22
|
+
try {
|
|
23
|
+
const validRegistration = {
|
|
24
|
+
name: "John Doe",
|
|
25
|
+
email: "john@example.com",
|
|
26
|
+
password: "password123",
|
|
27
|
+
role: "user",
|
|
28
|
+
mobile: "+1234567890",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const result = UserRegistrationSchema.safeParse(validRegistration);
|
|
32
|
+
if (result.success) {
|
|
33
|
+
console.log("✅ PASS - Valid registration data accepted");
|
|
34
|
+
} else {
|
|
35
|
+
console.log("❌ FAIL - Valid data rejected");
|
|
36
|
+
console.log(result.error.errors);
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.log("❌ FAIL - Unexpected error:", error.message);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Test 2: Invalid Email
|
|
43
|
+
console.log("\nTest 2: Invalid Email");
|
|
44
|
+
try {
|
|
45
|
+
const invalidEmail = {
|
|
46
|
+
name: "John Doe",
|
|
47
|
+
email: "not-an-email",
|
|
48
|
+
password: "password123",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const result = UserRegistrationSchema.safeParse(invalidEmail);
|
|
52
|
+
if (!result.success) {
|
|
53
|
+
console.log("✅ PASS - Invalid email rejected");
|
|
54
|
+
console.log(" Error:", result.error.errors[0].message);
|
|
55
|
+
} else {
|
|
56
|
+
console.log("❌ FAIL - Invalid email accepted");
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.log("❌ FAIL - Unexpected error:", error.message);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Test 3: Short Password
|
|
63
|
+
console.log("\nTest 3: Short Password");
|
|
64
|
+
try {
|
|
65
|
+
const shortPassword = {
|
|
66
|
+
name: "John Doe",
|
|
67
|
+
email: "john@example.com",
|
|
68
|
+
password: "123",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const result = UserRegistrationSchema.safeParse(shortPassword);
|
|
72
|
+
if (!result.success) {
|
|
73
|
+
console.log("✅ PASS - Short password rejected");
|
|
74
|
+
console.log(" Error:", result.error.errors[0].message);
|
|
75
|
+
} else {
|
|
76
|
+
console.log("❌ FAIL - Short password accepted");
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.log("❌ FAIL - Unexpected error:", error.message);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Test 4: Invalid Role
|
|
83
|
+
console.log("\nTest 4: Invalid Role");
|
|
84
|
+
try {
|
|
85
|
+
const invalidRole = {
|
|
86
|
+
name: "John Doe",
|
|
87
|
+
email: "john@example.com",
|
|
88
|
+
password: "password123",
|
|
89
|
+
role: "superadmin", // Not a valid role
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const result = UserRegistrationSchema.safeParse(invalidRole);
|
|
93
|
+
if (!result.success) {
|
|
94
|
+
console.log("✅ PASS - Invalid role rejected");
|
|
95
|
+
console.log(" Error:", result.error.errors[0].message);
|
|
96
|
+
} else {
|
|
97
|
+
console.log("❌ FAIL - Invalid role accepted");
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.log("❌ FAIL - Unexpected error:", error.message);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Test 5: Valid Login
|
|
104
|
+
console.log("\nTest 5: Valid Login");
|
|
105
|
+
try {
|
|
106
|
+
const validLogin = {
|
|
107
|
+
email: "john@example.com",
|
|
108
|
+
password: "password123",
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const result = UserLoginSchema.safeParse(validLogin);
|
|
112
|
+
if (result.success) {
|
|
113
|
+
console.log("✅ PASS - Valid login data accepted");
|
|
114
|
+
} else {
|
|
115
|
+
console.log("❌ FAIL - Valid login data rejected");
|
|
116
|
+
console.log(result.error.errors);
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.log("❌ FAIL - Unexpected error:", error.message);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Test 6: Valid Event Data
|
|
123
|
+
console.log("\nTest 6: Valid Event Data");
|
|
124
|
+
try {
|
|
125
|
+
const validEvent = {
|
|
126
|
+
userId: "123e4567-e89b-12d3-a456-426614174000",
|
|
127
|
+
name: "John Doe",
|
|
128
|
+
email: "john@example.com",
|
|
129
|
+
mobile: "+1234567890",
|
|
130
|
+
role: "user",
|
|
131
|
+
createdAt: new Date(),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const result = UserRegisteredEventSchema.safeParse(validEvent);
|
|
135
|
+
if (result.success) {
|
|
136
|
+
console.log("✅ PASS - Valid event data accepted");
|
|
137
|
+
} else {
|
|
138
|
+
console.log("❌ FAIL - Valid event data rejected");
|
|
139
|
+
console.log(result.error.errors);
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.log("❌ FAIL - Unexpected error:", error.message);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Test 7: Invalid UUID in Event
|
|
146
|
+
console.log("\nTest 7: Invalid UUID in Event");
|
|
147
|
+
try {
|
|
148
|
+
const invalidUUID = {
|
|
149
|
+
userId: "not-a-uuid",
|
|
150
|
+
name: "John Doe",
|
|
151
|
+
email: "john@example.com",
|
|
152
|
+
role: "user",
|
|
153
|
+
createdAt: new Date(),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const result = UserRegisteredEventSchema.safeParse(invalidUUID);
|
|
157
|
+
if (!result.success) {
|
|
158
|
+
console.log("✅ PASS - Invalid UUID rejected");
|
|
159
|
+
console.log(" Error:", result.error.errors[0].message);
|
|
160
|
+
} else {
|
|
161
|
+
console.log("❌ FAIL - Invalid UUID accepted");
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.log("❌ FAIL - Unexpected error:", error.message);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Test 8: Profile Update (Partial Data)
|
|
168
|
+
console.log("\nTest 8: Profile Update (Partial Data)");
|
|
169
|
+
try {
|
|
170
|
+
const partialUpdate = {
|
|
171
|
+
name: "Jane Doe", // Only updating name
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const result = UserProfileUpdateSchema.safeParse(partialUpdate);
|
|
175
|
+
if (result.success) {
|
|
176
|
+
console.log("✅ PASS - Partial update accepted");
|
|
177
|
+
} else {
|
|
178
|
+
console.log("❌ FAIL - Partial update rejected");
|
|
179
|
+
console.log(result.error.errors);
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.log("❌ FAIL - Unexpected error:", error.message);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Test 9: Default Role
|
|
186
|
+
console.log("\nTest 9: Default Role");
|
|
187
|
+
try {
|
|
188
|
+
const noRole = {
|
|
189
|
+
name: "John Doe",
|
|
190
|
+
email: "john@example.com",
|
|
191
|
+
password: "password123",
|
|
192
|
+
// role not provided
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const result = UserRegistrationSchema.safeParse(noRole);
|
|
196
|
+
if (result.success && result.data.role === "user") {
|
|
197
|
+
console.log("✅ PASS - Default role 'user' applied");
|
|
198
|
+
} else {
|
|
199
|
+
console.log("❌ FAIL - Default role not applied");
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.log("❌ FAIL - Unexpected error:", error.message);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Test 10: UserRole Enum
|
|
206
|
+
console.log("\nTest 10: UserRole Enum");
|
|
207
|
+
try {
|
|
208
|
+
const validRoles = ["user", "provider", "admin"];
|
|
209
|
+
const allValid = validRoles.every((role) => {
|
|
210
|
+
const result = UserRole.safeParse(role);
|
|
211
|
+
return result.success;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const invalidResult = UserRole.safeParse("invalid");
|
|
215
|
+
|
|
216
|
+
if (allValid && !invalidResult.success) {
|
|
217
|
+
console.log("✅ PASS - UserRole enum works correctly");
|
|
218
|
+
} else {
|
|
219
|
+
console.log("❌ FAIL - UserRole enum validation failed");
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.log("❌ FAIL - Unexpected error:", error.message);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
console.log("\n✨ Testing complete!");
|
|
226
|
+
console.log("\nTo use in your services:");
|
|
227
|
+
console.log(
|
|
228
|
+
' const { UserRegistrationSchema, validateRequest } = require("@node-micro/contracts");',
|
|
229
|
+
);
|