create-elseware-nodejs-app 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/README.md +1 -0
- package/bin/elseware.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +114 -0
- package/package.json +42 -0
- package/templates/base/.env.example +7 -0
- package/templates/base/README.md +41 -0
- package/templates/base/eslint.config.js +73 -0
- package/templates/base/jest.config.js +5 -0
- package/templates/base/package.json +45 -0
- package/templates/base/src/app.ts +24 -0
- package/templates/base/src/configs/database.ts +4 -0
- package/templates/base/src/configs/env.ts +26 -0
- package/templates/base/src/controllers/health.controller.ts +14 -0
- package/templates/base/src/controllers/post.controller.ts +27 -0
- package/templates/base/src/controllers/user.controller.ts +33 -0
- package/templates/base/src/models/post.model.ts +26 -0
- package/templates/base/src/models/user.model.ts +25 -0
- package/templates/base/src/routes/health.routes.ts +8 -0
- package/templates/base/src/routes/index.ts +12 -0
- package/templates/base/src/routes/post.routes.ts +18 -0
- package/templates/base/src/routes/user.routes.ts +18 -0
- package/templates/base/src/server.ts +20 -0
- package/templates/base/src/services/user.service.ts +39 -0
- package/templates/base/tests/health.test.js +9 -0
- package/templates/base/tests/user.test.js +13 -0
- package/templates/base/tsconfig.json +17 -0
- package/templates/base-package.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# create-elseware-app
|
package/bin/elseware.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/create.ts
|
|
4
|
+
import path2 from "path";
|
|
5
|
+
import fs2 from "fs";
|
|
6
|
+
|
|
7
|
+
// src/prompts.ts
|
|
8
|
+
import inquirer from "inquirer";
|
|
9
|
+
async function getProjectOptions(projectNameFromArg) {
|
|
10
|
+
const answers = await inquirer.prompt([
|
|
11
|
+
{
|
|
12
|
+
type: "input",
|
|
13
|
+
name: "name",
|
|
14
|
+
message: "Project name:",
|
|
15
|
+
default: projectNameFromArg,
|
|
16
|
+
validate: (input) => Boolean(input) || "Project name is required"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
type: "input",
|
|
20
|
+
name: "version",
|
|
21
|
+
message: "Initial version:",
|
|
22
|
+
default: "1.0.0"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
type: "input",
|
|
26
|
+
name: "description",
|
|
27
|
+
message: "Project description:",
|
|
28
|
+
default: "Elseware NodeJS application"
|
|
29
|
+
}
|
|
30
|
+
]);
|
|
31
|
+
return answers;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/generator.ts
|
|
35
|
+
import fs from "fs";
|
|
36
|
+
import path from "path";
|
|
37
|
+
import { fileURLToPath } from "url";
|
|
38
|
+
|
|
39
|
+
// utils/logger.ts
|
|
40
|
+
var logger = {
|
|
41
|
+
success: (msg) => console.log(`\u2705 ${msg}`),
|
|
42
|
+
info: (msg) => console.log(`\u2139\uFE0F ${msg}`),
|
|
43
|
+
warn: (msg) => console.log(`\u26A0\uFE0F ${msg}`),
|
|
44
|
+
error: (msg) => console.error(`\u274C ${msg}`)
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// src/generator.ts
|
|
48
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
49
|
+
var __dirname = path.dirname(__filename);
|
|
50
|
+
async function generateProject({ targetDir, options }) {
|
|
51
|
+
const templateDir = path.resolve(__dirname, "../templates/base");
|
|
52
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
53
|
+
copyRecursive(templateDir, targetDir);
|
|
54
|
+
updatePackageJson(targetDir, options);
|
|
55
|
+
}
|
|
56
|
+
function copyRecursive(src, dest) {
|
|
57
|
+
for (const entry of fs.readdirSync(src)) {
|
|
58
|
+
const srcPath = path.join(src, entry);
|
|
59
|
+
const destPath = path.join(dest, entry);
|
|
60
|
+
if (fs.statSync(srcPath).isDirectory()) {
|
|
61
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
62
|
+
copyRecursive(srcPath, destPath);
|
|
63
|
+
} else {
|
|
64
|
+
fs.copyFileSync(srcPath, destPath);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function updatePackageJson(targetDir, options) {
|
|
69
|
+
const pkgPath = path.join(targetDir, "package.json");
|
|
70
|
+
if (!fs.existsSync(pkgPath)) {
|
|
71
|
+
throw new Error("package.json not found in template");
|
|
72
|
+
}
|
|
73
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
74
|
+
pkg.name = options.name;
|
|
75
|
+
pkg.version = options.version;
|
|
76
|
+
pkg.description = options.description;
|
|
77
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
78
|
+
logger.success("package.json configured");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/create.ts
|
|
82
|
+
async function createProject(projectName2) {
|
|
83
|
+
const options = await getProjectOptions(projectName2);
|
|
84
|
+
const targetDir = path2.resolve(process.cwd(), options.name);
|
|
85
|
+
if (fs2.existsSync(targetDir)) {
|
|
86
|
+
throw new Error(`Directory "${options.name}" already exists`);
|
|
87
|
+
}
|
|
88
|
+
logger.info(`Creating Elseware app: ${options.name}`);
|
|
89
|
+
await generateProject({
|
|
90
|
+
targetDir,
|
|
91
|
+
options
|
|
92
|
+
});
|
|
93
|
+
logger.success("Project created successfully!");
|
|
94
|
+
logger.info("Next steps:");
|
|
95
|
+
logger.info(` cd ${options.name}`);
|
|
96
|
+
logger.info(" npm install");
|
|
97
|
+
logger.info(" npm run dev");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/index.ts
|
|
101
|
+
var [, , projectName] = process.argv;
|
|
102
|
+
async function bootstrap() {
|
|
103
|
+
try {
|
|
104
|
+
await createProject(projectName);
|
|
105
|
+
return;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
logger.error("Failed to execute command");
|
|
108
|
+
if (error instanceof Error) {
|
|
109
|
+
logger.error(error.message);
|
|
110
|
+
}
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
bootstrap();
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-elseware-nodejs-app",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI to scaffold Elseware NodeJS applications",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-elseware-nodejs-app": "bin/elseware.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"bin",
|
|
13
|
+
"templates"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"clean": "rm -rf dist",
|
|
17
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
18
|
+
"dev": "tsx src/index.ts",
|
|
19
|
+
"prepublishOnly": "npm run build",
|
|
20
|
+
"sync-template": "tsx src/sync-template.ts",
|
|
21
|
+
"release": "npm publish --access public"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/elsewaretechnologies/elseware-npm-cli.git"
|
|
26
|
+
},
|
|
27
|
+
"author": "Dhanushka Sandakelum",
|
|
28
|
+
"license": "ISC",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/elsewaretechnologies/elseware-npm-cli/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/elsewaretechnologies/elseware-npm-cli#readme",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"inquirer": "^12.11.1"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^25.0.3",
|
|
38
|
+
"tsup": "^8.0.0",
|
|
39
|
+
"tsx": "^4.19.0",
|
|
40
|
+
"typescript": "^5.9.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# elseware-nodejs-template
|
|
2
|
+
|
|
3
|
+
Production-ready Node.js REST API template.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Express + ESM
|
|
8
|
+
- Clean architecture
|
|
9
|
+
- Jest testing
|
|
10
|
+
- Env validation
|
|
11
|
+
- Scalable structure
|
|
12
|
+
|
|
13
|
+
## Run
|
|
14
|
+
|
|
15
|
+
npm install
|
|
16
|
+
npm run dev
|
|
17
|
+
|
|
18
|
+
## Test
|
|
19
|
+
|
|
20
|
+
npm test
|
|
21
|
+
|
|
22
|
+
## Architecture
|
|
23
|
+
|
|
24
|
+
### Hybrid Pattern for APIs
|
|
25
|
+
|
|
26
|
+
To quickly implment generic REST APIs, we can use APIFactory without concerning **service layer**. But for the APIs that require complex **data layer** validations, we can use **service layer**.
|
|
27
|
+
|
|
28
|
+
- Use ApiFactory for “simple CRUD”
|
|
29
|
+
- Use Services for “business logic”
|
|
30
|
+
- Mixing them is not a smell — it’s a strength.
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
// Simple
|
|
34
|
+
export const getUsers = ApiFactory.getAll(UserModel);
|
|
35
|
+
|
|
36
|
+
// Complex
|
|
37
|
+
export const createUser = asyncHandler(async (req, res) => {
|
|
38
|
+
const user = await userService.createUser(req.body);
|
|
39
|
+
ApiResponse.created(res, user);
|
|
40
|
+
});
|
|
41
|
+
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
import globals from "globals";
|
|
3
|
+
import jestPlugin from "eslint-plugin-jest";
|
|
4
|
+
import tseslint from "typescript-eslint";
|
|
5
|
+
|
|
6
|
+
export default [
|
|
7
|
+
// Base JS rules (real bugs)
|
|
8
|
+
js.configs.recommended,
|
|
9
|
+
|
|
10
|
+
// TypeScript-aware rules
|
|
11
|
+
...tseslint.configs.recommended,
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
files: ["**/*.ts"],
|
|
15
|
+
languageOptions: {
|
|
16
|
+
parserOptions: {
|
|
17
|
+
project: "./tsconfig.json",
|
|
18
|
+
tsconfigRootDir: import.meta.dirname,
|
|
19
|
+
},
|
|
20
|
+
globals: {
|
|
21
|
+
...globals.node,
|
|
22
|
+
...globals.es2022,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
rules: {
|
|
26
|
+
// =============================
|
|
27
|
+
// Formatting (DISABLED)
|
|
28
|
+
// =============================
|
|
29
|
+
indent: "off",
|
|
30
|
+
"comma-dangle": "off",
|
|
31
|
+
quotes: "off",
|
|
32
|
+
semi: "off",
|
|
33
|
+
|
|
34
|
+
// =============================
|
|
35
|
+
// TypeScript-specific safety
|
|
36
|
+
// =============================
|
|
37
|
+
"@typescript-eslint/no-unused-vars": [
|
|
38
|
+
"warn",
|
|
39
|
+
{ argsIgnorePattern: "^_" },
|
|
40
|
+
],
|
|
41
|
+
|
|
42
|
+
"@typescript-eslint/no-explicit-any": "warn",
|
|
43
|
+
|
|
44
|
+
"@typescript-eslint/consistent-type-imports": [
|
|
45
|
+
"warn",
|
|
46
|
+
{ prefer: "type-imports" },
|
|
47
|
+
],
|
|
48
|
+
|
|
49
|
+
"@typescript-eslint/no-floating-promises": "error",
|
|
50
|
+
|
|
51
|
+
"@typescript-eslint/await-thenable": "error",
|
|
52
|
+
|
|
53
|
+
// =============================
|
|
54
|
+
// Node / backend
|
|
55
|
+
// =============================
|
|
56
|
+
"no-console": "off",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
// Jest tests
|
|
61
|
+
{
|
|
62
|
+
files: ["**/*.test.ts"],
|
|
63
|
+
plugins: {
|
|
64
|
+
jest: jestPlugin,
|
|
65
|
+
},
|
|
66
|
+
languageOptions: {
|
|
67
|
+
globals: globals.jest,
|
|
68
|
+
},
|
|
69
|
+
rules: {
|
|
70
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "elseware-nodejs-template",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Sample elseware App for NodeJs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "server.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "tsx watch src/server.ts",
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"start": "node dist/server.js",
|
|
11
|
+
"test": "NODE_ENV=test node --experimental-vm-modules ./node_modules/.bin/jest",
|
|
12
|
+
"coverage": "NODE_ENV=test node --experimental-vm-modules ./node_modules/.bin/jest --coverage",
|
|
13
|
+
"lint": "eslint .",
|
|
14
|
+
"lint:fix": "eslint . --fix"
|
|
15
|
+
},
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"dotenv": "^17.2.3",
|
|
19
|
+
"elseware-nodejs": "^1.0.3",
|
|
20
|
+
"express": "^5.2.1",
|
|
21
|
+
"helmet": "^8.1.0",
|
|
22
|
+
"joi": "^18.0.2",
|
|
23
|
+
"mongoose": "^9.0.2",
|
|
24
|
+
"pino": "^10.1.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@eslint/js": "^9.39.2",
|
|
28
|
+
"@types/express": "^5.0.6",
|
|
29
|
+
"@types/jest": "^30.0.0",
|
|
30
|
+
"@types/node": "^25.0.3",
|
|
31
|
+
"@types/supertest": "^6.0.3",
|
|
32
|
+
"eslint": "^9.39.2",
|
|
33
|
+
"eslint-config-prettier": "^10.1.8",
|
|
34
|
+
"eslint-plugin-jest": "^28.14.0",
|
|
35
|
+
"globals": "^16.5.0",
|
|
36
|
+
"jest": "^29.7.0",
|
|
37
|
+
"pino-pretty": "^13.1.3",
|
|
38
|
+
"prettier": "^3.7.4",
|
|
39
|
+
"supertest": "^7.1.4",
|
|
40
|
+
"ts-node": "^10.9.2",
|
|
41
|
+
"tsx": "^4.21.0",
|
|
42
|
+
"typescript": "^5.9.3",
|
|
43
|
+
"typescript-eslint": "^8.51.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import express, { Application } from "express";
|
|
2
|
+
import { AppError, GlobalErrorHandler } from "elseware-nodejs";
|
|
3
|
+
import helmet from "helmet";
|
|
4
|
+
|
|
5
|
+
import routes from "./routes/index.js";
|
|
6
|
+
import { env } from "./configs/env.js";
|
|
7
|
+
|
|
8
|
+
const app: Application = express();
|
|
9
|
+
|
|
10
|
+
app.use(helmet());
|
|
11
|
+
|
|
12
|
+
app.use(express.json());
|
|
13
|
+
|
|
14
|
+
app.use("/api/v1", routes);
|
|
15
|
+
|
|
16
|
+
// Unhandled routes
|
|
17
|
+
app.use((req, _res, next) => {
|
|
18
|
+
next(new AppError(`Route ${req.originalUrl} not found`, 404));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Global error middleware
|
|
22
|
+
app.use(GlobalErrorHandler(env.NODE_ENV === "production"));
|
|
23
|
+
|
|
24
|
+
export default app;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import Joi from "joi";
|
|
2
|
+
import { loadEnv, logger } from "elseware-nodejs";
|
|
3
|
+
|
|
4
|
+
export interface EnvConfig {
|
|
5
|
+
NODE_ENV: "development" | "production" | "test";
|
|
6
|
+
PORT: number;
|
|
7
|
+
DATABASE_URL: string;
|
|
8
|
+
JWT_SECRET: string;
|
|
9
|
+
LOG_LEVEL: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const schema = Joi.object<EnvConfig>({
|
|
13
|
+
NODE_ENV: Joi.string()
|
|
14
|
+
.valid("development", "production", "test")
|
|
15
|
+
.default("development"),
|
|
16
|
+
PORT: Joi.number().default(4000),
|
|
17
|
+
DATABASE_URL: Joi.string().required(),
|
|
18
|
+
JWT_SECRET: Joi.string().required(),
|
|
19
|
+
LOG_LEVEL: Joi.string().default("info"),
|
|
20
|
+
}).unknown(true);
|
|
21
|
+
|
|
22
|
+
const value = loadEnv(schema);
|
|
23
|
+
|
|
24
|
+
logger.success("Configurations passed");
|
|
25
|
+
|
|
26
|
+
export const env: EnvConfig = value;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Request, Response } from "express";
|
|
2
|
+
import { ApiResponse } from "elseware-nodejs";
|
|
3
|
+
|
|
4
|
+
export const healthCheck = (_req: Request, res: Response): Response => {
|
|
5
|
+
return ApiResponse.ok(
|
|
6
|
+
res,
|
|
7
|
+
{
|
|
8
|
+
status: "ok",
|
|
9
|
+
service: "elseware-nodejs-template",
|
|
10
|
+
timestamp: new Date().toISOString(),
|
|
11
|
+
},
|
|
12
|
+
"Service is healthy"
|
|
13
|
+
);
|
|
14
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { APIFactory } from "elseware-nodejs";
|
|
2
|
+
|
|
3
|
+
import { PostModel } from "../models/post.model.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SIMPLE CRUD → APIFactory (no service needed)
|
|
7
|
+
*/
|
|
8
|
+
export const getPosts = APIFactory.getAll(PostModel, {
|
|
9
|
+
message: "Posts fetched successfully",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const getPostById = APIFactory.getOne(PostModel, {
|
|
13
|
+
notFoundMessage: "Post not found",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const createPost = APIFactory.createOne(PostModel, {
|
|
17
|
+
message: "Post created successfully",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const updatePost = APIFactory.updateOne(PostModel, {
|
|
21
|
+
message: "Post updated successfully",
|
|
22
|
+
notFoundMessage: "Post not found",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const deletePost = APIFactory.deleteOne(PostModel, {
|
|
26
|
+
notFoundMessage: "Post not found",
|
|
27
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { asyncHandler, ApiResponse } from "elseware-nodejs";
|
|
2
|
+
|
|
3
|
+
import { userService } from "../services/user.service.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* COMPLEX CRUD → service needed
|
|
7
|
+
*/
|
|
8
|
+
export const createUser = asyncHandler(async (req, res) => {
|
|
9
|
+
const user = await userService.create(req.body);
|
|
10
|
+
ApiResponse.created(res, user, "User created successfully");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const getUsers = asyncHandler(async (_req, res) => {
|
|
14
|
+
const users = await userService.findAll();
|
|
15
|
+
ApiResponse.ok(res, users, "Users fetched successfully", {
|
|
16
|
+
count: users.length,
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const getUserById = asyncHandler(async (req, res) => {
|
|
21
|
+
const user = await userService.findById(req.params.id);
|
|
22
|
+
ApiResponse.ok(res, user);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const updateUser = asyncHandler(async (req, res) => {
|
|
26
|
+
const user = await userService.updateById(req.params.id, req.body);
|
|
27
|
+
ApiResponse.ok(res, user);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const deleteUser = asyncHandler(async (req, res) => {
|
|
31
|
+
await userService.deleteById(req.params.id);
|
|
32
|
+
ApiResponse.noContent(res);
|
|
33
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import mongoose, { Schema, InferSchemaType } from "mongoose";
|
|
2
|
+
|
|
3
|
+
const postSchema = new Schema(
|
|
4
|
+
{
|
|
5
|
+
title: {
|
|
6
|
+
type: String,
|
|
7
|
+
required: true,
|
|
8
|
+
trim: true,
|
|
9
|
+
},
|
|
10
|
+
content: {
|
|
11
|
+
type: String,
|
|
12
|
+
required: true,
|
|
13
|
+
},
|
|
14
|
+
published: {
|
|
15
|
+
type: Boolean,
|
|
16
|
+
default: false,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
timestamps: true,
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export type Post = InferSchemaType<typeof postSchema>;
|
|
25
|
+
|
|
26
|
+
export const PostModel = mongoose.model<Post>("Post", postSchema);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import mongoose, { Schema, InferSchemaType } from "mongoose";
|
|
2
|
+
|
|
3
|
+
const userSchema = new Schema(
|
|
4
|
+
{
|
|
5
|
+
email: {
|
|
6
|
+
type: String,
|
|
7
|
+
required: true,
|
|
8
|
+
unique: true,
|
|
9
|
+
lowercase: true,
|
|
10
|
+
trim: true,
|
|
11
|
+
},
|
|
12
|
+
name: {
|
|
13
|
+
type: String,
|
|
14
|
+
default: "Anonymous",
|
|
15
|
+
trim: true,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
timestamps: true,
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export type User = InferSchemaType<typeof userSchema>;
|
|
24
|
+
|
|
25
|
+
export const UserModel = mongoose.model<User>("User", userSchema);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import healthRoutes from "./health.routes.js";
|
|
3
|
+
import userRoutes from "./user.routes.js";
|
|
4
|
+
import postRoutes from "./post.routes.js";
|
|
5
|
+
|
|
6
|
+
const router = Router();
|
|
7
|
+
|
|
8
|
+
router.use("/health", healthRoutes);
|
|
9
|
+
router.use("/users", userRoutes);
|
|
10
|
+
router.use("/posts", postRoutes);
|
|
11
|
+
|
|
12
|
+
export default router;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import {
|
|
3
|
+
getPosts,
|
|
4
|
+
getPostById,
|
|
5
|
+
createPost,
|
|
6
|
+
updatePost,
|
|
7
|
+
deletePost,
|
|
8
|
+
} from "../controllers/post.controller.js";
|
|
9
|
+
|
|
10
|
+
const router = Router();
|
|
11
|
+
|
|
12
|
+
router.post("/", createPost);
|
|
13
|
+
router.get("/", getPosts);
|
|
14
|
+
router.get("/:id", getPostById);
|
|
15
|
+
router.put("/:id", updatePost);
|
|
16
|
+
router.delete("/:id", deletePost);
|
|
17
|
+
|
|
18
|
+
export default router;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import {
|
|
3
|
+
createUser,
|
|
4
|
+
getUsers,
|
|
5
|
+
getUserById,
|
|
6
|
+
updateUser,
|
|
7
|
+
deleteUser,
|
|
8
|
+
} from "../controllers/user.controller.js";
|
|
9
|
+
|
|
10
|
+
const router = Router();
|
|
11
|
+
|
|
12
|
+
router.post("/", createUser);
|
|
13
|
+
router.get("/", getUsers);
|
|
14
|
+
router.get("/:id", getUserById);
|
|
15
|
+
router.put("/:id", updateUser);
|
|
16
|
+
router.delete("/:id", deleteUser);
|
|
17
|
+
|
|
18
|
+
export default router;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { logger } from "elseware-nodejs";
|
|
2
|
+
|
|
3
|
+
import app from "./app.js";
|
|
4
|
+
import { env } from "./configs/env.js";
|
|
5
|
+
import { connectDatabase } from "./configs/database.js";
|
|
6
|
+
|
|
7
|
+
const startServer = async (): Promise<void> => {
|
|
8
|
+
logger.configure({
|
|
9
|
+
timestamp: true,
|
|
10
|
+
enabled: env.NODE_ENV !== "production",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
await connectDatabase();
|
|
14
|
+
|
|
15
|
+
app.listen(env.PORT, () => {
|
|
16
|
+
logger.success(`App running on port ${env.PORT} as ${env.NODE_ENV} mode`);
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
startServer();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { UpdateQuery } from "mongoose";
|
|
2
|
+
import { BaseService, CrudService, AppError } from "elseware-nodejs";
|
|
3
|
+
|
|
4
|
+
import { UserModel, type User } from "../models/user.model.js";
|
|
5
|
+
|
|
6
|
+
class UserService extends BaseService<User> implements CrudService<User> {
|
|
7
|
+
constructor() {
|
|
8
|
+
super(UserModel, "user");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Override create with domain-specific logic
|
|
13
|
+
*/
|
|
14
|
+
override async create(data: Partial<User>): Promise<User> {
|
|
15
|
+
const existing = await UserModel.findOne({
|
|
16
|
+
email: data.email,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (existing) {
|
|
20
|
+
throw new AppError("User already exists", 400, {
|
|
21
|
+
code: "USER_ALREADY_EXISTS",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return super.create(data);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Optional: override update if needed later
|
|
30
|
+
*/
|
|
31
|
+
override async updateById(
|
|
32
|
+
id: string,
|
|
33
|
+
data: UpdateQuery<User>
|
|
34
|
+
): Promise<User> {
|
|
35
|
+
return super.updateById(id, data);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const userService = new UserService();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import request from "supertest";
|
|
2
|
+
import app from "../src/app.js";
|
|
3
|
+
|
|
4
|
+
describe("User API", () => {
|
|
5
|
+
it("creates a user", async () => {
|
|
6
|
+
const res = await request(app)
|
|
7
|
+
.post("/api/users")
|
|
8
|
+
.send({ email: "test@mail.com" });
|
|
9
|
+
|
|
10
|
+
expect(res.statusCode).toBe(201);
|
|
11
|
+
expect(res.body.email).toBe("test@mail.com");
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"rootDir": "src",
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"noImplicitAny": false
|
|
14
|
+
},
|
|
15
|
+
"include": ["src"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0.0",
|
|
3
|
+
"description": "Sample elseware App for NodeJs",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "server.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx watch src/server.ts",
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "node dist/server.js",
|
|
10
|
+
"test": "NODE_ENV=test node --experimental-vm-modules ./node_modules/.bin/jest",
|
|
11
|
+
"coverage": "NODE_ENV=test node --experimental-vm-modules ./node_modules/.bin/jest --coverage",
|
|
12
|
+
"lint": "eslint .",
|
|
13
|
+
"lint:fix": "eslint . --fix"
|
|
14
|
+
},
|
|
15
|
+
"license": "ISC"
|
|
16
|
+
}
|