Initial commit

This commit is contained in:
2025-07-21 20:35:19 +00:00
commit f10bf53522
29 changed files with 22699 additions and 0 deletions

6
.babelrc Normal file
View File

@ -0,0 +1,6 @@
{
"presets": [
["@babel/preset-env", {"targets": {"node": "current"}}],
"@babel/preset-typescript"
]
}

8
.env.example Normal file
View File

@ -0,0 +1,8 @@
#ENVIRONMENT Defauld production
ENV=
#WHITELIST URLS Default to http://localhost
WHITELIST_URLS=
#PLAYGROUND GRAPHQL Default to "false"
PLAYGROUND_GRAPHQL=
# PORT EXPOSE APP Default to 4000
PORT=

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# dependencies
/node_modules
/build
.env

1
.npmrc Normal file
View File

@ -0,0 +1 @@
//registry.npmjs.org/:_authToken=${NPM_PERSONAL_TOKEN}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Alejandro Lembke Barrientos
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

11
PRNameGenerator.ts Normal file
View File

@ -0,0 +1,11 @@
const PRName = function () {
let ID = '';
// let characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const characters = '0123456789';
for ( let i = 0; i < 6; i++ ) {
ID += characters.charAt(Math.floor(Math.random() * 10));
}
return 'PR-'+ID;
};
console.log(PRName());

75
README.md Normal file
View File

@ -0,0 +1,75 @@
# Create Node TS GraphQL Server
This project aims to have a starter kit for creating a new Node with typescript, GraphQL server and tools that generally go along with it.
Tech(Library or Framework) | Version |
--- | --- |
Jest (Testing) | 29.7.0
Typescript | 5.6.2
GraphQL | 16.9.0
Type GraphQL | 2.0.0-rc.2
## Setup
To create a new project run in the terminal:
```
npx @aleleba/create-node-ts-graphql-server server-app-name
```
Then run:
```
cd server-app-name
```
You will need to create a new .env file at the root of the project for global config.
This is an example of config.
```
#ENVIRONMENT Defauld production
ENVIRONMENT=development
#WHITELIST URLS Default to http://localhost
WHITELIST_URLS=https://someurl.com
#PLAYGROUND GRAPHQL Default to "false"
PLAYGROUND_GRAPHQL=true
# PORT EXPOSE APP Default to 4000
PORT=4000
```
The default environment is production, the server-app port defauld is 4000, the default whitelist is http://localhost and the default graphiql is false.
### For Development
In the terminal run:
```
npm run start:dev
```
The ENV enviroment variable should be "development" and choose the port of your preference with the enviroment variable PORT.
You will find the controllers on:
```
scr/controllers/
```
You will find the models on:
```
scr/models
```
You will find the GraphQL server, resolvers and schema definition on:
```
scr/GraphQL
```
The manage of the routes for custom API you should find on:
```
scr/routes
```
This will start the app in development mode, also use nodemon and webpack to real time coding!
Enjoy coding!
### For Production
In the terminal run:
```
npm run build
```
It will create a build folder and run:
```
npm start
```
This will start the app.
## Cheers
Hope you enjoy this proyect! Sincerely Alejandro Lembke Barrientos.

17
config/index.ts Normal file
View File

@ -0,0 +1,17 @@
import * as dotenv from 'dotenv';
dotenv.config();
export const deFaultValues = {
ENV: 'production',
PLAYGROUND_GRAPHQL: 'false',
WHITELIST_URLS: 'http://localhost',
PORT: '4000',
};
export const config = {
ENV: process.env.ENV,
PLAYGROUND_GRAPHQL: process.env.PLAYGROUND_GRAPHQL === 'true' ? true : false,
WHITELIST_URLS: process.env.WHITELIST_URLS ? process.env.WHITELIST_URLS.split(',') : deFaultValues.WHITELIST_URLS,
PORT: process.env.PORT,
};

55
eslint.config.js Normal file
View File

