feat: use gpt to create project, readme
This commit is contained in:
2
server/.env.example
Normal file
2
server/.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
MONGODB_URI=mongodb://localhost:27017/travel-journal
|
||||
PORT=4000
|
13
server/.eslintrc.cjs
Normal file
13
server/.eslintrc.cjs
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
|
||||
env: {
|
||||
node: true
|
||||
}
|
||||
};
|
4
server/.gitignore
vendored
Normal file
4
server/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
coverage
|
33
server/package.json
Normal file
33
server/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "traveling-around-the-world-server",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"start": "node dist/index.js",
|
||||
"lint": "eslint src --ext .ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"helmet": "^7.1.0",
|
||||
"mongoose": "^8.3.3",
|
||||
"morgan": "^1.10.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/morgan": "^1.9.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.4.2"
|
||||
}
|
||||
}
|
27
server/src/app.ts
Normal file
27
server/src/app.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import morgan from 'morgan';
|
||||
import { visitRouter } from './modules/visits/visit.routes';
|
||||
|
||||
export function createApp() {
|
||||
const app = express();
|
||||
|
||||
app.use(helmet());
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(morgan('dev'));
|
||||
|
||||
app.get('/health', (_req, res) => {
|
||||
res.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
app.use('/api/visits', visitRouter);
|
||||
|
||||
app.use((err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
||||
console.error(err);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
15
server/src/config/env.ts
Normal file
15
server/src/config/env.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'dotenv/config';
|
||||
|
||||
const get = (key: string, fallback?: string): string => {
|
||||
const value = process.env[key] ?? fallback;
|
||||
if (!value) {
|
||||
throw new Error(`Missing environment variable: ${key}`);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
export const env = {
|
||||
nodeEnv: process.env.NODE_ENV ?? 'development',
|
||||
port: parseInt(process.env.PORT ?? '4000', 10),
|
||||
mongoUri: get('MONGODB_URI', 'mongodb://localhost:27017/travel-journal')
|
||||
};
|
18
server/src/index.ts
Normal file
18
server/src/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createApp } from './app';
|
||||
import { connectMongo } from './infra/db/mongo';
|
||||
import { env } from './config/env';
|
||||
|
||||
async function bootstrap() {
|
||||
try {
|
||||
await connectMongo();
|
||||
const app = createApp();
|
||||
app.listen(env.port, () => {
|
||||
console.log(`API server ready at http://localhost:${env.port}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize server', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
bootstrap();
|
10
server/src/infra/db/mongo.ts
Normal file
10
server/src/infra/db/mongo.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { env } from '../../config/env';
|
||||
|
||||
export async function connectMongo(): Promise<void> {
|
||||
await mongoose.connect(env.mongoUri);
|
||||
}
|
||||
|
||||
export function disconnectMongo(): Promise<void> {
|
||||
return mongoose.disconnect();
|
||||
}
|
49
server/src/modules/visits/visit.controller.ts
Normal file
49
server/src/modules/visits/visit.controller.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { Request, Response } from 'express';
|
||||
import {
|
||||
createVisit,
|
||||
deleteVisit,
|
||||
getVisit,
|
||||
listVisits,
|
||||
updateVisit
|
||||
} from './visit.service';
|
||||
import { visitInputSchema, visitUpdateSchema } from './visit.types';
|
||||
import { toVisitDto } from './visit.mapper';
|
||||
|
||||
export async function listVisitsHandler(_req: Request, res: Response) {
|
||||
const visits = await listVisits();
|
||||
res.json(visits.map(toVisitDto));
|
||||
}
|
||||
|
||||
export async function createVisitHandler(req: Request, res: Response) {
|
||||
const parsed = visitInputSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ errors: parsed.error.flatten() });
|
||||
}
|
||||
const visit = await createVisit(parsed.data);
|
||||
res.status(201).json(toVisitDto(visit));
|
||||
}
|
||||
|
||||
export async function getVisitHandler(req: Request, res: Response) {
|
||||
const visit = await getVisit(req.params.id);
|
||||
if (!visit) {
|
||||
return res.status(404).json({ message: 'Visit not found' });
|
||||
}
|
||||
res.json(toVisitDto(visit));
|
||||
}
|
||||
|
||||
export async function updateVisitHandler(req: Request, res: Response) {
|
||||
const parsed = visitUpdateSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ errors: parsed.error.flatten() });
|
||||
}
|
||||
const visit = await updateVisit(req.params.id, parsed.data);
|
||||
if (!visit) {
|
||||
return res.status(404).json({ message: 'Visit not found' });
|
||||
}
|
||||
res.json(toVisitDto(visit));
|
||||
}
|
||||
|
||||
export async function deleteVisitHandler(req: Request, res: Response) {
|
||||
await deleteVisit(req.params.id);
|
||||
res.status(204).send();
|
||||
}
|
24
server/src/modules/visits/visit.mapper.ts
Normal file
24
server/src/modules/visits/visit.mapper.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { VisitDoc } from './visit.model';
|
||||
import type { VisitDto } from '../../../shared/visit';
|
||||
|
||||
export function toVisitDto(doc: VisitDoc): VisitDto {
|
||||
const json = doc.toJSON();
|
||||
return {
|
||||
id: json.id,
|
||||
location: {
|
||||
country: json.location.country,
|
||||
city: json.location.city,
|
||||
lat: json.location.lat,
|
||||
lng: json.location.lng
|
||||
},
|
||||
date: {
|
||||
start: new Date(json.date.start).toISOString(),
|
||||
end: json.date.end ? new Date(json.date.end).toISOString() : undefined
|
||||
},
|
||||
notes: json.notes,
|
||||
tags: json.tags,
|
||||
photos: json.photos,
|
||||
createdAt: new Date(json.createdAt).toISOString(),
|
||||
updatedAt: new Date(json.updatedAt).toISOString()
|
||||
};
|
||||
}
|
50
server/src/modules/visits/visit.model.ts
Normal file
50
server/src/modules/visits/visit.model.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Schema, model, type Document } from 'mongoose';
|
||||
|
||||
export interface VisitDoc extends Document {
|
||||
location: {
|
||||
country: string;
|
||||
city?: string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
};
|
||||
date: {
|
||||
start: Date;
|
||||
end?: Date;
|
||||
};
|
||||
notes?: string;
|
||||
tags?: string[];
|
||||
photos?: string[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const visitSchema = new Schema<VisitDoc>(
|
||||
{
|
||||
location: {
|
||||
country: { type: String, required: true },
|
||||
city: { type: String },
|
||||
lat: { type: Number, required: true },
|
||||
lng: { type: Number, required: true }
|
||||
},
|
||||
date: {
|
||||
start: { type: Date, required: true },
|
||||
end: { type: Date }
|
||||
},
|
||||
notes: { type: String },
|
||||
tags: [{ type: String }],
|
||||
photos: [{ type: String }]
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
visitSchema.set('toJSON', {
|
||||
virtuals: true,
|
||||
versionKey: false,
|
||||
transform: (_doc, ret) => {
|
||||
ret.id = ret._id;
|
||||
delete ret._id;
|
||||
return ret;
|
||||
}
|
||||
});
|
||||
|
||||
export const VisitModel = model<VisitDoc>('Visit', visitSchema);
|
16
server/src/modules/visits/visit.routes.ts
Normal file
16
server/src/modules/visits/visit.routes.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Router } from 'express';
|
||||
import {
|
||||
createVisitHandler,
|
||||
deleteVisitHandler,
|
||||
getVisitHandler,
|
||||
listVisitsHandler,
|
||||
updateVisitHandler
|
||||
} from './visit.controller';
|
||||
|
||||
export const visitRouter = Router();
|
||||
|
||||
visitRouter.get('/', listVisitsHandler);
|
||||
visitRouter.post('/', createVisitHandler);
|
||||
visitRouter.get('/:id', getVisitHandler);
|
||||
visitRouter.put('/:id', updateVisitHandler);
|
||||
visitRouter.delete('/:id', deleteVisitHandler);
|
23
server/src/modules/visits/visit.service.ts
Normal file
23
server/src/modules/visits/visit.service.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { FilterQuery } from 'mongoose';
|
||||
import { VisitModel, type VisitDoc } from './visit.model';
|
||||
import type { VisitInput, VisitUpdateInput } from './visit.types';
|
||||
|
||||
export async function listVisits(filter: FilterQuery<VisitDoc> = {}): Promise<VisitDoc[]> {
|
||||
return VisitModel.find(filter).sort({ 'date.start': -1 }).exec();
|
||||
}
|
||||
|
||||
export async function getVisit(id: string): Promise<VisitDoc | null> {
|
||||
return VisitModel.findById(id).exec();
|
||||
}
|
||||
|
||||
export async function createVisit(data: VisitInput): Promise<VisitDoc> {
|
||||
return VisitModel.create(data);
|
||||
}
|
||||
|
||||
export async function updateVisit(id: string, data: VisitUpdateInput): Promise<VisitDoc | null> {
|
||||
return VisitModel.findByIdAndUpdate(id, data, { new: true }).exec();
|
||||
}
|
||||
|
||||
export async function deleteVisit(id: string): Promise<void> {
|
||||
await VisitModel.findByIdAndDelete(id).exec();
|
||||
}
|
26
server/src/modules/visits/visit.types.ts
Normal file
26
server/src/modules/visits/visit.types.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const dateSchema = z.coerce.date();
|
||||
|
||||
export const visitSchema = z.object({
|
||||
location: z.object({
|
||||
country: z.string().min(1),
|
||||
city: z.string().optional(),
|
||||
lat: z.number(),
|
||||
lng: z.number()
|
||||
}),
|
||||
date: z.object({
|
||||
start: dateSchema,
|
||||
end: dateSchema.optional()
|
||||
}),
|
||||
notes: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
photos: z.array(z.string().url()).optional()
|
||||
});
|
||||
|
||||
export const visitInputSchema = visitSchema;
|
||||
|
||||
export const visitUpdateSchema = visitSchema.partial();
|
||||
|
||||
export type VisitInput = z.infer<typeof visitInputSchema>;
|
||||
export type VisitUpdateInput = z.infer<typeof visitUpdateSchema>;
|
8
server/tsconfig.build.json
Normal file
8
server/tsconfig.build.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": false,
|
||||
"noEmit": false
|
||||
}
|
||||
}
|
17
server/tsconfig.json
Normal file
17
server/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "dist",
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
Reference in New Issue
Block a user