@ -0,0 +1,55 @@
import globals from 'globals';
import tseslint from 'typescript-eslint';
import js from '@eslint/js';
export default [
// Ignorar archivos y carpetas especificados en el antiguo .eslintignore
{
ignores: [
'.eslintrc.js', // Aunque se eliminará, es bueno mantenerlo por si acaso
'build/',
'webpack.config.ts',
'webpack.config.dev.ts',
],
},
// Configuración recomendada por ESLint
js.configs.recommended,
// Configuraciones recomendadas por typescript-eslint
...tseslint.configs.recommended,
// Configuración personalizada
{
languageOptions: {
ecmaVersion: 2021,
sourceType: 'module',
globals: {
...globals.browser,
...globals.node,
},
// El parser ya está configurado por tseslint.configs.recommended
},
// Los plugins ya están configurados por tseslint.configs.recommended
rules: {
// Reglas personalizadas del antiguo .eslintrc.js
'indent': [
'error',
'tab'
],
'linebreak-style': [
'error',
'unix'
],
'quotes': [
'error',
'single'
],
'semi': [
'error',
'always'
],
// Puedes añadir o sobrescribir reglas de las configuraciones recomendadas aquí si es necesario
},
}
];

15
jest.config.js Normal file
View File

@ -0,0 +1,15 @@
const { pathsToModuleNameMapper } = require('ts-jest');
const { compilerOptions } = require('./tsconfig');
const aliases = pathsToModuleNameMapper(compilerOptions.paths, {
prefix: '<rootDir>'
});
module.exports = {
testEnvironment: 'node',
transform: {
"^.+\\.ts$": "ts-jest"
},moduleNameMapper: {
...aliases,
},
};

21924
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

90
package.json Normal file
View File

@ -0,0 +1,90 @@
{
"name": "drawio-cline-mcp-server",
"version": "0.0.1",
"description": "Node with Typescript and GraphQL Server",
"main": "index.js",
"scripts": {
"start": "node build/index.js",
"start:dev": "webpack-cli --config webpack.config.dev.ts",
"start:nodemon": "nodemon build/index.js",
"build": "webpack-cli --config webpack.config.ts",
"lint": "eslint ./ --ext .js --ext .ts",
"lint:fix": "eslint ./ --ext .js --ext .ts --fix",
"test": "jest",
"test:watch": "jest --watch",
"check-updates": "npx npm-check-updates -u && npm i --legacy-peer-deps"
},
"repository": {
"type": "git",
"url": "git+https://github.com/aleleba/node-ts-graphql-server.git"
},
"keywords": [
"node",
"express",
"typescript",
"graphql",
"server"
],
"author": "Alejandro Lembke Barrientos",
"license": "MIT",
"bugs": {
"url": "https://github.com/aleleba/node-ts-graphql-server/issues"
},
"homepage": "https://github.com/aleleba/node-ts-graphql-server#readme",
"dependencies": {
"@graphql-tools/schema": "^10.0.25",
"body-parser": "^2.2.0",
"class-validator": "^0.14.2",
"cookie-parse": "^0.4.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^17.2.0",
"express": "^5.1.0",
"graphql": "^16.11.0",
"graphql-http": "^1.22.4",
"graphql-playground-middleware-express": "^1.7.23",
"graphql-scalars": "^1.24.2",
"graphql-subscriptions": "^3.0.0",
"graphql-tools": "^9.0.20",
"graphql-ws": "^6.0.6",
"reflect-metadata": "^0.2.2",
"type-graphql": "^2.0.0-rc.2",
"web-push": "^3.6.7",
"ws": "^8.18.3"
},
"devDependencies": {
"@babel/core": "^7.28.0",
"@babel/preset-env": "^7.28.0",
"@babel/preset-typescript": "^7.27.1",
"@babel/register": "^7.27.1",
"@types/body-parser": "^1.19.6",
"@types/cookie-parser": "^1.4.9",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.3",
"@types/jest": "^30.0.0",
"@types/node": "^24.0.15",
"@types/supertest": "^6.0.3",
"@types/webpack": "^5.28.5",
"@types/webpack-node-externals": "^3.0.4",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.38.0",
"@typescript-eslint/parser": "^8.38.0",
"babel-loader": "^10.0.0",
"clean-webpack-plugin": "^4.0.0",
"compression-webpack-plugin": "^11.1.0",
"eslint": "^9.31.0",
"eslint-webpack-plugin": "^5.0.2",
"jest": "^30.0.4",
"nodemon": "^3.1.10",
"resolve-ts-aliases": "^1.0.1",
"supertest": "^7.1.3",
"ts-jest": "^29.4.0",
"ts-loader": "^9.5.2",
"typescript": "^5.8.3",
"webpack": "^5.100.2",
"webpack-cli": "^6.0.1",
"webpack-manifest-plugin": "^5.0.1",
"webpack-node-externals": "^3.0.0",
"webpack-shell-plugin-next": "^2.3.2"
}
}

20
schema.gql Normal file
View File

@ -0,0 +1,20 @@
# -----------------------------------------------
# !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!!
# !!! DO NOT MODIFY THIS FILE BY YOURSELF !!!
# -----------------------------------------------
type Mutation {
testMutation: TestMutation!
}
type Query {
test: Test!
}
type Test {
text: String!
}
type TestMutation {
text(text: String!): String!
}

20
schema.graphql Normal file
View File

@ -0,0 +1,20 @@
# -----------------------------------------------
# !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!!
# !!! DO NOT MODIFY THIS FILE BY YOURSELF !!!
# -----------------------------------------------
type Mutation {
testMutation: TestMutation!
}
type Query {
test: Test!
}
type Test {
text: String!
}
type TestMutation {
text(text: String!): String!
}

14
src/@types/custom.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
// index.d.ts
declare module "*.gql" {
const content: any;
export default content;
}
declare module 'cookie-parse' {
const content: any;
export default content;
}
declare module 'graphql-ws/use/ws' {
export * from 'graphql-ws/dist/use/ws';
}

View File

@ -0,0 +1 @@
export * from './test.resolver';

View File

@ -0,0 +1,33 @@
/* eslint-disable no-mixed-spaces-and-tabs */
'use strict';
import { Query, Resolver, Mutation, FieldResolver, Root, Arg } from 'type-graphql';
import { Test, TestMutation } from '@GraphQL/schema';
import { getTest, addText } from '@controllerGraphQL';
@Resolver(() => Test)
export class TestResolverQuery {
@Query(() => Test)
async test() {
return {};
}
@FieldResolver(() => String)
async text() {
return await getTest({});
}
}
@Resolver(() => TestMutation)
export class TestResolverMutation {
@Mutation(() => TestMutation)
async testMutation() {
return {};
}
@FieldResolver(() => String)
async text(@Arg('text') text?: string){
return await addText({text});
}
}

View File

@ -0,0 +1,18 @@
'use strict'
import { buildSchemaSync } from "type-graphql"
import {
TestResolverQuery,
TestResolverMutation
} from "@GraphQL/resolvers";
export * from './test.schema'
const schema = buildSchemaSync({
resolvers: [
TestResolverQuery,
TestResolverMutation,
],
emitSchemaFile: true,
})
export default schema

View File

@ -0,0 +1,16 @@
/* eslint-disable no-mixed-spaces-and-tabs */
'use strict';
import { Field, ObjectType } from 'type-graphql';
@ObjectType()
export class Test {
@Field(() => String)
text?: string;
}
@ObjectType()
export class TestMutation {
@Field(() => String)
text?: string;
}

30
src/GraphQL/server.ts Normal file
View File

@ -0,0 +1,30 @@
'use strict';
import express from 'express'; //express
import { createHandler } from 'graphql-http/lib/use/express';
import schema from '@src/GraphQL/schema';
const server = express.Router();//Router de Express
server.use(
'/',
createHandler({
schema,
context(req) {
const res = req.context.res
return {req, res};
},
})
);
// DO NOT DO app.listen() unless we're testing this directly
if (require.main === module) {
const app = express();
app.listen((process.env.PORT || 4000), () => {
console.log(`Iniciando Express en el puerto 4000`); /*${app.get('port')}*/
});
}
// Instead do export the app:
export default server;

View File

@ -0,0 +1,13 @@
'use strict';
import { getTestModel, addTextModel } from '@models';
// eslint-disable-next-line
export const getTest = async ({}) => {
return getTestModel();
};
// eslint-disable-next-line
export const addText = async ({ text }: {text?: string}) => {
return addTextModel({ text });
};

91
src/index.ts Normal file
View File

@ -0,0 +1,91 @@
'use strict';
import 'reflect-metadata';
import ws from 'ws'; // yarn add ws
import express from 'express'; //express
import cors from 'cors';
import cookieParser from 'cookie-parser';
import { useServer } from 'graphql-ws/use/ws';
import { execute, subscribe } from 'graphql';
import GraphQLserver from '@GraphQL/server';// Server of GraphQL,
import expressPlayground from 'graphql-playground-middleware-express';
import schema from '@GraphQL/schema';
import { config } from '@config';
import apiRouter from '@routes';
const app = express(), //creating app
whitelist = config.WHITELIST_URLS,
corsOptions = {
origin: function (origin: string | undefined, callback: (arg0: Error | null, arg1?: boolean) => void) {
if (whitelist.indexOf(origin as string) !== -1 || !origin) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
};
//Inicialization of services of express
app
.use(cookieParser())
.use(express.urlencoded({limit: '500mb', extended: true}))
.use(express.json({limit: '500mb'}))
.use(cors(corsOptions))
.use(apiRouter)//Routes de App
.use('/graphql', GraphQLserver);//Server of Graphql
if(config.PLAYGROUND_GRAPHQL === true){
app.get('/playground', expressPlayground({
endpoint: '/graphql',
subscriptionEndpoint: '/graphql',
settings: {
'request.credentials': 'include', //Include Credentials for playground
},
}));
}
// DO NOT DO app.listen() unless we're testing this directly
if (require.main === module) {
const server = app.listen(config.PORT, () => {
// create and use the websocket server
const wsServer = new ws.Server({
server,
path: '/graphql',
});
useServer({
schema,
execute,
subscribe,
// eslint-disable-next-line
onConnect: (ctx) => {
//console.log('Connect');
},
// eslint-disable-next-line
onSubscribe: (ctx, msg) => {
//console.log('Subscribe');
},
// eslint-disable-next-line
onNext: (ctx, msg, args, result) => {
//console.debug('Next');
},
// eslint-disable-next-line
onError: (ctx, msg, errors) => {
//console.error('Error');
},
// eslint-disable-next-line
onComplete: (ctx, msg) => {
//console.log('Complete');
},
}, wsServer);
console.log(`Starting Express on port ${config.PORT} and iniciating server of web sockets`);
});
}
// Instead do export the app:
export default app;

9
src/models/index.ts Normal file
View File

@ -0,0 +1,9 @@
'use strict';
export const getTestModel = async () => {
return 'This is the text response for Test Query from a model';
};
export const addTextModel = async ({ text }: {text?: string}) => {
return `Simulate to insert some text: ${text} from a model`;
};

13
src/routes/index.ts Normal file
View File

@ -0,0 +1,13 @@
'use strict';
// use this to set API REST
import express from 'express';
import bodyParser from 'body-parser';//bodyParser conversionde Api REST,
const apiRouter = express.Router();//Router de Express
apiRouter
.use(bodyParser.json())
.use(bodyParser.urlencoded({extended: false}));
export default apiRouter;

View File

@ -0,0 +1,41 @@
import server from '@src';
import supertest from 'supertest';
describe('global server tests', () => {
let request: supertest.SuperTest<supertest.Test>;
beforeEach( async () => {
request = await supertest(server) as unknown as supertest.SuperTest<supertest.Test>;
});
it('should return Test data from test Query', async () => {
const bodyResponse = {
data: {
test: {
text: 'This is the text response for Test Query from a model'
}
}
};
const response = await request.get('/graphql?query=%7B%0A%20%20test%7B%0A%20%20%20%20text%0A%20%20%7D%0A%7D')
.set('Accept', 'application/json');
expect(response.status).toEqual(200);
expect(response.body).toEqual(bodyResponse);
});
it('should return Test data from test Mutation', async () => {
const bodyResponse = {
data: {
testMutation: {
text: 'Simulate to insert some text: testing text from a model'
}
}
};
const response = await request.post('/graphql')
.send({'query': `mutation{
testMutation{
text(text: "testing text")
}
}`})
.set('Accept', 'application/json');
expect(response.status).toEqual(200);
expect(response.body).toEqual(bodyResponse);
});
});

32
tsconfig.json Normal file
View File

@ -0,0 +1,32 @@
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"target": "es2021",
"moduleResolution": "node",
"sourceMap": true,
"typeRoots" : ["./src/@types", "./node_modules/@types"],
"strict": true,
"forceConsistentCasingInFileNames": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"@src/*": ["src/*"],
"@src": ["src"],
"@routes*": ["src/routes/*"],
"@routes": ["src/routes"],
"@controllers/*": ["src/controllers/*"],
"@controllers": ["src/controllers"],
"@models/*": ["src/models/*"],
"@models": ["src/models"],
"@controllerGraphQL/*": ["src/controllers/controllerGraphQL/*"],
"@controllerGraphQL": ["src/controllers/controllerGraphQL"],
"@GraphQL/*": ["src/GraphQL/*"],
"@GraphQL": ["src/GraphQL"],
"@config/*": ["config/*"],
"@config": ["config"]
}
},
"lib": ["es2018", "esnext.asynciterable"]
}

60
webpack.config.dev.ts Normal file
View File

@ -0,0 +1,60 @@
import path from 'path';
import webpack from 'webpack';
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
import ESLintPlugin from 'eslint-webpack-plugin';
import nodeExternals from 'webpack-node-externals';
import WebpackShellPluginNext from 'webpack-shell-plugin-next';
import { resolveTsAliases } from 'resolve-ts-aliases';
import { deFaultValues } from './config';
const ROOT_DIR = path.resolve(__dirname);
const resolvePath = (...args: string[]) => path.resolve(ROOT_DIR, ...args);
const BUILD_DIR = resolvePath('build');
const alias = resolveTsAliases(path.resolve('tsconfig.json'));
const config = {
entry: './src/index.ts',
target: 'node',
watch: true,
externals: [nodeExternals()],
output: {
path: BUILD_DIR,
filename: 'index.js',
},
resolve: {
extensions: ['.js', '.ts', '.json', '.gql'],
alias,
},
mode: 'development',
module: {
rules: [
{
test: /\.(js|ts|mjs|gql)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /\.(ts)$/, loader: "ts-loader",
exclude: /node_modules/
},
],
},
plugins: [
new CleanWebpackPlugin(),
new ESLintPlugin(),
new webpack.EnvironmentPlugin({
...deFaultValues
}),
new WebpackShellPluginNext({
onBuildEnd: {
scripts: ['nodemon build/index.js'],
blocking: false,
parallel: true
}
})
],
};
export default config;

58
webpack.config.ts Normal file
View File

@ -0,0 +1,58 @@
import path from 'path';
import webpack from 'webpack';
import TerserPlugin from 'terser-webpack-plugin';
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
import ESLintPlugin from 'eslint-webpack-plugin';
import nodeExternals from 'webpack-node-externals';
import { resolveTsAliases } from 'resolve-ts-aliases';
import { deFaultValues } from './config';
const ROOT_DIR = path.resolve(__dirname);
const resolvePath = (...args: string[]) => path.resolve(ROOT_DIR, ...args);
const BUILD_DIR = resolvePath('build');
const alias = resolveTsAliases(path.resolve('tsconfig.json'));
const config = {
entry: './src/index.ts',
target: 'node',
externals: [nodeExternals()],
output: {
path: BUILD_DIR,
filename: 'index.js',
},
resolve: {
extensions: ['.js', '.ts', '.json', '.gql'],
alias,
},
mode: 'production',
module: {
rules: [
{
test: /\.(js|ts|mjs|gql)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /\.(ts)$/, loader: "ts-loader",
exclude: /node_modules/
},
],
},
plugins: [
new CleanWebpackPlugin(),
new ESLintPlugin(),
new webpack.EnvironmentPlugin({
...deFaultValues
}),
],
optimization: {
minimize: true,
minimizer: [
new TerserPlugin(),
],
},
};
export default config;