diff --git a/.babelrc b/.babelrc deleted file mode 100644 index d401b0ae..00000000 --- a/.babelrc +++ /dev/null @@ -1 +0,0 @@ -{ "plugins": ["@babel/plugin-syntax-jsx"] } diff --git a/.gitignore b/.gitignore index f44aef3c..09aa265d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .env /node_modules /http -.vscode \ No newline at end of file +.vscode +dist \ No newline at end of file diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 6db23d5e..00000000 --- a/.prettierrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "singleQuote": true, - "semi": true, - "useTabs": false, - "tabWidth": 2, - "trailingComma": "all", - "printWidth": 130, - "arrowParens": "avoid", - "bracketSpacing": true, - "endOfLine": "auto" -} diff --git a/app_mongoose.js b/app_mongoose.js deleted file mode 100644 index 0b34fa8d..00000000 --- a/app_mongoose.js +++ /dev/null @@ -1,111 +0,0 @@ -import express from 'express'; -import mongoose from 'mongoose'; -import * as dotenv from 'dotenv'; -import cors from 'cors'; -import Product from './models/Product.js'; - -const app = express(); -app.use(cors()); -app.use(express.json()); -dotenv.config(); -mongoose - .connect(process.env.DATABASE_URL) - .then(() => console.log('Connected to DB')); -app.listen(process.env.PORT || 3000, () => console.log('Server Started')); - -const MESSAGES = Object.freeze({ - NOID: 'Cannot find given id.', - IDFORMAT: 'Invalid id format.' -}); - -// handler를 인자로 받아서 오류처리 해주는 함수 -function asyncHandler(handler) { - return async (req, res) => { - try { - await handler(req, res); - } catch (e) { - if (e.name === 'ValidationError') { - res.status(400).send({ message: e.message }); - } else if (e.name === 'CastError') { - res.status(404).send({ message: MESSAGES.IDFORMAT }); - } else { - res.status(500).send({ message: e.message }); - } - } - }; -} - -// get API -app.get( - '/products', - asyncHandler(async (req, res) => { - const orderBy = req.query.orderBy || 'recent'; - const page = Number(req.query.page) || 1; - const pageSize = Number(req.query.pageSize) || 10; - const keyword = req.query.keyword; - - const sortOption = { createdAt: orderBy === 'recent' ? 'asc' : 'desc' }; - const searchOption = keyword ? { $text: { $search: keyword } } : {}; - - const query = Product.find(searchOption); - const count = await query.clone().countDocuments(); - const products = await query - .sort(sortOption) - .skip((page - 1) * pageSize) - .limit(pageSize); - - const response = { list: products, totalCount: count }; - - res.send(response); - }) -); - -// get :id API -app.get( - '/products/:id', - asyncHandler(async (req, res) => { - const id = req.params.id; - const product = await Product.findById(id); - - if (product) res.send(product); - else res.status(404).send({ message: MESSAGES.NOID }); - }) -); - -// post API -app.post( - '/products', - asyncHandler(async (req, res) => { - const newProduct = await Product.create(req.body); - - res.status(201).send(newProduct); - }) -); - -// patch API -app.patch( - '/products/:id', - asyncHandler(async (req, res) => { - const id = req.params.id; - const product = await Product.findById(id); - if (product) { - Object.keys(req.body).forEach((k) => { - product[k] = req.body[k]; - }); - await product.save(); - res.send(product); - } else res.status(404).send({ message: MESSAGES.NOID }); - }) -); - -// delete API -app.delete( - '/products/:id', - asyncHandler(async (req, res) => { - const id = req.params.id; - const product = await Product.findByIdAndDelete(id); - if (product) { - res.sendStatus(204); - } else res.status(404).send({ message: MESSAGES.NOID }); - }) -); diff --git a/eslint.config.js b/eslint.config.js index c3eaf482..d7c37960 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,52 +1,163 @@ -import globals from 'globals'; import pluginJs from '@eslint/js'; +import prettierConfig from 'eslint-config-prettier'; import pluginImport from 'eslint-plugin-import'; -import pluginJsxA11y from 'eslint-plugin-jsx-a11y'; import pluginPrettier from 'eslint-plugin-prettier'; -import prettierConfig from 'eslint-config-prettier'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; export default [ - { files: ['**/*.{js,mjs,cjs,jsx,ts,tsx}'] }, + { files: ['**/*.{js,mjs,cjs,ts}'] }, { languageOptions: { - globals: { ...globals.browser, ...globals.node }, + globals: { ...globals.node }, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], }, }, { - settings: { - react: { - version: 'detect', + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parser: tseslint.parser, + parserOptions: { + project: './tsconfig.json', }, }, - }, - pluginJs.configs.recommended, - prettierConfig, - { plugins: { + '@typescript-eslint': tseslint.plugin, prettier: pluginPrettier, - 'jsx-a11y': pluginJsxA11y, import: pluginImport, }, }, + { + settings: { + 'import/resolver': { + node: true, + alias: { + map: [ + ['#configs', './src/configs'], + ['#connection', './src/connection'], + ['#containers', './src/containers'], + ['#controllers', './src/controllers'], + ['#interfaces', './src/interfaces'], + ['#middlewares', './src/middlewares'], + ['#repositories', './src/repositories'], + ['#routes', './src/routes'], + ['#services', './src/services'], + ['#types', './src/types'], + ['#utils', './src/utils'], + ['@', './'], + ], + extensions: ['.js', '.ts', '.json'], + }, + }, + 'import/internal-regex': '@/', + }, + }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + prettierConfig, { rules: { 'prettier/prettier': ['error', { endOfLine: 'auto' }], 'no-restricted-globals': 'off', - 'no-lone-blocks': 'off', 'no-unused-vars': 'off', - 'react/react-in-jsx-scope': 'off', // Next.js에서는 필요 없음 - 'react/jsx-uses-react': 'off', // React 17+에서는 필요 없음 - 'import/extensions': 'off', - 'no-bitwise': 'off', - 'react/prop-types': 'off', 'consistent-return': 'off', - 'jsx-a11y/click-events-have-key-events': 'off', - 'jsx-a11y/no-static-element-interactions': 'off', - 'jsx-a11y/no-noninteractive-element-interactions': 'off', - 'jsx-a11y/label-has-associated-control': ['error', { some: ['nesting', 'id'] }], - 'guard-for-in': 'off', - 'no-underscore-dangle': 'off', - camelcase: 'off', + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['.*'], + message: "상대 경로 import는 허용되지 않습니다. '#/'로 시작하는 alias를 사용해주세요.", + }, + ], + }, + ], + 'import/no-relative-parent-imports': 'error', + 'import/no-relative-packages': 'error', + 'import/extensions': ['error', { js: 'never', ts: 'never' }], + 'import/no-duplicates': ['warn', { 'prefer-inline': true, considerQueryString: true }], + 'import/order': [ + 'warn', + { + groups: ['builtin', 'external', 'internal'], + 'newlines-between': 'never', + distinctGroup: false, + pathGroups: [ + { + pattern: '#configs/**', + group: 'internal', + position: 'before', + }, + { + pattern: '#connection/**', + group: 'internal', + position: 'before', + }, + { + pattern: '#containers/**', + group: 'internal', + position: 'before', + }, + { + pattern: '#controllers/**', + group: 'internal', + position: 'before', + }, + { + pattern: '#interfaces/**', + group: 'internal', + position: 'before', + }, + { + pattern: '#middlewares/**', + group: 'internal', + position: 'before', + }, + { + pattern: '#repositories/**', + group: 'internal', + position: 'before', + }, + { + pattern: '#routes/**', + group: 'internal', + position: 'before', + }, + { + pattern: '#services/**', + group: 'internal', + position: 'before', + }, + { + pattern: '#types/**', + group: 'internal', + position: 'before', + }, + { + pattern: '#utils/**', + group: 'internal', + position: 'before', + }, + { + pattern: '@/**', + group: 'internal', + position: 'after', + }, + { + pattern: './**', + group: 'internal', + position: 'after', + }, + ], + pathGroupsExcludedImportTypes: ['builtin'], + warnOnUnassignedImports: true, + alphabetize: { + order: 'asc', + caseInsensitive: false, + }, + }, + ], }, }, ]; diff --git a/jsconfig.json b/jsconfig.json deleted file mode 100644 index d8e34b77..00000000 --- a/jsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - // "paths": { - // "@/*": ["./*"] - // }, - "module": "NodeNext", - "moduleResolution": "NodeNext", - "target": "ESNext", - "allowJs": true - } -} diff --git a/package-lock.json b/package-lock.json index f91e20a7..dfadb071 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "PandaMarket_BE", + "name": "BE", "lockfileVersion": 3, "requires": true, "packages": { @@ -7,27 +7,582 @@ "dependencies": { "@prisma/client": "^5.21.1", "cookie-parser": "^1.4.7", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "express": "^4.19.2", "express-async-errors": "^3.1.1", "express-jwt": "^8.4.1", "is-email": "^1.0.2", + "express-jwt": "^8.4.1", + "is-email": "^1.0.2", "is-uuid": "^1.0.2", "jsonwebtoken": "^9.0.2", - "mongoose": "^8.6.1", "multer": "^1.4.5-lts.1", - "prisma": "^5.21.1", "superstruct": "^2.0.2" }, "devDependencies": { "@eslint/js": "^9.13.0", + "@trivago/prettier-plugin-sort-imports": "^5.2.0", + "@types/cookie-parser": "^1.4.8", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/is-email": "^1.0.0", + "@types/is-uuid": "^1.0.2", + "@types/multer": "^1.4.12", "eslint": "^9.13.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jsx-a11y": "^6.10.1", "eslint-plugin-prettier": "^5.2.1", "globals": "^15.11.0", - "nodemon": "^3.1.4" + "prisma": "^5.21.1", + "tsx": "^4.19.2", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", + "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.3", + "@babel/types": "^7.26.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", + "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", + "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.3", + "@babel/parser": "^7.26.3", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", + "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -233,13 +788,95 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@mongodb-js/saslprep": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", - "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, "license": "MIT", "dependencies": { - "sparse-bitfield": "^3.0.3" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" } }, "node_modules/@pkgr/core": { @@ -277,12 +914,14 @@ "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -296,12 +935,14 @@ "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "5.22.0", @@ -313,6 +954,7 @@ "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "5.22.0" @@ -325,10 +967,133 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "node_modules/@trivago/prettier-plugin-sort-imports": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.0.tgz", + "integrity": "sha512-yEIJ7xMKYQwyNRjxSdi4Gs37iszikAjxfky+3hu9bn24u8eHLJNDMAoOTyowp8p6EpSl8IQMdkfBx+WnJTttsw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/generator": "^7.26.2", + "@babel/parser": "^7.26.2", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", + "javascript-natural-sort": "^0.7.1", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">18.12" + }, + "peerDependencies": { + "@vue/compiler-sfc": "3.x", + "prettier": "2.x - 3.x", + "prettier-plugin-svelte": "3.x", + "svelte": "4.x" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + }, + "svelte": { + "optional": true + } + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz", + "integrity": "sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz", + "integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/is-email": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/is-email/-/is-email-1.0.0.tgz", + "integrity": "sha512-b/76ooKpYY/b+oPrOuc/pmM5eag+ZlzctPsKcRCIKs+TFzh0FL58OeXtSPkbXt3uKNK84YCKHmjnoREtwve5Kg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/is-uuid": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/is-uuid/-/is-uuid-1.0.2.tgz", + "integrity": "sha512-S+gWwUEApOjGCCO5LQrft4kciGWatvB0LyiyWTXSlDkclZBr6glSgstET573GsC5QPFdw2NeOw2PHOOMuTDFSQ==", "dev": true, "license": "MIT" }, @@ -355,28 +1120,286 @@ "@types/node": "*" } }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", + "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { - "version": "22.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", - "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "license": "MIT", "dependencies": { - "undici-types": "~6.19.8" + "undici-types": "~6.20.0" } }, - "node_modules/@types/webidl-conversions": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "node_modules/@types/qs": { + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", + "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "dev": true, "license": "MIT" }, - "node_modules/@types/whatwg-url": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", - "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz", + "integrity": "sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/type-utils": "8.18.0", + "@typescript-eslint/utils": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.0.tgz", + "integrity": "sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q==", + "dev": true, + "license": "MITClause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/typescript-estree": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz", + "integrity": "sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.0.tgz", + "integrity": "sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.18.0", + "@typescript-eslint/utils": "8.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.0.tgz", + "integrity": "sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz", + "integrity": "sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.0.tgz", + "integrity": "sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/typescript-estree": "8.18.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz", + "integrity": "sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==", + "dev": true, "license": "MIT", "dependencies": { - "@types/webidl-conversions": "*" + "@typescript-eslint/types": "8.18.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/accepts": { @@ -448,20 +1471,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -475,16 +1484,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/array-buffer-byte-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", @@ -611,13 +1610,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true, - "license": "MIT" - }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -634,26 +1626,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/axe-core": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz", - "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==", - "dev": true, - "license": "MPL-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -661,19 +1633,6 @@ "dev": true, "license": "MIT" }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -737,15 +1696,6 @@ "node": ">=8" } }, - "node_modules/bson": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.9.0.tgz", - "integrity": "sha512-X9hJeyeM0//Fus+0pc5dSUMhhrrmWwQUtdavaQeF3Ta6m69matZkGWV/MrBcnwUeLC8W9kwwc2hfkZgUuCX3Ig==", - "license": "Apache-2.0", - "engines": { - "node": ">=16.20.1" - } - }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -824,44 +1774,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -904,6 +1816,21 @@ "typedarray": "^0.0.6" } }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -922,13 +1849,35 @@ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -968,6 +1917,12 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -996,13 +1951,6 @@ "node": ">= 8" } }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -1061,6 +2009,7 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1148,6 +2097,13 @@ "node": ">=0.10.0" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -1163,13 +2119,6 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -1317,6 +2266,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1504,36 +2493,6 @@ "ms": "^2.1.1" } }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", - "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "aria-query": "^5.3.2", - "array-includes": "^3.1.8", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "^4.10.0", - "axobject-query": "^4.1.0", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "hasown": "^2.0.2", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.1" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" - } - }, "node_modules/eslint-plugin-prettier": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", @@ -1739,6 +2698,26 @@ "integrity": "sha512-wj4tLMyCVYuIIKHGt0FhCtIViBcwzWejX0EjNxveAa6dG+0XBCQhMbx+PnkLkFCxLC69qoFrxds4pIyL88inaQ==", "license": "MIT" }, + "node_modules/express-jwt": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-8.4.1.tgz", + "integrity": "sha512-IZoZiDv2yZJAb3QrbaSATVtTCYT11OcqgFGoTN4iKVyN6NBkBkhtVIixww5fmakF0Upt5HfOxJuS6ZmJVeOtTQ==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9", + "express-unless": "^2.1.3", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/express-unless": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-2.1.3.tgz", + "integrity": "sha512-wj4tLMyCVYuIIKHGt0FhCtIViBcwzWejX0EjNxveAa6dG+0XBCQhMbx+PnkLkFCxLC69qoFrxds4pIyL88inaQ==", + "license": "MIT" + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1768,6 +2747,36 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -1782,6 +2791,16 @@ "dev": true, "license": "MIT" }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -1911,6 +2930,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -1996,6 +3016,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2051,6 +3084,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -2173,13 +3213,6 @@ "node": ">= 4" } }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" - }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -2267,19 +3300,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -2364,6 +3384,12 @@ "integrity": "sha512-UojUgD2EhDTBQ2SGKwrK9edce5phRzgLsP+V5+Uu2Swi+uvjVXgH3zduM3HhT9iaC/9Kq19/TYUbP0jPoi6ioA==", "license": "SEE LICENSE IN LICENSE" }, + "node_modules/is-email": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-email/-/is-email-1.0.2.tgz", + "integrity": "sha512-UojUgD2EhDTBQ2SGKwrK9edce5phRzgLsP+V5+Uu2Swi+uvjVXgH3zduM3HhT9iaC/9Kq19/TYUbP0jPoi6ioA==", + "license": "SEE LICENSE IN LICENSE" + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2540,6 +3566,20 @@ "dev": true, "license": "ISC" }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2553,6 +3593,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2621,22 +3674,6 @@ "node": ">=10" } }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -2658,15 +3695,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/kareem": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", - "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2677,26 +3705,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", - "dev": true, - "license": "MIT", - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -2727,6 +3735,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -2776,6 +3791,12 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -2785,12 +3806,6 @@ "node": ">= 0.6" } }, - "node_modules/memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "license": "MIT" - }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -2800,6 +3815,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -2809,6 +3834,20 @@ "node": ">= 0.6" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -2849,130 +3888,31 @@ "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mongodb": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.10.0.tgz", - "integrity": "sha512-gP9vduuYWb9ZkDM546M+MP2qKVk5ZG2wPF63OvSRuUbqCR+11ZCAE1mOfllhlAG0wcoJY5yDL/rV3OmYEwXIzg==", - "license": "Apache-2.0", - "dependencies": { - "@mongodb-js/saslprep": "^1.1.5", - "bson": "^6.7.0", - "mongodb-connection-string-url": "^3.0.0" - }, - "engines": { - "node": ">=16.20.1" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", - "socks": "^2.7.1" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } - } - }, - "node_modules/mongodb-connection-string-url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", - "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", - "license": "Apache-2.0", - "dependencies": { - "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^13.0.0" - } - }, - "node_modules/mongoose": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.0.tgz", - "integrity": "sha512-KluvgwnQB1GPOYZZXUHJRjS1TW6xxwTlf/YgjWExuuNanIe3W7VcR7dDXQVCIRk8L7NYge8EnoTcu2grWtN+XQ==", - "license": "MIT", - "dependencies": { - "bson": "^6.7.0", - "kareem": "2.6.3", - "mongodb": "~6.10.0", - "mpath": "0.9.0", - "mquery": "5.0.0", - "ms": "2.1.3", - "sift": "17.1.3" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16.20.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mongoose" + "node": "*" } }, - "node_modules/mpath": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", - "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", - "engines": { - "node": ">=4.0.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mquery": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", - "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "license": "MIT", "dependencies": { - "debug": "4.x" + "minimist": "^1.2.6" }, - "engines": { - "node": ">=14.0.0" + "bin": { + "mkdirp": "bin/cmd.js" } }, "node_modules/ms": { @@ -3015,81 +3955,6 @@ "node": ">= 0.6" } }, - "node_modules/nodemon": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", - "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nodemon/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3309,6 +4174,13 @@ "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", "license": "MIT" }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -3376,6 +4248,7 @@ "version": "5.22.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -3397,6 +4270,12 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3410,17 +4289,11 @@ "node": ">= 0.10" } }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, - "license": "MIT" - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3441,6 +4314,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -3492,19 +4386,6 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", @@ -3552,6 +4433,51 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-array-concat": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", @@ -3768,47 +4694,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sift": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", - "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", - "license": "MIT" - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "license": "MIT", - "dependencies": { - "memory-pager": "^1.0.2" - } - }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -3841,21 +4726,6 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, - "node_modules/string.prototype.includes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", - "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -4012,26 +4882,17 @@ "node": ">=0.6" } }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", "dev": true, - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, - "node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", "license": "MIT", - "dependencies": { - "punycode": "^2.3.0" - }, "engines": { - "node": ">=14" + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" } }, "node_modules/tsconfig-paths": { @@ -4054,6 +4915,26 @@ "dev": true, "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", + "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4163,6 +5044,43 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.0.tgz", + "integrity": "sha512-Xq2rRjn6tzVpAyHr3+nmSg1/9k9aIHnJ2iZeOH7cfGOWqTkXTm3kwpQglEuLGdNrYvPF+2gtAs+/KF5rjVo+WQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.18.0", + "@typescript-eslint/parser": "8.18.0", + "@typescript-eslint/utils": "8.18.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -4179,17 +5097,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true, - "license": "MIT" - }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "license": "MIT" }, "node_modules/unpipe": { @@ -4217,6 +5128,12 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -4235,28 +5152,6 @@ "node": ">= 0.8" } }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", - "license": "MIT", - "dependencies": { - "tr46": "^4.1.1", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4329,6 +5224,15 @@ "node": ">=0.4" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 48445ce7..d5f546bc 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,62 @@ { "type": "module", + "imports": { + "#configs/*": "./dist/src/configs/*", + "#connection/*": "./dist/src/connection/*", + "#containers/*": "./dist/src/containers/*", + "#controllers/*": "./dist/src/controllers/*", + "#interfaces/*": "./dist/src/interfaces/*", + "#middlewares/*": "./dist/src/middlewares/*", + "#repositories/*": "./dist/src/repositories/*", + "#routes/*": "./dist/src/routes/*", + "#services/*": "./dist/src/services/*", + "#types/*": "./dist/src/types/*", + "#utils/*": "./dist/src/utils/*", + "@/*": "./dist/*" + }, "scripts": { - "dev": "nodemon src/app.js", - "start": "node --env-file=.env src/app.js", - "seed": "node --env-file=.env data/seed.js" + "dev": "tsx --env-file=.env --watch src/app.ts", + "start": "node --env-file=.env dist/src/app.js", + "build": "tsc", + "lint": "eslint .", + "format": "prettier --write ." + }, + "prisma": { + "seed": "node prisma/seed.js" }, "dependencies": { "@prisma/client": "^5.21.1", "cookie-parser": "^1.4.7", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "express": "^4.19.2", "express-async-errors": "^3.1.1", "express-jwt": "^8.4.1", "is-email": "^1.0.2", + "express-jwt": "^8.4.1", + "is-email": "^1.0.2", "is-uuid": "^1.0.2", "jsonwebtoken": "^9.0.2", - "mongoose": "^8.6.1", "multer": "^1.4.5-lts.1", - "prisma": "^5.21.1", "superstruct": "^2.0.2" }, "devDependencies": { "@eslint/js": "^9.13.0", + "@trivago/prettier-plugin-sort-imports": "^5.2.0", + "@types/cookie-parser": "^1.4.8", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/is-email": "^1.0.0", + "@types/is-uuid": "^1.0.2", + "@types/multer": "^1.4.12", "eslint": "^9.13.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jsx-a11y": "^6.10.1", "eslint-plugin-prettier": "^5.2.1", "globals": "^15.11.0", - "nodemon": "^3.1.4" + "prisma": "^5.21.1", + "tsx": "^4.19.2", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.0" } } diff --git a/prettier.config.mjs b/prettier.config.mjs new file mode 100644 index 00000000..68b6a07e --- /dev/null +++ b/prettier.config.mjs @@ -0,0 +1,29 @@ +export default { + singleQuote: true, + semi: true, + useTabs: false, + tabWidth: 2, + trailingComma: 'all', + printWidth: 130, + arrowParens: 'avoid', + bracketSpacing: true, + endOfLine: 'auto', + + plugins: ['@trivago/prettier-plugin-sort-imports'], + importOrder: [ + '^#configs/(.*)$', + '^#connection/(.*)$', + '^#containers/(.*)$', + '^#controllers/(.*)$', + '^#interfaces/(.*)$', + '^#middlewares/(.*)$', + '^#repositories/(.*)$', + '^#routes/(.*)$', + '^#services/(.*)$', + '^#types/(.*)$', + '^#utils/(.*)$', + '^@/(.*)$', + '^[./]', + ], + importOrderSortSpecifiers: true, +}; diff --git a/prisma/migrations/20241113070145_add_product_user_ownership/migration.sql b/prisma/migrations/20241113070145_add_product_user_ownership/migration.sql deleted file mode 100644 index 30a57129..00000000 --- a/prisma/migrations/20241113070145_add_product_user_ownership/migration.sql +++ /dev/null @@ -1,38 +0,0 @@ -/* - Warnings: - - - You are about to drop the `_ProductToUser` table. If the table is not empty, all the data it contains will be lost. - -*/ --- DropForeignKey -ALTER TABLE "_ProductToUser" DROP CONSTRAINT "_ProductToUser_A_fkey"; - --- DropForeignKey -ALTER TABLE "_ProductToUser" DROP CONSTRAINT "_ProductToUser_B_fkey"; - --- AlterTable -ALTER TABLE "Product" ADD COLUMN "ownerId" TEXT; - --- DropTable -DROP TABLE "_ProductToUser"; - --- CreateTable -CREATE TABLE "_UserLikesProduct" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "_UserLikesProduct_AB_unique" ON "_UserLikesProduct"("A", "B"); - --- CreateIndex -CREATE INDEX "_UserLikesProduct_B_index" ON "_UserLikesProduct"("B"); - --- AddForeignKey -ALTER TABLE "Product" ADD CONSTRAINT "Product_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_UserLikesProduct" ADD CONSTRAINT "_UserLikesProduct_A_fkey" FOREIGN KEY ("A") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_UserLikesProduct" ADD CONSTRAINT "_UserLikesProduct_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20241113070503_product_owner_to_not_null/migration.sql b/prisma/migrations/20241113070503_product_owner_to_not_null/migration.sql deleted file mode 100644 index d181d58f..00000000 --- a/prisma/migrations/20241113070503_product_owner_to_not_null/migration.sql +++ /dev/null @@ -1,14 +0,0 @@ -/* - Warnings: - - - Made the column `ownerId` on table `Product` required. This step will fail if there are existing NULL values in that column. - -*/ --- DropForeignKey -ALTER TABLE "Product" DROP CONSTRAINT "Product_ownerId_fkey"; - --- AlterTable -ALTER TABLE "Product" ALTER COLUMN "ownerId" SET NOT NULL; - --- AddForeignKey -ALTER TABLE "Product" ADD CONSTRAINT "Product_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20241114042138_edit_product_image/migration.sql b/prisma/migrations/20241114042138_edit_product_image/migration.sql deleted file mode 100644 index d27efd21..00000000 --- a/prisma/migrations/20241114042138_edit_product_image/migration.sql +++ /dev/null @@ -1,12 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `image` on the `ProductImage` table. All the data in the column will be lost. - - Added the required column `fileName` to the `ProductImage` table without a default value. This is not possible if the table is not empty. - - Added the required column `originalName` to the `ProductImage` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "ProductImage" DROP COLUMN "image", -ADD COLUMN "fileName" TEXT NOT NULL, -ADD COLUMN "originalName" TEXT NOT NULL; diff --git a/prisma/migrations/20241114042833_product_owner_on_delete_to_set_null/migration.sql b/prisma/migrations/20241114042833_product_owner_on_delete_to_set_null/migration.sql deleted file mode 100644 index f8a7e053..00000000 --- a/prisma/migrations/20241114042833_product_owner_on_delete_to_set_null/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ --- DropForeignKey -ALTER TABLE "Product" DROP CONSTRAINT "Product_ownerId_fkey"; - --- AlterTable -ALTER TABLE "Product" ALTER COLUMN "ownerId" DROP NOT NULL; - --- AddForeignKey -ALTER TABLE "Product" ADD CONSTRAINT "Product_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20241114104336_add_email_password_to_user/migration.sql b/prisma/migrations/20241114104336_add_email_password_to_user/migration.sql deleted file mode 100644 index c8333fe6..00000000 --- a/prisma/migrations/20241114104336_add_email_password_to_user/migration.sql +++ /dev/null @@ -1,10 +0,0 @@ -/* - Warnings: - - - Added the required column `email` to the `User` table without a default value. This is not possible if the table is not empty. - - Added the required column `password` to the `User` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "User" ADD COLUMN "email" TEXT NOT NULL, -ADD COLUMN "password" TEXT NOT NULL; diff --git a/prisma/migrations/20241114110150_email_to_unique/migration.sql b/prisma/migrations/20241114110150_email_to_unique/migration.sql deleted file mode 100644 index 5e77f8e9..00000000 --- a/prisma/migrations/20241114110150_email_to_unique/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail. - -*/ --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/prisma/migrations/20241114124514_add_salt_to_user/migration.sql b/prisma/migrations/20241114124514_add_salt_to_user/migration.sql deleted file mode 100644 index e21ea571..00000000 --- a/prisma/migrations/20241114124514_add_salt_to_user/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "User" ADD COLUMN "salt" TEXT NOT NULL DEFAULT ''; diff --git a/prisma/migrations/20241114124644_delete_default_from_salt/migration.sql b/prisma/migrations/20241114124644_delete_default_from_salt/migration.sql deleted file mode 100644 index eb18ccff..00000000 --- a/prisma/migrations/20241114124644_delete_default_from_salt/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "User" ALTER COLUMN "salt" DROP DEFAULT; diff --git a/prisma/migrations/20241114131825_add_refresh_token_to_user/migration.sql b/prisma/migrations/20241114131825_add_refresh_token_to_user/migration.sql deleted file mode 100644 index 701b3730..00000000 --- a/prisma/migrations/20241114131825_add_refresh_token_to_user/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "User" ADD COLUMN "refreshToken" TEXT; diff --git a/prisma/migrations/20241113061446_init/migration.sql b/prisma/migrations/20241213074142_init/migration.sql similarity index 70% rename from prisma/migrations/20241113061446_init/migration.sql rename to prisma/migrations/20241213074142_init/migration.sql index e897c690..c297b943 100644 --- a/prisma/migrations/20241113061446_init/migration.sql +++ b/prisma/migrations/20241213074142_init/migration.sql @@ -1,8 +1,12 @@ -- CreateTable CREATE TABLE "User" ( "id" TEXT NOT NULL, + "email" TEXT NOT NULL, "nickname" TEXT NOT NULL, "image" TEXT, + "password" TEXT NOT NULL, + "salt" TEXT NOT NULL, + "refreshToken" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, @@ -18,6 +22,7 @@ CREATE TABLE "Product" ( "likeCount" INTEGER NOT NULL DEFAULT 0, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, + "ownerId" TEXT, CONSTRAINT "Product_pkey" PRIMARY KEY ("id") ); @@ -34,7 +39,8 @@ CREATE TABLE "ProductTag" ( -- CreateTable CREATE TABLE "ProductImage" ( "id" TEXT NOT NULL, - "image" TEXT NOT NULL, + "originalName" TEXT NOT NULL, + "fileName" TEXT NOT NULL, "productId" TEXT NOT NULL, CONSTRAINT "ProductImage_pkey" PRIMARY KEY ("id") @@ -76,17 +82,20 @@ CREATE TABLE "Comment" ( ); -- CreateTable -CREATE TABLE "_ProductToUser" ( +CREATE TABLE "_UserLikesProduct" ( "A" TEXT NOT NULL, "B" TEXT NOT NULL ); -- CreateTable -CREATE TABLE "_likeOwnership" ( +CREATE TABLE "_UserLikesArticle" ( "A" TEXT NOT NULL, "B" TEXT NOT NULL ); +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + -- CreateIndex CREATE INDEX "Product_createdAt_idx" ON "Product"("createdAt"); @@ -94,16 +103,19 @@ CREATE INDEX "Product_createdAt_idx" ON "Product"("createdAt"); CREATE INDEX "Article_createdAt_idx" ON "Article"("createdAt"); -- CreateIndex -CREATE UNIQUE INDEX "_ProductToUser_AB_unique" ON "_ProductToUser"("A", "B"); +CREATE UNIQUE INDEX "_UserLikesProduct_AB_unique" ON "_UserLikesProduct"("A", "B"); -- CreateIndex -CREATE INDEX "_ProductToUser_B_index" ON "_ProductToUser"("B"); +CREATE INDEX "_UserLikesProduct_B_index" ON "_UserLikesProduct"("B"); -- CreateIndex -CREATE UNIQUE INDEX "_likeOwnership_AB_unique" ON "_likeOwnership"("A", "B"); +CREATE UNIQUE INDEX "_UserLikesArticle_AB_unique" ON "_UserLikesArticle"("A", "B"); -- CreateIndex -CREATE INDEX "_likeOwnership_B_index" ON "_likeOwnership"("B"); +CREATE INDEX "_UserLikesArticle_B_index" ON "_UserLikesArticle"("B"); + +-- AddForeignKey +ALTER TABLE "Product" ADD CONSTRAINT "Product_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "ProductTag" ADD CONSTRAINT "ProductTag_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; @@ -127,13 +139,13 @@ ALTER TABLE "Comment" ADD CONSTRAINT "Comment_articleId_fkey" FOREIGN KEY ("arti ALTER TABLE "Comment" ADD CONSTRAINT "Comment_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "_ProductToUser" ADD CONSTRAINT "_ProductToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "_UserLikesProduct" ADD CONSTRAINT "_UserLikesProduct_A_fkey" FOREIGN KEY ("A") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "_ProductToUser" ADD CONSTRAINT "_ProductToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "_UserLikesProduct" ADD CONSTRAINT "_UserLikesProduct_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "_likeOwnership" ADD CONSTRAINT "_likeOwnership_A_fkey" FOREIGN KEY ("A") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "_UserLikesArticle" ADD CONSTRAINT "_UserLikesArticle_A_fkey" FOREIGN KEY ("A") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "_likeOwnership" ADD CONSTRAINT "_likeOwnership_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "_UserLikesArticle" ADD CONSTRAINT "_UserLikesArticle_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/data/mock.js b/prisma/mock/mock.js similarity index 100% rename from data/mock.js rename to prisma/mock/mock.js diff --git a/uploads/2.jpg b/prisma/mock/mockImages/products/2.jpg similarity index 100% rename from uploads/2.jpg rename to prisma/mock/mockImages/products/2.jpg diff --git a/uploads/2113.png b/prisma/mock/mockImages/products/2113.png similarity index 100% rename from uploads/2113.png rename to prisma/mock/mockImages/products/2113.png diff --git a/uploads/21353.png b/prisma/mock/mockImages/products/21353.png similarity index 100% rename from uploads/21353.png rename to prisma/mock/mockImages/products/21353.png diff --git a/uploads/2313561.png b/prisma/mock/mockImages/products/2313561.png similarity index 100% rename from uploads/2313561.png rename to prisma/mock/mockImages/products/2313561.png diff --git a/uploads/233.png b/prisma/mock/mockImages/products/233.png similarity index 100% rename from uploads/233.png rename to prisma/mock/mockImages/products/233.png diff --git a/uploads/31563.png b/prisma/mock/mockImages/products/31563.png similarity index 100% rename from uploads/31563.png rename to prisma/mock/mockImages/products/31563.png diff --git a/uploads/321351.png b/prisma/mock/mockImages/products/321351.png similarity index 100% rename from uploads/321351.png rename to prisma/mock/mockImages/products/321351.png diff --git a/uploads/3514562.png b/prisma/mock/mockImages/products/3514562.png similarity index 100% rename from uploads/3514562.png rename to prisma/mock/mockImages/products/3514562.png diff --git a/uploads/5146532.png b/prisma/mock/mockImages/products/5146532.png similarity index 100% rename from uploads/5146532.png rename to prisma/mock/mockImages/products/5146532.png diff --git a/uploads/5389615.png b/prisma/mock/mockImages/products/5389615.png similarity index 100% rename from uploads/5389615.png rename to prisma/mock/mockImages/products/5389615.png diff --git a/uploads/CRP-DHP0610FD.png b/prisma/mock/mockImages/products/CRP-DHP0610FD.png similarity index 100% rename from uploads/CRP-DHP0610FD.png rename to prisma/mock/mockImages/products/CRP-DHP0610FD.png diff --git a/uploads/gbu_buying_defalut_pc.png b/prisma/mock/mockImages/products/gbu_buying_defalut_pc.png similarity index 100% rename from uploads/gbu_buying_defalut_pc.png rename to prisma/mock/mockImages/products/gbu_buying_defalut_pc.png diff --git a/uploads/pexels-colour-creation-28649-112811.jpg b/prisma/mock/mockImages/products/pexels-colour-creation-28649-112811.jpg similarity index 100% rename from uploads/pexels-colour-creation-28649-112811.jpg rename to prisma/mock/mockImages/products/pexels-colour-creation-28649-112811.jpg diff --git a/uploads/water-lily-8175845_1280.jpg b/prisma/mock/mockImages/products/water-lily-8175845_1280.jpg similarity index 100% rename from uploads/water-lily-8175845_1280.jpg rename to prisma/mock/mockImages/products/water-lily-8175845_1280.jpg diff --git a/prisma/mock/mock_seq.js b/prisma/mock/mock_seq.js new file mode 100644 index 00000000..72f6ef64 --- /dev/null +++ b/prisma/mock/mock_seq.js @@ -0,0 +1,1324 @@ +const data = [ + { + seq: 1, + name: '쿠쿠 밥솥', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 2, + name: '토스터', + description: '토스트를 만들기 위한 토스터~', + price: 19000, + tags: ['토스터'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/72/1724836584916/2.jpg' + ], + favoriteCount: 0 + }, + { + seq: 3, + name: '화사한분위기의수련', + description: '무료 이미지 꽃, 수련, water lily', + price: 12000, + tags: ['꽃', '수련', 'water lily'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/36/1724120702896/water-lily-8175845_1280.jpg' + ], + favoriteCount: 1 + }, + { + seq: 4, + name: '밝은분위기의벽지와전', + description: '무료 이미지 전등, 벽지, 인테리어', + price: 36000, + tags: ['전등', '벽지', '인테리어'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/36/1724119838524/pexels-colour-creation-28649-112811.jpg' + ], + favoriteCount: 1 + }, + { + seq: 5, + name: '갤럭시북4', + description: '또 다른 갤럭시를 만나보세요', + price: 1000000, + tags: [], + images: [ + 'https://images.samsung.com/kdp/event/sec/2024/0301_galaxy_book4_ultra/buying/slide_v7/gbu_buying_defalut_pc.jpg' + ], + favoriteCount: 1 + }, + { + seq: 6, + name: '갤럭시 탭 S7', + description: '삼성 갤럭시 탭 S7', + price: 350000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991844193/5146532.png' + ], + favoriteCount: 6 + }, + { + seq: 7, + name: '보스 헤드폰', + description: '보스 노이즈 캔슬링 헤드폰 700', + price: 350000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991827255/3514562.png' + ], + favoriteCount: 12 + }, + { + seq: 8, + name: '사무용 의자', + description: '편안한 사무용 의자', + price: 120000, + tags: ['가구'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991812368/2313561.png' + ], + favoriteCount: 4 + }, + { + seq: 9, + name: '스니커즈', + description: '편안한 스니커즈편안한 스니커즈', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 10, + name: '레노버 노트북', + description: '레노버 아이디어패드 5', + price: 800000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991798558/321351.png' + ], + favoriteCount: 4 + }, + { + seq: 11, + name: '갤럭시 S21', + description: '삼성 갤럭시 S21', + price: 600000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991786504/31563.png' + ], + favoriteCount: 8 + }, + { + seq: 12, + name: '아이패드', + description: '애플 아이패드 10.2인치', + price: 450000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991756711/21353.png' + ], + favoriteCount: 15 + }, + { + seq: 13, + name: '퀸사이즈 침대', + description: '퀸사이즈 침대 프레임', + price: 500000, + tags: ['가구'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991744735/2113.png' + ], + favoriteCount: 6 + }, + { + seq: 14, + name: '판다인형', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 15, + name: '맥북 프로', + description: '애플 맥북 프로 13인치', + price: 1500000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991786504/31563.png' + ], + favoriteCount: 7 + }, + { + seq: 16, + name: '초기 데이터', + description: '초기 데이터입니다. 초기 데이터입니다.', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 17, + name: '초기 데이터2', + description: '초기 데이터입니다. 초기 데이터입니다.', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 18, + name: '초기 데이터3', + description: '초기 데이터입니다. 초기 데이터입니다.', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 19, + name: '초기 데이터4', + description: '초기 데이터입니다. 초기 데이터입니다.', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 20, + name: '초기 데이터5', + description: '초기 데이터입니다. 초기 데이터입니다.', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 21, + name: '테스트데이터', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 22, + name: '테스트데이터2', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 23, + name: '테스트데이터3', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 24, + name: '테스트데이터4', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 25, + name: '테스트데이터5', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 26, + name: '검색 테스트용', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 27, + name: '검색 테스트용2', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 28, + name: '검색 테스트용3', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 29, + name: '검색 테스트용4', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 30, + name: '검색 테스트용5', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 31, + name: '쿠쿠 밥솥', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 32, + name: '토스터', + description: '토스트를 만들기 위한 토스터~', + price: 19000, + tags: ['토스터'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/72/1724836584916/2.jpg' + ], + favoriteCount: 0 + }, + { + seq: 33, + name: '화사한분위기의수련', + description: '무료 이미지 꽃, 수련, water lily', + price: 12000, + tags: ['꽃', '수련', 'water lily'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/36/1724120702896/water-lily-8175845_1280.jpg' + ], + favoriteCount: 1 + }, + { + seq: 34, + name: '밝은분위기의벽지와전', + description: '무료 이미지 전등, 벽지, 인테리어', + price: 36000, + tags: ['전등', '벽지', '인테리어'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/36/1724119838524/pexels-colour-creation-28649-112811.jpg' + ], + favoriteCount: 1 + }, + { + seq: 35, + name: '갤럭시북4', + description: '또 다른 갤럭시를 만나보세요', + price: 1000000, + tags: [], + images: [ + 'https://images.samsung.com/kdp/event/sec/2024/0301_galaxy_book4_ultra/buying/slide_v7/gbu_buying_defalut_pc.jpg' + ], + favoriteCount: 1 + }, + { + seq: 36, + name: '갤럭시 탭 S7', + description: '삼성 갤럭시 탭 S7', + price: 350000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991844193/5146532.png' + ], + favoriteCount: 6 + }, + { + seq: 37, + name: '보스 헤드폰', + description: '보스 노이즈 캔슬링 헤드폰 700', + price: 350000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991827255/3514562.png' + ], + favoriteCount: 12 + }, + { + seq: 38, + name: '사무용 의자', + description: '편안한 사무용 의자', + price: 120000, + tags: ['가구'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991812368/2313561.png' + ], + favoriteCount: 4 + }, + { + seq: 39, + name: '스니커즈', + description: '편안한 스니커즈편안한 스니커즈', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 40, + name: '레노버 노트북', + description: '레노버 아이디어패드 5', + price: 800000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991798558/321351.png' + ], + favoriteCount: 4 + }, + { + seq: 41, + name: '갤럭시 S21', + description: '삼성 갤럭시 S21', + price: 600000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991786504/31563.png' + ], + favoriteCount: 8 + }, + { + seq: 42, + name: '아이패드', + description: '애플 아이패드 10.2인치', + price: 450000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991756711/21353.png' + ], + favoriteCount: 15 + }, + { + seq: 43, + name: '퀸사이즈 침대', + description: '퀸사이즈 침대 프레임', + price: 500000, + tags: ['가구'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991744735/2113.png' + ], + favoriteCount: 6 + }, + { + seq: 44, + name: '판다인형', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 45, + name: '맥북 프로', + description: '애플 맥북 프로 13인치', + price: 1500000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991786504/31563.png' + ], + favoriteCount: 7 + }, + { + seq: 46, + name: '초기 데이터', + description: '초기 데이터입니다. 초기 데이터입니다.', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 47, + name: '초기 데이터2', + description: '초기 데이터입니다. 초기 데이터입니다.', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 48, + name: '초기 데이터3', + description: '초기 데이터입니다. 초기 데이터입니다.', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 49, + name: '초기 데이터4', + description: '초기 데이터입니다. 초기 데이터입니다.', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 50, + name: '초기 데이터5', + description: '초기 데이터입니다. 초기 데이터입니다.', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 51, + name: '테스트데이터', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 52, + name: '테스트데이터2', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 53, + name: '테스트데이터3', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 54, + name: '테스트데이터4', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 55, + name: '테스트데이터5', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 56, + name: '검색 테스트용', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 57, + name: '검색 테스트용2', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 58, + name: '검색 테스트용3', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 59, + name: '검색 테스트용4', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 60, + name: '검색 테스트용5', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 61, + name: '쿠쿠 밥솥', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 62, + name: '토스터', + description: '토스트를 만들기 위한 토스터~', + price: 19000, + tags: ['토스터'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/72/1724836584916/2.jpg' + ], + favoriteCount: 0 + }, + { + seq: 63, + name: '화사한분위기의수련', + description: '무료 이미지 꽃, 수련, water lily', + price: 12000, + tags: ['꽃', '수련', 'water lily'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/36/1724120702896/water-lily-8175845_1280.jpg' + ], + favoriteCount: 1 + }, + { + seq: 64, + name: '밝은분위기의벽지와전', + description: '무료 이미지 전등, 벽지, 인테리어', + price: 36000, + tags: ['전등', '벽지', '인테리어'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/36/1724119838524/pexels-colour-creation-28649-112811.jpg' + ], + favoriteCount: 1 + }, + { + seq: 65, + name: '갤럭시북4', + description: '또 다른 갤럭시를 만나보세요', + price: 1000000, + tags: [], + images: [ + 'https://images.samsung.com/kdp/event/sec/2024/0301_galaxy_book4_ultra/buying/slide_v7/gbu_buying_defalut_pc.jpg' + ], + favoriteCount: 1 + }, + { + seq: 66, + name: '갤럭시 탭 S7', + description: '삼성 갤럭시 탭 S7', + price: 350000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991844193/5146532.png' + ], + favoriteCount: 6 + }, + { + seq: 67, + name: '보스 헤드폰', + description: '보스 노이즈 캔슬링 헤드폰 700', + price: 350000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991827255/3514562.png' + ], + favoriteCount: 12 + }, + { + seq: 68, + name: '사무용 의자', + description: '편안한 사무용 의자', + price: 120000, + tags: ['가구'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991812368/2313561.png' + ], + favoriteCount: 4 + }, + { + seq: 69, + name: '스니커즈', + description: '편안한 스니커즈편안한 스니커즈', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 70, + name: '레노버 노트북', + description: '레노버 아이디어패드 5', + price: 800000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991798558/321351.png' + ], + favoriteCount: 4 + }, + { + seq: 71, + name: '갤럭시 S21', + description: '삼성 갤럭시 S21', + price: 600000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991786504/31563.png' + ], + favoriteCount: 8 + }, + { + seq: 72, + name: '아이패드', + description: '애플 아이패드 10.2인치', + price: 450000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991756711/21353.png' + ], + favoriteCount: 15 + }, + { + seq: 73, + name: '퀸사이즈 침대', + description: '퀸사이즈 침대 프레임', + price: 500000, + tags: ['가구'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991744735/2113.png' + ], + favoriteCount: 6 + }, + { + seq: 74, + name: '판다인형', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 75, + name: '맥북 프로', + description: '애플 맥북 프로 13인치', + price: 1500000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991786504/31563.png' + ], + favoriteCount: 7 + }, + { + seq: 76, + name: '초기 데이터', + description: '초기 데이터입니다. 초기 데이터입니다.', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 77, + name: '초기 데이터2', + description: '초기 데이터입니다. 초기 데이터입니다.', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 78, + name: '초기 데이터3', + description: '초기 데이터입니다. 초기 데이터입니다.', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 79, + name: '초기 데이터4', + description: '초기 데이터입니다. 초기 데이터입니다.', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 80, + name: '초기 데이터5', + description: '초기 데이터입니다. 초기 데이터입니다.', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 81, + name: '테스트데이터', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 82, + name: '테스트데이터2', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 83, + name: '테스트데이터3', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 84, + name: '테스트데이터4', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 85, + name: '테스트데이터5', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 86, + name: '검색 테스트용', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 87, + name: '검색 테스트용2', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 88, + name: '검색 테스트용3', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 89, + name: '검색 테스트용4', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 90, + name: '검색 테스트용5', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 91, + name: '쿠쿠 밥솥', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 92, + name: '토스터', + description: '토스트를 만들기 위한 토스터~', + price: 19000, + tags: ['토스터'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/72/1724836584916/2.jpg' + ], + favoriteCount: 0 + }, + { + seq: 93, + name: '화사한분위기의수련', + description: '무료 이미지 꽃, 수련, water lily', + price: 12000, + tags: ['꽃', '수련', 'water lily'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/36/1724120702896/water-lily-8175845_1280.jpg' + ], + favoriteCount: 1 + }, + { + seq: 94, + name: '밝은분위기의벽지와전', + description: '무료 이미지 전등, 벽지, 인테리어', + price: 36000, + tags: ['전등', '벽지', '인테리어'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/36/1724119838524/pexels-colour-creation-28649-112811.jpg' + ], + favoriteCount: 1 + }, + { + seq: 95, + name: '갤럭시북4', + description: '또 다른 갤럭시를 만나보세요', + price: 1000000, + tags: [], + images: [ + 'https://images.samsung.com/kdp/event/sec/2024/0301_galaxy_book4_ultra/buying/slide_v7/gbu_buying_defalut_pc.jpg' + ], + favoriteCount: 1 + }, + { + seq: 96, + name: '갤럭시 탭 S7', + description: '삼성 갤럭시 탭 S7', + price: 350000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991844193/5146532.png' + ], + favoriteCount: 6 + }, + { + seq: 97, + name: '보스 헤드폰', + description: '보스 노이즈 캔슬링 헤드폰 700', + price: 350000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991827255/3514562.png' + ], + favoriteCount: 12 + }, + { + seq: 98, + name: '사무용 의자', + description: '편안한 사무용 의자', + price: 120000, + tags: ['가구'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991812368/2313561.png' + ], + favoriteCount: 4 + }, + { + seq: 99, + name: '스니커즈', + description: '편안한 스니커즈편안한 스니커즈', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 100, + name: '레노버 노트북', + description: '레노버 아이디어패드 5', + price: 800000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991798558/321351.png' + ], + favoriteCount: 4 + }, + { + seq: 101, + name: '갤럭시 S21', + description: '삼성 갤럭시 S21', + price: 600000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991786504/31563.png' + ], + favoriteCount: 8 + }, + { + seq: 102, + name: '아이패드', + description: '애플 아이패드 10.2인치', + price: 450000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991756711/21353.png' + ], + favoriteCount: 15 + }, + { + seq: 103, + name: '퀸사이즈 침대', + description: '퀸사이즈 침대 프레임', + price: 500000, + tags: ['가구'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991744735/2113.png' + ], + favoriteCount: 6 + }, + { + seq: 104, + name: '판다인형', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 105, + name: '맥북 프로', + description: '애플 맥북 프로 13인치', + price: 1500000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991786504/31563.png' + ], + favoriteCount: 7 + }, + { + seq: 106, + name: '초기 데이터', + description: '초기 데이터입니다. 초기 데이터입니다.', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 107, + name: '초기 데이터2', + description: '초기 데이터입니다. 초기 데이터입니다.', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 108, + name: '초기 데이터3', + description: '초기 데이터입니다. 초기 데이터입니다.', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 109, + name: '초기 데이터4', + description: '초기 데이터입니다. 초기 데이터입니다.', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 110, + name: '초기 데이터5', + description: '초기 데이터입니다. 초기 데이터입니다.', + price: 100000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png' + ], + favoriteCount: 10 + }, + { + seq: 111, + name: '테스트데이터', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 112, + name: '테스트데이터2', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 113, + name: '테스트데이터3', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 114, + name: '테스트데이터4', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 115, + name: '테스트데이터5', + description: '귀여운 판다인형입니다', + price: 30000, + tags: [], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991725132/233.png' + ], + favoriteCount: 7 + }, + { + seq: 116, + name: '검색 테스트용', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 117, + name: '검색 테스트용2', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 118, + name: '검색 테스트용3', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 119, + name: '검색 테스트용4', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + }, + { + seq: 120, + name: '검색 테스트용5', + description: '쿠쿠하세요~~쿠쿠하세요~~', + price: 310000, + tags: ['쿠쿠쿠쿠쿸'], + images: [ + 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/92/1725209779217/CRP-DHP0610FD.png' + ], + favoriteCount: 0 + } +]; + +export default data; diff --git a/data/seed.js b/prisma/seed.js similarity index 70% rename from data/seed.js rename to prisma/seed.js index 043359fe..d5e0574e 100644 --- a/data/seed.js +++ b/prisma/seed.js @@ -1,5 +1,11 @@ -import { prismaClient as prisma } from '../src/postgresql/connection/postgres.connection.js'; -import * as mock from './mock.js'; +import { PrismaClient } from '@prisma/client'; +import * as mock from './mock/mock.js'; + +if (!process.env.DATABASE_URL) { + throw new Error('DATABASE_URL 환경변수가 설정되지 않았습니다.'); +} + +const prismaClient = new PrismaClient({ datasourceUrl: process.env.DATABASE_URL }); function getRandomInteger(min, max) { const minCeiled = Math.ceil(min); @@ -11,30 +17,27 @@ function getRandomInteger(min, max) { } async function main() { - // postgres seeding part - // delete part - await prisma.$transaction([ - prisma.productTag.deleteMany(), - prisma.productImage.deleteMany(), - prisma.articleImage.deleteMany(), - prisma.user.deleteMany(), - prisma.product.deleteMany(), - prisma.article.deleteMany(), - prisma.comment.deleteMany(), + await prismaClient.$transaction([ + prismaClient.productTag.deleteMany(), + prismaClient.productImage.deleteMany(), + prismaClient.articleImage.deleteMany(), + prismaClient.user.deleteMany(), + prismaClient.product.deleteMany(), + prismaClient.article.deleteMany(), + prismaClient.comment.deleteMany(), ]); - // create part // 관계형이 아닌 데이터부터 처리 - await prisma.user.createMany({ + await prismaClient.user.createMany({ data: mock.users, skipDuplicates: true, }); // 관계형 데이터 처리 // products - const userIds = (await prisma.user.findMany()).map(u => u.id); + const userIds = (await prismaClient.user.findMany()).map(u => u.id); const newProducts = mock.products.map(product => ({ ...product, ownerId: userIds[getRandomInteger(0, userIds.length - 1)] })); - await prisma.product.createMany({ + await prismaClient.product.createMany({ data: newProducts, skipDuplicates: true, }); @@ -46,14 +49,14 @@ async function main() { ownerId: userIds[getRandomInteger(0, userIds.length - 1)], }; }); - await prisma.article.createMany({ + await prismaClient.article.createMany({ data: newArticles, skipDuplicates: true, }); // comments - const productIds = (await prisma.product.findMany()).map(p => p.id); - const articleIds = (await prisma.article.findMany()).map(a => a.id); + const productIds = (await prismaClient.product.findMany()).map(p => p.id); + const articleIds = (await prismaClient.article.findMany()).map(a => a.id); const newComments = mock.comments.map(comment => { const relatedWithArticles = getRandomInteger(0, 1); // 0 or 1 const relation = relatedWithArticles // 위에서 얻은 결과에 따라 랜덤하게 배정됨 @@ -65,7 +68,7 @@ async function main() { ...relation, }; }); - await prisma.comment.createMany({ + await prismaClient.comment.createMany({ data: newComments, skipDuplicates: true, }); @@ -87,7 +90,7 @@ async function main() { filter.push(tagIndex); } }); - await prisma.productTag.createMany({ + await prismaClient.productTag.createMany({ data: newTags, skipDuplicates: true, }); @@ -99,7 +102,7 @@ async function main() { newProductImages.push({ originalName: mock.productImages[imgIndex], fileName: mock.productImages[imgIndex], productId: id }); }); - await prisma.productImage.createMany({ + await prismaClient.productImage.createMany({ data: newProductImages, skipDuplicates: true, }); @@ -110,7 +113,7 @@ async function main() { newArticleImages.push({ image: mock.productImages[imgIndex], articleId: id }); }); - await prisma.articleImage.createMany({ + await prismaClient.articleImage.createMany({ data: newArticleImages, skipDuplicates: true, }); @@ -118,11 +121,11 @@ async function main() { main() .then(async () => { - await prisma.$disconnect(); + await prismaClient.$disconnect(); }) .catch(async e => { console.log(e); - await prisma.$disconnect(); + await prismaClient.$disconnect(); process.exit(1); }) .finally(() => { diff --git a/src/app.js b/src/app.js deleted file mode 100644 index 2d6b46b3..00000000 --- a/src/app.js +++ /dev/null @@ -1,62 +0,0 @@ -import cookieParser from 'cookie-parser'; -import cors from 'cors'; -import express from 'express'; -import 'express-async-errors'; -import multer from 'multer'; -import errorHandler from './middlewares/error-handler.js'; -import validatePaginationOptions from './middlewares/pagination.validation.js'; -import articleRouter from './routes/articles.route.js'; -import authRouter from './routes/auth.route.js'; -import commentRouter from './routes/comments.route.js'; -import devRouter from './routes/dev.route.js'; -import productRouter from './routes/products.route.js'; - -const app = express(); - -const upload = multer({ dest: 'uploads/' }); -/*********************************************************************************** middlewares **********************************************************************************************/ -app.use(cors({ credentials: true, origin: true })); -app.use(express.json()); -app.use(cookieParser()); -app.use(upload.single('file')); -app.use('/files', express.static('uploads')); -app.use(validatePaginationOptions); - -/*********************************************************************************** routes **********************************************************************************************/ -app.use('/dev', devRouter); -app.use('/auth', authRouter); -app.use('/products', productRouter); -app.use('/articles', articleRouter); -app.use('/comments', commentRouter); - -/*********************************************************************************** handlers **********************************************************************************************/ -app.use(errorHandler); - -const startServer = async () => { - try { - // 기본 포트(3000)로 시도 - await new Promise((resolve, reject) => { - const server = app.listen(process.env.PORT, () => { - console.log(`Server Started on port ${process.env.PORT}`); - resolve(); - }); - - server.on('error', err => { - if (err.code === 'EADDRINUSE') { - // 기본 포트가 사용 중이면 reject - reject(err); - } - }); - }); - } catch (error) { - if (error.code === 'EADDRINUSE') { - // 3000번 포트가 사용 중이면 3001로 시도 - app.listen(3001, () => { - console.log('Server Started on port 3001 (fallback)'); - }); - } else { - console.error('Failed to start server:', error); - } - } -}; -startServer(); diff --git a/src/app.middlewares.ts b/src/app.middlewares.ts new file mode 100644 index 00000000..3b631b34 --- /dev/null +++ b/src/app.middlewares.ts @@ -0,0 +1,24 @@ +import cookieParser from 'cookie-parser'; +import cors from 'cors'; +import express from 'express'; +import multer from 'multer'; +import { runAsyncLocalStorage } from '#middlewares/asyncLocalStorage.js'; +import validatePaginationOptions from '#middlewares/pagination.validation.js'; + +const upload = multer({ dest: 'uploads/' }); + +export default function setupMiddlewares(app: express.Application) { + app.use(runAsyncLocalStorage); + app.use(cors({ credentials: true, origin: true })); + app.use(express.json()); + app.use(cookieParser()); + app.use(upload.single('file')); + app.use('/files', express.static('uploads')); + app.use(validatePaginationOptions); + + app.use((req, res, next) => { + console.log(`Request Path: ${req.path}`); + console.log(`Request Method: ${req.method}`); + next(); + }); +} diff --git a/src/app.routes.ts b/src/app.routes.ts new file mode 100644 index 00000000..b06847e5 --- /dev/null +++ b/src/app.routes.ts @@ -0,0 +1,18 @@ +import express from 'express'; +import articleRouter from '#routes/articles.route.js'; +import authRouter from '#routes/auth.route.js'; +import commentRouter from '#routes/comments.route.js'; +import devRouter from '#routes/dev.route.js'; +import productRouter from '#routes/products.route.js'; + +export default function setupRoutes(app: express.Application) { + app.use('/dev', devRouter); + app.use('/auth', authRouter); + app.use('/products', productRouter); + app.use('/articles', articleRouter); + app.use('/comments', commentRouter); + + app.get('/hello', (req, res) => { + res.send('Hello World'); + }); +} diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 00000000..b760560b --- /dev/null +++ b/src/app.ts @@ -0,0 +1,40 @@ +import express from 'express'; +import 'express-async-errors'; +import setupMiddlewares from '@/src/app.middlewares.js'; +import setupRoutes from '@/src/app.routes.js'; +import errorHandler from './middlewares/error-handler.js'; + +export const app = express(); + +setupMiddlewares(app); +setupRoutes(app); +app.use(errorHandler); + +const startServer = async () => { + try { + // 기본 포트(3000)로 시도 + await new Promise((resolve, reject) => { + const server = app.listen(process.env.PORT, () => { + console.log(`Server Started on port ${process.env.PORT}`); + resolve(null); + }); + + server.on('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EADDRINUSE') { + // 기본 포트가 사용 중이면 reject + reject(err); + } + }); + }); + } catch (error: unknown) { + if (error instanceof Error && 'code' in error && error.code === 'EADDRINUSE') { + // 3000번 포트가 사용 중이면 3001로 시도 + app.listen(3001, () => { + console.log('Server Started on port 3001 (fallback)'); + }); + } else { + console.error('Failed to start server:', error); + } + } +}; +startServer(); diff --git a/src/configs/auth.config.js b/src/configs/auth.config.js deleted file mode 100644 index 4e436b3b..00000000 --- a/src/configs/auth.config.js +++ /dev/null @@ -1 +0,0 @@ -export const jwtSecret = process.env.JWT_SECRET; diff --git a/src/configs/jwt.config.ts b/src/configs/jwt.config.ts new file mode 100644 index 00000000..271b6783 --- /dev/null +++ b/src/configs/jwt.config.ts @@ -0,0 +1,8 @@ +if (!process.env.JWT_SECRET) { + throw new Error('JWT_SECRET 환경변수가 설정되지 않았습니다.'); +} + +export const jwtSecret = process.env.JWT_SECRET; + +export const accessExpireTime = process.env.ACCESS_EXPIRE_TIME || '1h'; +export const refreshExpireTime = process.env.REFRESH_EXPIRE_TIME || '2w'; diff --git a/src/configs/postgres.config.ts b/src/configs/postgres.config.ts new file mode 100644 index 00000000..f2942915 --- /dev/null +++ b/src/configs/postgres.config.ts @@ -0,0 +1,11 @@ +if (!process.env.DATABASE_URL) { + throw new Error('DATABASE_URL 환경변수가 설정되지 않았습니다.'); +} +if (!process.env.DATABASE_URL_render) { + throw new Error('DATABASE_URL_render 환경변수가 설정되지 않았습니다.'); +} + +export const postgresConnectionConfig = { + url: process.env.DATABASE_URL, + render: process.env.DATABASE_URL_render, +}; diff --git a/src/postgresql/connection/postgres.connection.js b/src/connection/postgres.connection.ts similarity index 88% rename from src/postgresql/connection/postgres.connection.js rename to src/connection/postgres.connection.ts index 42acc794..1bd93cb8 100644 --- a/src/postgresql/connection/postgres.connection.js +++ b/src/connection/postgres.connection.ts @@ -1,5 +1,5 @@ import { PrismaClient } from '@prisma/client'; -import { postgresConnectionConfig } from '../configs/postgres.config.js'; +import { postgresConnectionConfig } from '#configs/postgres.config.js'; const { render, url } = postgresConnectionConfig; diff --git a/src/containers/article.container.ts b/src/containers/article.container.ts new file mode 100644 index 00000000..84418a91 --- /dev/null +++ b/src/containers/article.container.ts @@ -0,0 +1,10 @@ +import { prismaClient } from '#connection/postgres.connection.js'; +import { ArticleController } from '#controllers/article.controller.js'; +import { ArticleRepository } from '#repositories/article.repository.js'; +import { ArticleService } from '#services/article.service.js'; + +const articleModel = new ArticleRepository(prismaClient); +const articleService = new ArticleService(articleModel); +const articleController = new ArticleController(articleService); + +export default articleController; diff --git a/src/containers/auth.container.ts b/src/containers/auth.container.ts new file mode 100644 index 00000000..59e68902 --- /dev/null +++ b/src/containers/auth.container.ts @@ -0,0 +1,10 @@ +import { prismaClient } from '#connection/postgres.connection.js'; +import { AuthController } from '#controllers/auth.controller.js'; +import { UserRepository } from '#repositories/user.repository.js'; +import { AuthService } from '#services/auth.service.js'; + +const authModel = new UserRepository(prismaClient); +const authService = new AuthService(authModel); +const authController = new AuthController(authService); + +export default authController; diff --git a/src/containers/comment.container.ts b/src/containers/comment.container.ts new file mode 100644 index 00000000..a9449f09 --- /dev/null +++ b/src/containers/comment.container.ts @@ -0,0 +1,10 @@ +import { prismaClient } from '#connection/postgres.connection.js'; +import { CommentController } from '#controllers/comment.controller.js'; +import { CommentRepository } from '#repositories/comment.repository.js'; +import { CommentService } from '#services/comment.service.js'; + +const commentModel = new CommentRepository(prismaClient); +const commentService = new CommentService(commentModel); +const commentController = new CommentController(commentService); + +export default commentController; diff --git a/src/containers/product.container.ts b/src/containers/product.container.ts new file mode 100644 index 00000000..e84aa528 --- /dev/null +++ b/src/containers/product.container.ts @@ -0,0 +1,10 @@ +import { prismaClient } from '#connection/postgres.connection.js'; +import { ProductController } from '#controllers/product.controller.js'; +import { ProductRepository } from '#repositories/product.repository.js'; +import { ProductService } from '#services/product.service.js'; + +const productModel = new ProductRepository(prismaClient); +const productService = new ProductService(productModel); +const productController = new ProductController(productService); + +export default productController; diff --git a/src/containers/user.container.ts b/src/containers/user.container.ts new file mode 100644 index 00000000..c29c5bd2 --- /dev/null +++ b/src/containers/user.container.ts @@ -0,0 +1,10 @@ +import { prismaClient } from '#connection/postgres.connection.js'; +import { UserController } from '#controllers/user.controller.js'; +import { UserRepository } from '#repositories/user.repository.js'; +import { UserService } from '#services/user.service.js'; + +const userModel = new UserRepository(prismaClient); +const userService = new UserService(userModel); +const userController = new UserController(userService); + +export default userController; diff --git a/src/containers/verify.container.ts b/src/containers/verify.container.ts new file mode 100644 index 00000000..6d890734 --- /dev/null +++ b/src/containers/verify.container.ts @@ -0,0 +1,10 @@ +import { prismaClient } from '#connection/postgres.connection.js'; +import { TokenVerifier } from '#middlewares/token.verifier.js'; +import { UserRepository } from '#repositories/user.repository.js'; +import { UserService } from '#services/user.service.js'; + +const userRepository = new UserRepository(prismaClient); +const userService = new UserService(userRepository); +const tokenVerifier = new TokenVerifier(userService); + +export default tokenVerifier; diff --git a/src/controllers/article.controller.ts b/src/controllers/article.controller.ts new file mode 100644 index 00000000..9f68744d --- /dev/null +++ b/src/controllers/article.controller.ts @@ -0,0 +1,82 @@ +import { assert } from 'superstruct'; +import { ArticleService } from '#services/article.service.js'; +import { ArticleDTO } from '#types/dtos.type.js'; +import { FindOptions } from '#types/options.type.js'; +import { RequestHandler } from '#types/request.type.js'; +import HTTP_CODES from '#utils/constants/http-codes.js'; +import MESSAGES from '#utils/constants/messages.js'; +import { CreateArticle, PatchArticle, Uuid } from '#utils/struct.js'; + +export class ArticleController { + constructor(private readonly articleService: ArticleService) {} + + getArticles: RequestHandler<{ query: FindOptions }> = async (req, res) => { + const orderBy = req.query.orderBy || 'recent'; + const page = Number(req.query.page) || 1; + const pageSize = Number(req.query.pageSize) || 10; + const keyword = req.query.keyword || ''; + + const resBody = await this.articleService.getPaginatedArticles({ + orderBy, + page, + pageSize, + keyword, + }); + + res.status(HTTP_CODES.OK).json(resBody); + }; + + getArticleById: RequestHandler<{ params: { id: string } }> = async (req, res) => { + assert(req.params.id, Uuid); + const id = req.params.id; + + const article = await this.articleService.getArticle(id); + + res.json(article); + }; + + postArticle: RequestHandler<{ body: ArticleDTO }> = async (req, res) => { + assert(req.body, CreateArticle); + + const article = await this.articleService.postArticle(req.body); + + res.status(HTTP_CODES.CREATED).json(article); + }; + + patchArticle: RequestHandler<{ params: { id: string }; body: ArticleDTO }> = async (req, res) => { + assert(req.params.id, Uuid, MESSAGES.IDFORMAT); + assert(req.body, PatchArticle); + const id = req.params.id; + + const article = await this.articleService.patchArticle(id, req.body); + + res.json(article); + }; + + deleteArticle: RequestHandler<{ params: { id: string } }> = async (req, res) => { + assert(req.params.id, Uuid, MESSAGES.IDFORMAT); + const id = req.params.id; + + const article = await this.articleService.deleteArticle(id); + + res.json(article); + }; + + postArticleLike: RequestHandler<{ params: { id: string } }> = async (req, res) => { + assert(req.params.id, Uuid, MESSAGES.IDFORMAT); + const articleId = req.params.id; + const userId = ''; + const article = await this.articleService.postArticleLike(articleId, userId); + + res.json(article); + }; + + deleteArticleLike: RequestHandler<{ params: { id: string } }> = async (req, res) => { + assert(req.params.id, Uuid, MESSAGES.IDFORMAT); + const articleId = req.params.id; + const userId = ''; + const article = await this.articleService.deleteArticleLike(articleId, userId); + + res.json(article); + }; +} diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts new file mode 100644 index 00000000..6fde9d71 --- /dev/null +++ b/src/controllers/auth.controller.ts @@ -0,0 +1,49 @@ +import { assert } from 'superstruct'; +import { AuthService } from '#services/auth.service.js'; +import { UserDTO } from '#types/dtos.type.js'; +import { RequestHandler } from '#types/request.type.js'; +import HTTP_CODES from '#utils/constants/http-codes.js'; +import MESSAGES from '#utils/constants/messages.js'; +import { CreateUser, SignIn } from '#utils/struct.js'; + +export class AuthController { + constructor(private readonly authService: AuthService) {} + + signUp: RequestHandler<{ body: UserDTO }> = async (req, res, next) => { + assert(req.body, CreateUser); + + const user = await this.authService.createUser(req.body); + + // NOTE 유저가 이미 존재함 + if (user === true) res.status(HTTP_CODES.BAD_REQUEST).json({ messages: MESSAGES.IS_EXIST }); + + res.status(HTTP_CODES.CREATED).json(user); + }; + + signIn: RequestHandler<{ body: UserDTO }> = async (req, res) => { + assert(req.body, SignIn); + + const { user, refreshToken } = (await this.authService.signIn(req.body))!; + + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + sameSite: 'none', + secure: true, + }); + res.json(user); + }; + + getMe: RequestHandler = async (req, res) => { + const { userId } = req.user!; + + const user = await this.authService.getUserById(userId); + + res.json(user); + }; + + refreshToken: RequestHandler = async (req, res) => { + const user = await this.authService.getNewToken(); + + res.json(user); + }; +} diff --git a/src/controllers/comment.controller.ts b/src/controllers/comment.controller.ts new file mode 100644 index 00000000..18971b5d --- /dev/null +++ b/src/controllers/comment.controller.ts @@ -0,0 +1,127 @@ +import { assert } from 'superstruct'; +import { CommentService } from '#services/comment.service.js'; +import { CommentType } from '#types/options.type.js'; +import { RequestHandler } from '#types/request.type.js'; +import HTTP_CODES from '#utils/constants/http-codes.js'; +import MESSAGES from '#utils/constants/messages.js'; +import { CreateComment, Cursor, PatchComment, PutComment, Uuid } from '#utils/struct.js'; + +export class CommentController { + constructor(private readonly commentService: CommentService) {} + + getCommentsDev: RequestHandler = async (req, res) => { + res.json(await this.commentService.getComments()); + }; + + getCommentsOfArticle: RequestHandler<{ + params: { id: string }; + query: { cursor: string; limit: string }; + }> = async (req, res) => { + assert(req.params.id, Uuid, MESSAGES.IDFORMAT); + assert(req.query.cursor, Cursor, MESSAGES.IDFORMAT); + + const articleId = req.params.id; + const limit = Number(req.query.limit) || 10; + const cursor = req.query.cursor; + + const resBody = await this.commentService.getPaginatedComments({ + id: articleId, + limit, + cursor, + type: CommentType.Article, + }); + + res.status(HTTP_CODES.OK).json(resBody); + }; + + getCommentsOfProduct: RequestHandler<{ + params: { id: string }; + query: { cursor: string; limit: string }; + }> = async (req, res) => { + assert(req.params.id, Uuid, MESSAGES.IDFORMAT); + assert(req.query.cursor, Cursor, MESSAGES.IDFORMAT); + + const productId = req.params.id; + const limit = Number(req.query.limit) || 10; + const cursor = req.query.cursor; + + const resBody = await this.commentService.getPaginatedComments({ + id: productId, + limit, + cursor, + type: CommentType.Product, + }); + + res.status(HTTP_CODES.OK).json(resBody); + }; + + postCommentOfArticle: RequestHandler<{ + params: { id: string }; + body: { content: string; ownerId: string }; + query: { cursor: string; limit: string }; + }> = async (req, res) => { + assert(req.params.id, Uuid, MESSAGES.IDFORMAT); + assert(req.body, CreateComment); + const articleId = req.params.id; + + const comment = await this.commentService.postComment({ + ...req.body, + articleId, + productId: null, + }); + + res.status(HTTP_CODES.CREATED).json(comment); + }; + + postCommentOfProduct: RequestHandler<{ + params: { id: string }; + body: { content: string; ownerId: string }; + }> = async (req, res) => { + assert(req.params.id, Uuid, MESSAGES.IDFORMAT); + assert(req.body, CreateComment); + const productId = req.params.id; + + const comment = await this.commentService.postComment({ + ...req.body, + productId, + articleId: null, + }); + + res.status(HTTP_CODES.CREATED).json(comment); + }; + + patchComment: RequestHandler<{ + params: { id: string }; + body: { content: string }; + }> = async (req, res) => { + assert(req.params.id, Uuid, MESSAGES.IDFORMAT); + assert(req.body, PatchComment); + const id = req.params.id; + + const comment = await this.commentService.patchComment(id, req.body); + + res.json(comment); + }; + + putComment: RequestHandler<{ + params: { id: string }; + body: { content: string; ownerId: string; articleId: string | null; productId: string | null }; + }> = async (req, res) => { + assert(req.params.id, Uuid, MESSAGES.IDFORMAT); + assert(req.body, PutComment); + const id = req.params.id; + + const comment = await this.commentService.putComment(id, req.body); + + res.json(comment); + }; + + deleteComment: RequestHandler<{ params: { id: string } }> = async (req, res) => { + assert(req.params.id, Uuid, MESSAGES.IDFORMAT); + const id = req.params.id; + + const comment = await this.commentService.deleteComment(id); + + res.json(comment); + }; +} diff --git a/src/controllers/product.controller.ts b/src/controllers/product.controller.ts new file mode 100644 index 00000000..6fb96c1e --- /dev/null +++ b/src/controllers/product.controller.ts @@ -0,0 +1,82 @@ +import { assert } from 'superstruct'; +import { ProductService } from '#services/product.service.js'; +import { ProductDTO } from '#types/dtos.type.js'; +import { FindOptions } from '#types/options.type.js'; +import { RequestHandler } from '#types/request.type.js'; +import HTTP_CODES from '#utils/constants/http-codes.js'; +import MESSAGES from '#utils/constants/messages.js'; +import { Uuid } from '#utils/struct.js'; + +export class ProductController { + constructor(private readonly productService: ProductService) {} + + getProducts: RequestHandler<{ query: FindOptions }> = async (req, res) => { + const orderBy = req.query.orderBy || 'recent'; + const page = Number(req.query.page) || 1; + const pageSize = Number(req.query.pageSize) || 10; + const keyword = req.query.keyword || ''; + + const resBody = await this.productService.getPaginatedProducts({ + orderBy, + page, + pageSize, + keyword, + }); + + res.json(resBody); + }; + + getProductById: RequestHandler<{ params: { id: string } }> = async (req, res) => { + assert(req.params.id, Uuid); + const id = req.params.id; + + const product = await this.productService.getProduct(id); + + res.json(product); + }; + + postProduct: RequestHandler<{ body: ProductDTO }> = async (req, res) => { + const product = await this.productService.postProduct(req.body); + + res.status(HTTP_CODES.CREATED).json(product); + }; + + patchProduct: RequestHandler<{ + params: { id: string }; + body: ProductDTO; + }> = async (req, res) => { + assert(req.params.id, Uuid, MESSAGES.IDFORMAT); + const id = req.params.id; + + const product = await this.productService.patchProduct(id, req.body); + + res.json(product); + }; + + deleteProduct: RequestHandler<{ params: { id: string } }> = async (req, res) => { + assert(req.params.id, Uuid, MESSAGES.IDFORMAT); + const id = req.params.id; + + const product = await this.productService.deleteProduct(id); + + res.json(product); + }; + + postProductLike: RequestHandler<{ params: { id: string } }> = async (req, res) => { + assert(req.params.id, Uuid, MESSAGES.IDFORMAT); + const productId = req.params.id; + const { userId } = req.user!; + const product = await this.productService.postProductLike(productId, userId); + + res.json(product); + }; + + deleteProductLike: RequestHandler<{ params: { id: string } }> = async (req, res) => { + assert(req.params.id, Uuid, MESSAGES.IDFORMAT); + const productId = req.params.id; + const { userId } = req.user!; + const product = await this.productService.deleteProductLike(productId, userId); + + res.json(product); + }; +} diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts new file mode 100644 index 00000000..d82bcc00 --- /dev/null +++ b/src/controllers/user.controller.ts @@ -0,0 +1,23 @@ +import { UserService } from '#services/user.service.js'; +import { FindOptions } from '#types/options.type.js'; +import { RequestHandler } from '#types/request.type.js'; + +export class UserController { + constructor(private readonly userService: UserService) {} + + getUsersDev: RequestHandler<{ query: FindOptions }> = async (req, res) => { + const orderBy = req.query.orderBy || 'recent'; + const page = Number(req.query.page) || 1; + const pageSize = Number(req.query.pageSize) || 10; + const keyword = req.query.keyword || ''; + + const resBody = await this.userService.getPaginatedUsers({ + orderBy, + page, + pageSize, + keyword, + }); + + res.json(resBody); + }; +} diff --git a/src/middlewares/asyncLocalStorage.ts b/src/middlewares/asyncLocalStorage.ts new file mode 100644 index 00000000..0249a2d2 --- /dev/null +++ b/src/middlewares/asyncLocalStorage.ts @@ -0,0 +1,32 @@ +import { AsyncLocalStorage } from 'async_hooks'; +import type { NextFunction, Request, Response } from 'express'; +import { IStorage } from '#types/common.type.js'; +import MESSAGES from '#utils/constants/messages.js'; +import { InternalServerError } from '#utils/http-errors.js'; + +export const asyncLocalStorage = new AsyncLocalStorage(); + +export const runAsyncLocalStorage = (req: Request, res: Response, next: NextFunction) => { + const storage: IStorage = {}; + + asyncLocalStorage.run(storage, () => { + res.on('finish', () => { + asyncLocalStorage.disable(); + }); + + res.on('close', () => { + asyncLocalStorage.disable(); + }); + + next(); + }); +}; +// NOTE 타입 검증하고 간단하게 사용하기 위한 get함수 +export const getStorage = (): IStorage => { + const store = asyncLocalStorage.getStore(); + if (!store) { + throw new InternalServerError(MESSAGES.INTERNAL_ERROR); + } + + return store; +}; diff --git a/src/middlewares/auth.js b/src/middlewares/auth.js deleted file mode 100644 index be7f2e33..00000000 --- a/src/middlewares/auth.js +++ /dev/null @@ -1,15 +0,0 @@ -import { expressjwt } from 'express-jwt'; -import { jwtSecret } from '../configs/auth.config.js'; - -export const verifyAccessToken = expressjwt({ - secret: jwtSecret, - algorithms: ['HS256'], - requestProperty: 'user', -}); - -export const verifyRefreshToken = expressjwt({ - secret: jwtSecret, - algorithms: ['HS256'], - getToken: req => req.cookies.refreshToken, - requestProperty: 'user', -}); diff --git a/src/middlewares/error-handler.js b/src/middlewares/error-handler.js deleted file mode 100644 index 83c9d541..00000000 --- a/src/middlewares/error-handler.js +++ /dev/null @@ -1,19 +0,0 @@ -import { Prisma } from '@prisma/client'; -import { StructError } from 'superstruct'; -import c from '../utils/constants.js'; -import { CastError, ValidationError } from '../utils/error.js'; - -export default function errorHandler(err, req, res, next) { - console.error(err); - if (err instanceof Prisma.PrismaClientValidationError || err instanceof TypeError || err instanceof ValidationError) { - res.status(c.HTTP_STATUS.BAD_REQUEST).send({ message: err.message }); - } else if ( - err instanceof StructError || - (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2025') || - err instanceof CastError - ) { - res.sendStatus(c.HTTP_STATUS.NOT_FOUND); - } else { - res.status(c.HTTP_STATUS.SERVER_ERROR).send({ message: err.message }); - } -} diff --git a/src/middlewares/error-handler.ts b/src/middlewares/error-handler.ts new file mode 100644 index 00000000..ab892328 --- /dev/null +++ b/src/middlewares/error-handler.ts @@ -0,0 +1,18 @@ +import { ErrorRequestHandler } from '#types/request.type.js'; +import HTTP_CODES from '#utils/constants/http-codes.js'; +import { HttpError } from '#utils/http-errors.js'; + +const errorHandler: ErrorRequestHandler = (err, req, res, next) => { + console.error(err); + // logErrorWithRequest(err, req); + if (err instanceof HttpError) { + res.status(err.status).send({ message: err.message }); + } else if (err.name === 'UnauthorizedError') { + // NOTE express-jwt error + res.status(HTTP_CODES.UNAUTHORIZED).send({ message: err.message }); + } else { + res.status(HTTP_CODES.SERVER_ERROR).send({ message: err.message }); + } +}; + +export default errorHandler; diff --git a/src/middlewares/hash.js b/src/middlewares/hash.ts similarity index 69% rename from src/middlewares/hash.js rename to src/middlewares/hash.ts index 48d2cdf5..1c07d4e7 100644 --- a/src/middlewares/hash.js +++ b/src/middlewares/hash.ts @@ -1,7 +1,8 @@ import crypto from 'crypto'; +import { RequestHandler } from 'express'; import hashingPassword from '../utils/hashingPassword.js'; -export default function hashPassword(req, res, next) { +const hashPassword: RequestHandler = (req, res, next) => { if (!req.body.password) next(); const salt = crypto.randomBytes(16).toString('hex'); @@ -11,4 +12,6 @@ export default function hashPassword(req, res, next) { req.body.salt = salt; next(); -} +}; + +export default hashPassword; diff --git a/src/middlewares/pagination.validation.js b/src/middlewares/pagination.validation.js deleted file mode 100644 index ccdd5f4c..00000000 --- a/src/middlewares/pagination.validation.js +++ /dev/null @@ -1,11 +0,0 @@ -export default function validatePaginationOptions(req, res, next) { - const page = req.query.page; - const pageSize = req.query.pageSize; - const limit = req.query.limit; - - if (page && isNaN(Number(page))) throw new TypeError('page should be an integer'); - if (pageSize && isNaN(Number(pageSize))) throw new TypeError('pageSize should be an integer'); - if (limit && isNaN(Number(limit))) throw new TypeError('limit should be an integer'); - - next(); -} diff --git a/src/middlewares/pagination.validation.ts b/src/middlewares/pagination.validation.ts new file mode 100644 index 00000000..8cab3c2f --- /dev/null +++ b/src/middlewares/pagination.validation.ts @@ -0,0 +1,21 @@ +import { RequestHandler } from '#types/request.type.js'; + +const validatePaginationOptions: RequestHandler<{ + query: { page?: string; pageSize?: string; limit?: string }; +}> = (req, res, next) => { + const page = req.query.page; + const pageSize = req.query.pageSize; + const limit = req.query.limit; + + if (page && isNaN(parseInt(page))) throw new TypeError('page should be an integer'); + if (pageSize && isNaN(parseInt(pageSize))) throw new TypeError('pageSize should be an integer'); + if (limit && isNaN(parseInt(limit))) throw new TypeError('limit should be an integer'); + + if (page && parseInt(page) <= 0) throw new TypeError('page must be at least 1.'); + if (pageSize && parseInt(pageSize) <= 0) throw new TypeError('pageSize must be at least 1.'); + if (limit && parseInt(limit) <= 0) throw new TypeError('limit must be at least 1.'); + + next(); +}; + +export default validatePaginationOptions; diff --git a/src/middlewares/product.validation.js b/src/middlewares/product.validation.ts similarity index 64% rename from src/middlewares/product.validation.js rename to src/middlewares/product.validation.ts index 7a40c6e0..d23577cf 100644 --- a/src/middlewares/product.validation.js +++ b/src/middlewares/product.validation.ts @@ -1,19 +1,22 @@ import { assert } from 'superstruct'; -import { CreateProduct, PatchProduct } from '../struct.js'; +import { RequestHandler } from '#types/request.type.js'; +import { CreateProduct, PatchProduct } from '#utils/struct.js'; -export default function validateProduct(req, res, next) { +const validateProduct: RequestHandler<{ body: { data: string; tags: string[]; file?: object } }> = (req, res, next) => { req.body = JSON.parse(req.body.data); switch (req.method) { case 'POST': - if (req.body?.tags?.length < 1 || req.body?.tags?.length > 5) throw new Error('invalid tags length'); assert(req.body, CreateProduct); + if (req.body?.tags?.length < 1 || req.body?.tags?.length > 5) throw new Error('invalid tags length'); break; case 'PATCH': - if (req.body?.tags?.length < 1 || req.body?.tags?.length > 5) throw new Error('invalid tags length'); assert(req.body, PatchProduct); + if (req.body?.tags?.length < 1 || req.body?.tags?.length > 5) throw new Error('invalid tags length'); break; } req.body.file = req.file; next(); -} +}; + +export default validateProduct; diff --git a/src/middlewares/request.ts b/src/middlewares/request.ts new file mode 100644 index 00000000..49367de5 --- /dev/null +++ b/src/middlewares/request.ts @@ -0,0 +1,9 @@ +import type { Request as expressRequest } from 'express'; + +export interface Request + extends expressRequest< + T extends { params: infer ParamsType } ? ParamsType : {}, + T extends { response: infer ResponseType } ? ResponseType : {}, + T extends { body: infer BodyType } ? BodyType : {}, + T extends { query: infer QueryType } ? QueryType : {} + > {} diff --git a/src/middlewares/token.verifier.ts b/src/middlewares/token.verifier.ts new file mode 100644 index 00000000..a3a9110e --- /dev/null +++ b/src/middlewares/token.verifier.ts @@ -0,0 +1,76 @@ +import { expressjwt } from 'express-jwt'; +import { jwtSecret } from '#configs/jwt.config.js'; +import { getStorage } from '#middlewares/asyncLocalStorage.js'; +import { UserService } from '#services/user.service.js'; +import { RequestHandler } from '#types/request.type.js'; +import assertExist from '#utils/assertExist.js'; + +export class TokenVerifier { + constructor(private userService: UserService) {} + + optionalVerifyAccessToken: RequestHandler = async (req, res, next) => { + const authorizationHeader = req.headers.authorization; + if (authorizationHeader) { + await this.verifyAccessToken(req, res, next); + return; + } + next(); + }; + + verifyAccessToken: RequestHandler = async (req, res, next) => { + const token = req.headers.authorization?.split(' ')[1]; + + return await new Promise(resolve => { + expressjwt({ + secret: jwtSecret, + algorithms: ['HS256'], + requestProperty: 'user', + })(req, res, async err => { + if (err) { + next(err); + return; + } + + const user = await this.userService.getUserById(req.user!.userId); + assertExist(user); + + const storage = getStorage(); + storage.userId = req.user!.userId; + storage.accessToken = token; + storage.tokenEXP = req.user!.exp; + + next(); + resolve(); + }); + }); + }; + + verifyRefreshToken: RequestHandler = async (req, res, next) => { + const token = req.cookies.refreshToken; + + return await new Promise(resolve => { + expressjwt({ + secret: jwtSecret, + algorithms: ['HS256'], + getToken: req => req.cookies.refreshToken, + requestProperty: 'user', + })(req, res, async err => { + if (err) { + next(err); + return; + } + + const user = await this.userService.getUserById(req.user!.userId); + assertExist(user); + + const storage = getStorage(); + storage.userId = req.user!.userId; + storage.refreshToken = token; + storage.tokenEXP = req.user!.exp; + + next(); + resolve(); + }); + }); + }; +} diff --git a/src/mongodb/configs/mongodb.config.js b/src/mongodb/configs/mongodb.config.js deleted file mode 100644 index 4ddb6bfc..00000000 --- a/src/mongodb/configs/mongodb.config.js +++ /dev/null @@ -1,10 +0,0 @@ -import * as dotenv from 'dotenv'; - -dotenv.config(); - -export const mongodbConnectionConfig = { - host: process.env.MONGODB_HOST, - database: process.env.MONGODB_DATABASE, - username: process.env.MONGODB_USERNAME, - password: process.env.MONGODB_PASSWORD, -}; diff --git a/src/mongodb/containers/product.container.js b/src/mongodb/containers/product.container.js deleted file mode 100644 index ab2fe62f..00000000 --- a/src/mongodb/containers/product.container.js +++ /dev/null @@ -1,10 +0,0 @@ -import { mongodbConnection } from '../db/mongodb.connection.js'; -import { ProductModel } from '../models/product.model.js'; -import { ProductService } from '../services/product.service.js'; -import { ProductController } from '../controllers/product.controller.js'; - -const productModel = new ProductModel(mongodbConnection); -const productService = new ProductService(productModel); -const productController = new ProductController(productService); - -export { productController }; diff --git a/src/mongodb/controllers/product.controller.js b/src/mongodb/controllers/product.controller.js deleted file mode 100644 index 64badfbc..00000000 --- a/src/mongodb/controllers/product.controller.js +++ /dev/null @@ -1,70 +0,0 @@ -import { assert } from 'superstruct'; -import { MongodbId } from '../../struct.js'; -import c from '../../utils/constants.js'; -import { TypeError } from '../../utils/error.js'; - -export class ProductController { - constructor(productService) { - this.productService = productService; - } - - getProducts = async (req, res) => { - const orderBy = req.query.orderBy || 'recent'; - const page = Number(req.query.page) || 1; - const pageSize = Number(req.query.pageSize) || 10; - const keyword = req.query.keyword || ''; - - if (isNaN(page) || isNaN(pageSize)) { - throw new TypeError('page and pageSize should be an integer'); - } - - res.json( - await this.productService.getProductsAndCount({ - orderBy, - page, - pageSize, - keyword, - }), - ); - }; - - getProductById = async (req, res) => { - assert(req.params.id, MongodbId, c.MESSAGES.IDFORMAT); - const id = req.params.id; - - const product = await this.productService.getProductById(id); - - if (!product) res.status(404).json({ message: c.MESSAGES.NOID }); - - res.json(product); - }; - - postProduct = async (req, res) => { - const newProduct = await this.productService.postProduct(req.body); - - res.status(201).json(newProduct); - }; - - patchProductById = async (req, res) => { - assert(req.params.id, MongodbId, c.MESSAGES.IDFORMAT); - const id = req.params.id; - - const product = await this.productService.patchProductById(id, req.body); - - if (!product) res.status(404).json({ message: c.MESSAGES.NOID }); - - res.json(product); - }; - - deleteProductById = async (req, res) => { - assert(req.params.id, MongodbId, c.MESSAGES.IDFORMAT); - const id = req.params.id; - - const product = await this.productService.deleteProductById(id); - - if (!product) res.status(404).json({ message: c.MESSAGES.NOID }); - - // res.json(product); - res.sendStatus(204); - }; -} diff --git a/src/mongodb/db/mongodb.connection.js b/src/mongodb/db/mongodb.connection.js deleted file mode 100644 index c27302ca..00000000 --- a/src/mongodb/db/mongodb.connection.js +++ /dev/null @@ -1,15 +0,0 @@ -import mongoose from 'mongoose'; -import { mongodbConnectionConfig } from '../configs/mongodb.config.js'; - -const { host, database, username, password } = mongodbConnectionConfig; -const mongooseUri = `mongodb+srv://${username}:${password}@${host}/${database}?retryWrites=true&w=majority&appName=Cluster0`; // uri 형식으로 -const mongodbConnection = mongoose.createConnection(mongooseUri); - -mongodbConnection.on('connected', () => console.log('connected')); -mongodbConnection.on('open', () => console.log('open')); -mongodbConnection.on('disconnected', () => console.log('disconnected')); -mongodbConnection.on('reconnected', () => console.log('reconnected')); -mongodbConnection.on('disconnecting', () => console.log('disconnecting')); -mongodbConnection.on('close', () => console.log('close')); - -export { mongodbConnection }; diff --git a/src/mongodb/models/product.model.js b/src/mongodb/models/product.model.js deleted file mode 100644 index 297e182f..00000000 --- a/src/mongodb/models/product.model.js +++ /dev/null @@ -1,50 +0,0 @@ -import { productSchema } from '../schemas/product.schema.js'; - -// DB 접근만 한다. -// 모델은 비즈니스 서비스 로직, http 통신 로직을 몰라야한다. -export class ProductModel { - constructor(connection) { - this.model = connection.model('Product', productSchema); - } - - getCount = async keyword => { - const searchOption = keyword ? { $text: { $search: keyword } } : {}; - - return await this.model.find(searchOption).countDocuments(); - }; - - findMany = async ({ orderBy, page, pageSize, keyword }) => { - const sortOption = { createdAt: orderBy === 'recent' ? 'desc' : 'asc' }; - const searchOption = keyword ? { $text: { $search: keyword } } : {}; - - return await this.model - .find(searchOption) - .sort(sortOption) - .skip((page - 1) * pageSize) - .limit(pageSize); - }; - - findById = async id => { - return this.model.findById(id); - }; - - create = async body => { - return await this.model.create(body); - }; - - save = async product => { - return await product.save(); - }; - - deleteById = async id => { - return await this.model.findByIdAndDelete(id); - }; - - deleteMany = async option => { - return await this.model.deleteMany(option); - }; - - insertMany = async data => { - return await this.model.insertMany(data); - }; -} diff --git a/src/mongodb/schemas/product.schema.js b/src/mongodb/schemas/product.schema.js deleted file mode 100644 index 98036659..00000000 --- a/src/mongodb/schemas/product.schema.js +++ /dev/null @@ -1,29 +0,0 @@ -import { Schema } from 'mongoose'; - -const productSchema = new Schema( - { - name: { - type: String, - required: true, - minLength: 1, - maxLength: 10, - }, - description: { - type: String, - minLength: 10, - maxLength: 100, - required: true, - }, - price: { type: Number, required: true, min: 1 }, - tags: { type: [String] }, - images: { type: [String] }, - favoriteCount: { type: Number, default: 0, min: 0 }, - }, - { - timestamps: true, - // 자동으로 생성해주는 collection을 사용하지 않고자 한다면 직접 명시할 수 있다. - collection: 'products', - }, -); - -export { productSchema }; diff --git a/src/mongodb/services/product.service.js b/src/mongodb/services/product.service.js deleted file mode 100644 index e4e45afa..00000000 --- a/src/mongodb/services/product.service.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * layered architecture(3-layer) - * controller, service, repository(=model) - * - * controller: http 통신만 함. - * service: 비즈니스 로직이 실행됨. - * 비즈니스 로직: 실제 api이 동작이 발생하는 부분 - * repository: db 접근만 함 - * - * flow - * 1. 사용자가 api 호출 - * 2. controller가 이 요청을 받음 - * 3. controller는 req params, query, body 등을 받아서 service에 넘김. - * 4. service는 controller로부터 받은 정보를 가지고 행동을 하고, db 관련 처리를 해야하면 repository를 호출함. - * 5. repository는 넘겨받은 정보를 바탕으로 db에 접근해서 결과를 service에 반환 - * - * MVC pattern(model, view, controller)에서 진화해 만들어진 architecture - */ -export class ProductService { - constructor(productModel) { - this.productModel = productModel; - } - - // getProducts = async ({ orderBy, page, pageSize, keyword }) => { - // return await this.productModel.findMany({ - // orderBy, - // page, - // pageSize, - // keyword, - // }); - // }; - - // getCount = async (keyword) => { - // return await this.productModel.getCount(keyword); - // }; - - getProductsAndCount = async ({ orderBy, page, pageSize, keyword }) => { - const list = await this.productModel.findMany({ - orderBy, - page, - pageSize, - keyword, - }); - - const totalCount = await this.productModel.getCount(keyword); - - return { list, totalCount }; - }; - - getProductById = async id => { - return await this.productModel.findById(id); - }; - - postProduct = async body => { - return await this.productModel.create(body); - }; - - patchProductById = async (id, body) => { - let product = await this.productModel.findById(id); - if (!product) return; - - Object.keys(body).forEach(k => { - product[k] = body[k]; - }); - return await this.productModel.save(product); - }; - - deleteProductById = async id => { - return await this.productModel.deleteById(id); - }; -} diff --git a/src/mongodb/utils/async-handler.js b/src/mongodb/utils/async-handler.js deleted file mode 100644 index f26fb1ab..00000000 --- a/src/mongodb/utils/async-handler.js +++ /dev/null @@ -1,19 +0,0 @@ -import { StructError } from 'superstruct'; -import { CastError, TypeError, ValidationError } from './utils/error.js'; - -// handler를 인자로 받아서 오류처리 해주는 함수 -export function asyncHandler(handler) { - return async (req, res) => { - try { - await handler(req, res); - } catch (e) { - if (e instanceof ValidationError || e instanceof TypeError) { - res.status(400).json({ message: e.message }); - } else if (e instanceof StructError || e instanceof CastError) { - res.status(404).json({ message: e.message }); - } else { - res.status(500).json({ message: e.message }); - } - } - }; -} diff --git a/src/mongodb/utils/error.js b/src/mongodb/utils/error.js deleted file mode 100644 index 7f6fdd68..00000000 --- a/src/mongodb/utils/error.js +++ /dev/null @@ -1,25 +0,0 @@ -class TypeError extends Error { - constructor(message) { - super(message); - this.name = 'TypeError'; - this.statusCode = 400; - } -} - -class ValidationError extends Error { - constructor(message) { - super(message); - this.name = 'ValidationError'; - this.statusCode = 400; - } -} - -class CastError extends Error { - constructor(message) { - super(message); - this.name = 'CastError'; - this.statusCode = 404; - } -} - -export { TypeError, ValidationError, CastError }; diff --git a/src/postgresql/configs/postgres.config.js b/src/postgresql/configs/postgres.config.js deleted file mode 100644 index a063322e..00000000 --- a/src/postgresql/configs/postgres.config.js +++ /dev/null @@ -1,13 +0,0 @@ -// import * as dotenv from 'dotenv'; - -// dotenv.config(); - -export const postgresConnectionConfig = { - host: process.env.POSTGRES_HOST, - port: process.env.POSTGRES_PORT, - username: process.env.POSTGRES_USERNAME, - password: process.env.POSTGRES_PASSWORD, - database: process.env.POSTGRES_DATABASE, - url: process.env.DATABASE_URL, - render: process.env.DATABASE_URL_render, -}; diff --git a/src/postgresql/containers/article.container.js b/src/postgresql/containers/article.container.js deleted file mode 100644 index df2da0fc..00000000 --- a/src/postgresql/containers/article.container.js +++ /dev/null @@ -1,10 +0,0 @@ -import { prismaClient } from '../connection/postgres.connection.js'; -import { ArticleController } from '../controllers/article.controller.js'; -import { ArticleRepo } from '../repos/article.repo.js'; -import { ArticleService } from '../services/article.service.js'; - -const articleModel = new ArticleRepo(prismaClient); -const articleService = new ArticleService(articleModel); -const articleController = new ArticleController(articleService); - -export default articleController; diff --git a/src/postgresql/containers/auth.container.js b/src/postgresql/containers/auth.container.js deleted file mode 100644 index c95f41a4..00000000 --- a/src/postgresql/containers/auth.container.js +++ /dev/null @@ -1,10 +0,0 @@ -import { prismaClient } from '../connection/postgres.connection.js'; -import { AuthController } from '../controllers/auth.controller.js'; -import { UserRepo } from '../repos/user.repo.js'; -import { AuthService } from '../services/auth.service.js'; - -const authModel = new UserRepo(prismaClient); -const authService = new AuthService(authModel); -const authController = new AuthController(authService); - -export default authController; diff --git a/src/postgresql/containers/comment.container.js b/src/postgresql/containers/comment.container.js deleted file mode 100644 index fa701ff5..00000000 --- a/src/postgresql/containers/comment.container.js +++ /dev/null @@ -1,10 +0,0 @@ -import { prismaClient } from '../connection/postgres.connection.js'; -import { CommentController } from '../controllers/comment.controller.js'; -import { CommentRepo } from '../repos/comment.repo.js'; -import { CommentService } from '../services/comment.service.js'; - -const commentModel = new CommentRepo(prismaClient); -const commentService = new CommentService(commentModel); -const commentController = new CommentController(commentService); - -export default commentController; diff --git a/src/postgresql/containers/product.container.js b/src/postgresql/containers/product.container.js deleted file mode 100644 index b44fefa9..00000000 --- a/src/postgresql/containers/product.container.js +++ /dev/null @@ -1,10 +0,0 @@ -import { prismaClient } from '../connection/postgres.connection.js'; -import { ProductController } from '../controllers/product.controller.js'; -import { ProductRepo } from '../repos/product.repo.js'; -import { ProductService } from '../services/product.service.js'; - -const productModel = new ProductRepo(prismaClient); -const productService = new ProductService(productModel); -const productController = new ProductController(productService); - -export default productController; diff --git a/src/postgresql/containers/user.container.js b/src/postgresql/containers/user.container.js deleted file mode 100644 index 1331f9b5..00000000 --- a/src/postgresql/containers/user.container.js +++ /dev/null @@ -1,10 +0,0 @@ -import { prismaClient } from '../connection/postgres.connection.js'; -import { UserController } from '../controllers/user.controller.js'; -import { UserRepo } from '../repos/user.repo.js'; -import { UserService } from '../services/user.service.js'; - -const userModel = new UserRepo(prismaClient); -const userService = new UserService(userModel); -const userController = new UserController(userService); - -export default userController; diff --git a/src/postgresql/controllers/article.controller.js b/src/postgresql/controllers/article.controller.js deleted file mode 100644 index 5a51ed39..00000000 --- a/src/postgresql/controllers/article.controller.js +++ /dev/null @@ -1,87 +0,0 @@ -import { assert } from 'superstruct'; -import { CreateArticle, PatchArticle, Uuid } from '../../struct.js'; -import c from '../../utils/constants.js'; - -export class ArticleController { - constructor(articleService) { - this.service = articleService; - } - - getArticles = async (req, res) => { - const orderBy = req.query.orderBy || 'recent'; - const page = Number(req.query.page) || 1; - const pageSize = Number(req.query.pageSize) || 10; - const keyword = req.query.keyword || ''; - - const resBody = await this.service.getPaginatedArticles({ - orderBy, - page, - pageSize, - keyword, - }); - - res.status(200).json(resBody); - }; - - getArticleById = async (req, res) => { - assert(req.params.id, Uuid); - const id = req.params.id; - - const article = await this.service.getArticle(id); - - if (!article) res.status(404).json({ message: c.MESSAGES.NOID }); - - res.json(article); - }; - - postArticle = async (req, res) => { - assert(req.body, CreateArticle); - - const article = await this.service.postArticle(req.body); - - res.status(201).json(article); - }; - - patchArticle = async (req, res) => { - assert(req.params.id, Uuid, c.MESSAGES.IDFORMAT); - assert(req.body, PatchArticle); - const id = req.params.id; - - const article = await this.service.patchArticle(id, req.body); - - if (!article) res.status(404).json({ message: c.MESSAGES.NOID }); - - res.json(article); - }; - - deleteArticle = async (req, res) => { - assert(req.params.id, Uuid, c.MESSAGES.IDFORMAT); - const id = req.params.id; - - const article = await this.service.deleteArticle(id); - - if (!article) res.status(404).json({ message: c.MESSAGES.NOID }); - - res.json(article); - }; - - postArticleLike = async (req, res) => { - assert(req.params.id, Uuid, c.MESSAGES.IDFORMAT); - const articleId = req.params.id; - const userId = ''; - const article = await this.service.postArticleLike(articleId, userId); - - if (!article) res.status(404).json(); - res.json(article); - }; - - deleteArticleLike = async (req, res) => { - assert(req.params.id, Uuid, c.MESSAGES.IDFORMAT); - const articleId = req.params.id; - const userId = ''; - const article = await this.service.deleteArticleLike(articleId, userId); - - if (!article) res.status(404).json(); - res.json(article); - }; -} diff --git a/src/postgresql/controllers/auth.controller.js b/src/postgresql/controllers/auth.controller.js deleted file mode 100644 index eb371302..00000000 --- a/src/postgresql/controllers/auth.controller.js +++ /dev/null @@ -1,54 +0,0 @@ -import { assert } from 'superstruct'; -import { CreateUser, SignIn } from '../../struct.js'; - -export class AuthController { - constructor(authService) { - this.service = authService; - } - - signUp = async (req, res, next) => { - assert(req.body, CreateUser); - - const user = await this.service.createUser(req.body); - - if (!user) res.status(400).json(); - // NOTE 유저가 이미 존재함 - if (user === true) res.status(400).json({ messages: 'User already exist' }); - res.status(201).json(user); - }; - - signIn = async (req, res) => { - assert(req.body, SignIn); - - const { user, refreshToken } = await this.service.signIn(req.body); - - if (!user) res.status(401).json(); - res.cookie('refreshToken', refreshToken, { - httpOnly: true, - sameSite: 'none', - secure: true, // NOTE https가 아니면 false로 - }); - res.json(user); - }; - - getMe = async (req, res) => { - const { userId } = req.user; - if (!userId) res.status(400).json(); - - const user = await this.service.getUserById(userId); - if (!user) res.status(404).json(); - - res.json(user); - }; - - refreshToken = async (req, res) => { - const { refreshToken } = req.cookies; - const { userId } = req.user; - if (!userId) res.status(400).json(); - - const user = await this.service.getNewToken(req.user, refreshToken); - if (!user) res.status(404).json(); - - res.json(user); - }; -} diff --git a/src/postgresql/controllers/comment.controller.js b/src/postgresql/controllers/comment.controller.js deleted file mode 100644 index 36d84ec2..00000000 --- a/src/postgresql/controllers/comment.controller.js +++ /dev/null @@ -1,107 +0,0 @@ -import { assert } from 'superstruct'; -import { CreateComment, Cursor, PatchComment, PutComment, Uuid } from '../../struct.js'; -import c from '../../utils/constants.js'; - -export class CommentController { - constructor(commentService) { - this.service = commentService; - } - - getCommentsDev = async (req, res) => { - res.json(await this.service.getComments()); - }; - - getCommentsOfArticle = async (req, res) => { - assert(req.params.id, Uuid, c.MESSAGES.IDFORMAT); - assert(req.query.cursor, Cursor, c.MESSAGES.IDFORMAT); - const articleId = req.params.id; - const limit = Number(req.query.limit) || 10; - const cursor = req.query.cursor; - - const resBody = await this.service.getPaginatedComments({ - id: articleId, - limit, - cursor, - type: 'article', - }); - - res.status(200).json(resBody); - }; - - getCommentsOfProduct = async (req, res) => { - assert(req.params.id, Uuid, c.MESSAGES.IDFORMAT); - assert(req.query.cursor, Cursor, c.MESSAGES.IDFORMAT); - const productId = req.params.id; - const limit = Number(req.query.limit) || 10; - const cursor = req.query.cursor; - - const resBody = await this.service.getPaginatedComments({ - id: productId, - limit, - cursor, - type: 'product', - }); - - res.status(200).json(resBody); - }; - - postCommentOfArticle = async (req, res) => { - assert(req.params.id, Uuid, c.MESSAGES.IDFORMAT); - assert(req.body, CreateComment); - const articleId = req.params.id; - - const comment = await this.service.postComment({ - ...req.body, - articleId, - }); - res.status(201).json(comment); - }; - - postCommentOfProduct = async (req, res) => { - assert(req.params.id, Uuid, c.MESSAGES.IDFORMAT); - assert(req.body, CreateComment); - const productId = req.params.id; - - const comment = await this.service.postComment({ - ...req.body, - productId, - }); - - res.status(201).json(comment); - }; - - patchComment = async (req, res) => { - assert(req.params.id, Uuid, c.MESSAGES.IDFORMAT); - assert(req.body, PatchComment); - const id = req.params.id; - - const comment = await this.service.patchComment(id, req.body); - - if (!comment) res.status(404).json({ message: c.MESSAGES.NOID }); - - res.json(comment); - }; - - putComment = async (req, res) => { - assert(req.params.id, Uuid, c.MESSAGES.IDFORMAT); - assert(req.body, PutComment); - const id = req.params.id; - - const comment = await this.service.putComment(id, req.body); - - if (!comment) res.status(404).json({ message: c.MESSAGES.NOID }); - - res.json(comment); - }; - - deleteComment = async (req, res) => { - assert(req.params.id, Uuid, c.MESSAGES.IDFORMAT); - const id = req.params.id; - - const comment = await this.service.deleteComment(id); - - if (!comment) res.status(404).json({ message: c.MESSAGES.NOID }); - - res.json(comment); - }; -} diff --git a/src/postgresql/controllers/product.controller.js b/src/postgresql/controllers/product.controller.js deleted file mode 100644 index 6e7536b8..00000000 --- a/src/postgresql/controllers/product.controller.js +++ /dev/null @@ -1,87 +0,0 @@ -import { assert } from 'superstruct'; -import { PatchProduct, Uuid } from '../../struct.js'; -import c from '../../utils/constants.js'; - -export class ProductController { - constructor(productService) { - this.service = productService; - } - - getProducts = async (req, res) => { - const orderBy = req.query.orderBy || 'recent'; - const page = Number(req.query.page) || 1; - const pageSize = Number(req.query.pageSize) || 10; - const keyword = req.query.keyword || ''; - - const resBody = await this.service.getPaginatedProducts({ - orderBy, - page, - pageSize, - keyword, - }); - if (!resBody) res.status(404).json(); - - res.json(resBody); - }; - - getProductById = async (req, res) => { - assert(req.params.id, Uuid); - const id = req.params.id; - - const product = await this.service.getProduct(id); - - if (!product) res.status(404).json({ message: c.MESSAGES.NOID }); - - res.json(product); - }; - - postProduct = async (req, res) => { - const product = await this.service.postProduct(req.body); - - if (!product) res.status(404).json(); - res.status(201).json(product); - }; - - patchProduct = async (req, res) => { - assert(req.params.id, Uuid, c.MESSAGES.IDFORMAT); - assert(req.body, PatchProduct); - const id = req.params.id; - - const product = await this.service.patchProduct(id, req.body); - - if (!product) res.status(404).json({ message: c.MESSAGES.NOID }); - - res.json(product); - }; - - deleteProduct = async (req, res) => { - assert(req.params.id, Uuid, c.MESSAGES.IDFORMAT); - const id = req.params.id; - - const product = await this.service.deleteProduct(id); - - if (!product) res.status(404).json({ message: c.MESSAGES.NOID }); - - res.json(product); - }; - - postProductLike = async (req, res) => { - assert(req.params.id, Uuid, c.MESSAGES.IDFORMAT); - const productId = req.params.id; - const userId = ''; - const product = await this.service.postProductLike(productId, userId); - - if (!product) res.status(404).json(); - res.json(product); - }; - - deleteProductLike = async (req, res) => { - assert(req.params.id, Uuid, c.MESSAGES.IDFORMAT); - const productId = req.params.id; - const userId = ''; - const product = await this.service.deleteProductLike(productId, userId); - - if (!product) res.status(404).json(); - res.json(product); - }; -} diff --git a/src/postgresql/controllers/user.controller.js b/src/postgresql/controllers/user.controller.js deleted file mode 100644 index 762fee91..00000000 --- a/src/postgresql/controllers/user.controller.js +++ /dev/null @@ -1,21 +0,0 @@ -export class UserController { - constructor(userService) { - this.service = userService; - } - - getUsersDev = async (req, res) => { - const orderBy = req.query.orderBy || 'recent'; - const page = Number(req.query.page) || 1; - const pageSize = Number(req.query.pageSize) || 10; - const keyword = req.query.keyword || ''; - - const resBody = await this.service.getPaginatedUsers({ - orderBy, - page, - pageSize, - keyword, - }); - - res.json(resBody); - }; -} diff --git a/src/postgresql/repos/article.repo.js b/src/postgresql/repos/article.repo.js deleted file mode 100644 index 3e6b4821..00000000 --- a/src/postgresql/repos/article.repo.js +++ /dev/null @@ -1,106 +0,0 @@ -export class ArticleRepo { - constructor(client) { - this.db = client.article; - } - - count = async keyword => { - const searchOption = keyword - ? { - where: { - OR: [{ title: { contains: keyword } }, { content: { contains: keyword } }], - }, - } - : {}; - - const count = await this.db.count(searchOption); - - return count; - }; - - findMany = async ({ orderBy, page, pageSize, keyword }) => { - let sortOption; - switch (orderBy) { - case 'like': - sortOption = { orderBy: { likeCount: 'desc' } }; - break; - case 'recent': - default: - sortOption = { orderBy: { createdAt: 'desc' } }; - } - - const searchOption = keyword ? { where: { title: { contains: keyword } } } : {}; - - const articles = await this.db.findMany({ - ...searchOption, - ...sortOption, - take: pageSize, - skip: (page - 1) * pageSize, - include: { - owner: { - select: { - nickname: true, - }, - }, - }, - }); - - return articles; - }; - - findById = async id => { - const article = await this.db.findUnique({ - where: { id }, - include: { - owner: { - select: { - nickname: true, - }, - }, - }, - }); - - return article; - }; - - create = async data => { - const article = await this.db.create({ data }); - - return article; - }; - - update = async (id, data) => { - const article = await this.db.update({ where: { id }, data }); - - return article; - }; - - deleteById = async id => { - const article = await this.db.delete({ where: { id } }); - - return article; - }; - - like = async (articleId, userId) => { - const article = await this.article.udpate({ - where: { id: articleId }, - data: { - likeUsers: { connect: { id: userId } }, - likeCount: { increment: 1 }, - }, - }); - - return article; - }; - - unlike = async (articleId, userId) => { - const article = await this.article.udpate({ - where: { id: articleId }, - data: { - likeUsers: { disconnect: { id: userId } }, - likeCount: { decrement: 1 }, - }, - }); - - return article; - }; -} diff --git a/src/postgresql/repos/comment.repo.js b/src/postgresql/repos/comment.repo.js deleted file mode 100644 index 750b252a..00000000 --- a/src/postgresql/repos/comment.repo.js +++ /dev/null @@ -1,69 +0,0 @@ -import { TypeError } from '../../utils/error.js'; - -export class CommentRepo { - constructor(client) { - this.db = client.comment; - } - - findMany = async () => { - const comments = await this.db.findMany({ orderBy: { createdAt: 'desc' } }); - - return comments; - }; - - findManyAndCursor = async ({ id, limit, cursor, type }) => { - let typeOption; - switch (type) { - case 'article': - typeOption = { articleId: id }; - break; - case 'product': - typeOption = { productId: id }; - break; - default: - throw new TypeError('find type must be article or product'); - } - const pageOption = cursor ? { skip: 1, cursor: { id: cursor } } : {}; - - const comments = await this.db.findMany({ - where: typeOption, - orderBy: { createdAt: 'desc' }, - take: limit, - include: { - owner: { - select: { - nickname: true, - }, - }, - }, - ...pageOption, - }); - const nextCursor = comments.length ? { nextCursor: comments.at(-1).id } : {}; - - return { ...nextCursor, list: comments }; - }; - - findById = async id => { - const comment = await this.db.findUnique({ where: { id } }); - - return comment; - }; - - create = async data => { - const comment = await this.db.create({ data }); - - return comment; - }; - - update = async (id, data) => { - const comment = await this.db.update({ where: { id }, data }); - - return comment; - }; - - deleteById = async id => { - const comment = await this.db.delete({ where: { id } }); - - return comment; - }; -} diff --git a/src/postgresql/repos/user.repo.js b/src/postgresql/repos/user.repo.js deleted file mode 100644 index b0d07466..00000000 --- a/src/postgresql/repos/user.repo.js +++ /dev/null @@ -1,73 +0,0 @@ -export class UserRepo { - constructor(client) { - this.db = client.user; - } - - count = async keyword => { - const searchOption = keyword ? { where: { nickname: { contains: keyword } } } : {}; - - const count = await this.db.count(searchOption); - - return count; - }; - - findMany = async ({ orderBy, page, pageSize, keyword }) => { - let sortOption; - switch (orderBy) { - case 'like': - sortOption = { orderBy: { like: 'desc' } }; - break; - case 'recent': - default: - sortOption = { orderBy: { createdAt: 'desc' } }; - } - - const searchOption = keyword ? { where: { nickname: { contains: keyword } } } : {}; - - const users = await this.db.findMany({ - ...searchOption, - ...sortOption, - take: pageSize, - skip: page, - }); - - return users; - }; - - findById = async id => { - const user = await this.db.findUnique({ - where: { id }, - }); - - return user; - }; - - findByEmail = async email => { - const user = await this.db.findUnique({ - where: { email }, - }); - - return user; - }; - - findBySignInForm = async body => { - const { email, password } = body; - const user = await this.db.findFirst({ - where: { email, password }, - }); - - return user; - }; - - create = async data => { - const user = await this.db.create({ data }); - - return user; - }; - - update = async (id, data) => { - const user = await this.db.update({ where: { id }, data }); - - return user; - }; -} diff --git a/src/postgresql/services/article.service.js b/src/postgresql/services/article.service.js deleted file mode 100644 index 1a785f42..00000000 --- a/src/postgresql/services/article.service.js +++ /dev/null @@ -1,56 +0,0 @@ -export class ArticleService { - constructor(articleRepo) { - this.repo = articleRepo; - } - - getPaginatedArticles = async ({ orderBy, page, pageSize, keyword }) => { - const totalCount = await this.repo.count(keyword); - - const list = await this.repo.findMany({ - orderBy, - page, - pageSize, - keyword, - }); - - return { totalCount, list }; - }; - - getArticle = async id => { - const article = await this.repo.findById(id); - - return article; - }; - - postArticle = async body => { - const article = await this.repo.create(body); - - return article; - }; - - patchArticle = async (id, body) => { - const updated = await this.repo.update(id, body); - - return updated; - }; - - deleteArticle = async id => { - const article = await this.repo.deleteById(id); - - return article; - }; - - postArticleLike = async (articleId, userId) => { - const article = await this.repo.like(articleId, userId); - article.isLiked = true; - - return article; - }; - - deleteArticleLike = async (articleId, userId) => { - const article = await this.repo.unlike(articleId, userId); - article.isLiked = false; - - return article; - }; -} diff --git a/src/postgresql/services/auth.service.js b/src/postgresql/services/auth.service.js deleted file mode 100644 index 34fc6f10..00000000 --- a/src/postgresql/services/auth.service.js +++ /dev/null @@ -1,67 +0,0 @@ -import compareExp from '../../utils/compareExp.js'; -import createToken from '../../utils/createToken.js'; -import filterSensitiveData from '../../utils/filterSensitiveData.js'; -import hashingPassword from '../../utils/hashingPassword.js'; - -export class AuthService { - constructor(userRepo) { - this.repo = userRepo; - } - - getUserById = async id => { - const user = await this.repo.findById(id); - if (!user) return null; - - return filterSensitiveData(user); - }; - - createUser = async body => { - const exist = await this.repo.findByEmail(body.email); - if (exist) return true; - - const user = await this.repo.create(body); - if (!user) return null; - - return filterSensitiveData(user); - }; - - signIn = async body => { - const user = await this.repo.findByEmail(body.email); - if (!user) return null; - - const { salt, password } = user; - const hashedPassword = hashingPassword(body.password, salt); - - if (hashedPassword != password) return null; - - const accessToken = createToken(user, 'access'); - // NOTE refreshToken 발급 후 최근 발급된 토큰을 유저 정보에 저장 - const refreshToken = createToken(user, 'refresh'); - user.refreshToken = refreshToken; - await this.repo.update(user.id, user); - // NOTE 엄데이트될 데이터에 accessToken이 들어가지 않도록 뒤에서 추가 - user.accessToken = accessToken; - - return { user: filterSensitiveData(user), refreshToken }; - }; - - getNewToken = async (userToken, refreshToken) => { - const user = await this.repo.findById(userToken.userId); - if (!user) return null; - if (user.refreshToken !== refreshToken) return null; - - // NOTE 리프레시 토큰의 남은 시간이 2시간 이내일경우 - const timeRemaining = compareExp(userToken.exp); - if (timeRemaining < 3600 * 2) { - // NOTE 새 리프레시 토큰을 발급하고 이를 업데이트 - const refreshToken = createToken(user, 'refresh'); - user.refreshToken = refreshToken; - await this.repo.update(user); - } - - const accessToken = createToken(user, 'access'); - user.accessToken = accessToken; - - return filterSensitiveData(user); - }; -} diff --git a/src/postgresql/services/comment.service.js b/src/postgresql/services/comment.service.js deleted file mode 100644 index a49f2632..00000000 --- a/src/postgresql/services/comment.service.js +++ /dev/null @@ -1,56 +0,0 @@ -export class CommentService { - constructor(commentRepo) { - this.repo = commentRepo; - } - - getComments = async () => { - const comments = await this.repo.findMany(); - - return comments; - }; - - getPaginatedComments = async ({ id, limit, cursor, type }) => { - const resBody = await this.repo.findManyAndCursor({ - id, - limit, - cursor, - type, - }); - - return resBody; - }; - - postComment = async body => { - const comment = await this.repo.create(body); - - return comment; - }; - - patchComment = async (id, body) => { - const comment = await this.repo.findById(id); - if (!comment) return; - - Object.keys(body).forEach(k => { - comment[k] = body[k]; - }); - - const updated = await this.repo.update(id, comment); - - return updated; - }; - - putComment = async (id, body) => { - const comment = await this.repo.findById(id); - if (!comment) return; - - const updated = await this.repo.update(id, body); - - return updated; - }; - - deleteComment = async id => { - const comment = await this.repo.deleteById(id); - - return comment; - }; -} diff --git a/src/postgresql/services/product.service.js b/src/postgresql/services/product.service.js deleted file mode 100644 index 150b2ecd..00000000 --- a/src/postgresql/services/product.service.js +++ /dev/null @@ -1,65 +0,0 @@ -export class ProductService { - constructor(productRepo) { - this.repo = productRepo; - } - - getPaginatedProducts = async ({ orderBy, page, pageSize, keyword }) => { - const totalCount = await this.repo.count(keyword); - - const list = await this.repo.findMany({ - orderBy, - page, - pageSize, - keyword, - }); - - return { totalCount, list }; - }; - - getProduct = async id => { - const product = await this.repo.findById(id); - - const tags = product.productTags.map(tagObj => tagObj.tag); - const result = { ...product, tags, ownerNickname: product.owner.nickname }; - delete result.productTags; - delete result.owner; - - return result; - }; - - postProduct = async body => { - const { product, image } = await this.repo.create(body); - product.imageUrl = `/files/${image.fileName}`; - - return product; - }; - - patchProduct = async (id, body) => { - const product = await this.repo.findById(id); - if (!product) return; - - const updated = await this.repo.update(id, body); - - return updated; - }; - - deleteProduct = async id => { - const product = await this.repo.deleteById(id); - - return product; - }; - - postProductLike = async (productId, userId) => { - const product = await this.repo.like(productId, userId); - product.isLiked = true; - - return product; - }; - - deleteProductLike = async (productId, userId) => { - const product = await this.repo.unlike(productId, userId); - product.isLiked = false; - - return product; - }; -} diff --git a/src/postgresql/services/user.service.js b/src/postgresql/services/user.service.js deleted file mode 100644 index 0631811c..00000000 --- a/src/postgresql/services/user.service.js +++ /dev/null @@ -1,18 +0,0 @@ -export class UserService { - constructor(userRepo) { - this.repo = userRepo; - } - - getPaginatedUsers = async ({ orderBy, page, pageSize, keyword }) => { - const totalCount = await this.repo.count(keyword); - - const list = await this.repo.findMany({ - orderBy, - page, - pageSize, - keyword, - }); - - return { totalCount, list }; - }; -} diff --git a/src/repositories/article.repository.ts b/src/repositories/article.repository.ts new file mode 100644 index 00000000..97b071cb --- /dev/null +++ b/src/repositories/article.repository.ts @@ -0,0 +1,112 @@ +import { Prisma, PrismaClient } from '@prisma/client'; +import { OrderByCondition, WhereCondition } from '#types/conditions.type.js'; +import { ArticleDTO } from '#types/dtos.type.js'; +import { FindOptions } from '#types/options.type.js'; + +export class ArticleRepository { + private readonly article; + constructor(private readonly prismaClient: PrismaClient) { + this.article = prismaClient.article; + } + + count = async (keyword: string) => { + const searchOption: WhereCondition = keyword + ? { + where: { + OR: [{ title: { contains: keyword } }, { content: { contains: keyword } }], + }, + } + : {}; + + const count = await this.article.count(searchOption); + + return count; + }; + + findMany = async ({ orderBy, page, pageSize, keyword }: FindOptions) => { + let sortOption: OrderByCondition; + switch (orderBy) { + case 'like': + sortOption = { orderBy: { likeCount: Prisma.SortOrder.desc } }; + break; + case 'recent': + default: + sortOption = { orderBy: { createdAt: Prisma.SortOrder.desc } }; + } + + const searchOption: WhereCondition = keyword ? { where: { title: { contains: keyword } } } : {}; + + const articles = await this.article.findMany({ + ...searchOption, + ...sortOption, + take: pageSize, + skip: (page - 1) * pageSize, + include: { + owner: { + select: { + nickname: true, + }, + }, + }, + }); + + return articles; + }; + + findById = async (id: string) => { + const article = await this.article.findUnique({ + where: { id }, + include: { + owner: { + select: { + nickname: true, + }, + }, + }, + }); + + return article; + }; + + create = async (data: ArticleDTO) => { + const article = await this.article.create({ data }); + + return article; + }; + + update = async (id: string, data: ArticleDTO) => { + const article = await this.article.update({ where: { id }, data }); + + return article; + }; + + deleteById = async (id: string) => { + const article = await this.article.delete({ where: { id } }); + + return article; + }; + + like = async (articleId: string, userId: string) => { + const article = await this.article.update({ + where: { id: articleId }, + data: { + likeUsers: { connect: { id: userId } }, + likeCount: { increment: 1 }, + }, + }); + + return article; + }; + + unlike = async (articleId: string, userId: string) => { + const article = await this.article.update({ + where: { id: articleId }, + data: { + likeUsers: { disconnect: { id: userId } }, + likeCount: { decrement: 1 }, + }, + }); + + return article; + }; +} diff --git a/src/repositories/comment.repository.ts b/src/repositories/comment.repository.ts new file mode 100644 index 00000000..28915cf8 --- /dev/null +++ b/src/repositories/comment.repository.ts @@ -0,0 +1,75 @@ +import { Prisma, PrismaClient } from '@prisma/client'; +import { CursorCondition } from '#types/conditions.type.js'; +import { CommentDTO } from '#types/dtos.type.js'; +import { CommentFindOptions, CommentType } from '#types/options.type.js'; +import MESSAGES from '#utils/constants/messages.js'; +import { BadRequest } from '#utils/http-errors.js'; + +export class CommentRepository { + private readonly comment; + constructor(private readonly prismaClient: PrismaClient) { + this.comment = prismaClient.comment; + } + + findMany = async () => { + const comments = await this.comment.findMany({ orderBy: { createdAt: Prisma.SortOrder.desc } }); + + return comments; + }; + + findManyAndCursor = async ({ id, limit, cursor, type }: CommentFindOptions) => { + let typeOption; + switch (type) { + case CommentType.Article: + typeOption = { articleId: id }; + break; + case CommentType.Product: + typeOption = { productId: id }; + break; + default: + throw new BadRequest(MESSAGES.BAD_REQUEST); + } + const pageOption: CursorCondition = cursor ? { skip: 1, cursor: { id: cursor } } : {}; + + const comments = await this.comment.findMany({ + where: typeOption, + orderBy: { createdAt: Prisma.SortOrder.desc }, + take: limit, + include: { + owner: { + select: { + nickname: true, + }, + }, + }, + ...pageOption, + }); + const nextCursor = comments.length ? { nextCursor: comments.at(-1)?.id } : {}; + + return { ...nextCursor, list: comments }; + }; + + findById = async (id: string) => { + const comment = await this.comment.findUnique({ where: { id } }); + + return comment; + }; + + create = async (data: CommentDTO) => { + const comment = await this.comment.create({ data }); + + return comment; + }; + + update = async (id: string, data: { content: string }) => { + const comment = await this.comment.update({ where: { id }, data }); + + return comment; + }; + + deleteById = async (id: string) => { + const comment = await this.comment.delete({ where: { id } }); + + return comment; + }; +} diff --git a/src/postgresql/repos/product.repo.js b/src/repositories/product.repository.ts similarity index 56% rename from src/postgresql/repos/product.repo.js rename to src/repositories/product.repository.ts index 7b75a902..4e075b9b 100644 --- a/src/postgresql/repos/product.repo.js +++ b/src/repositories/product.repository.ts @@ -1,12 +1,20 @@ -export class ProductRepo { - constructor(client) { - this.product = client.product; - this.productTag = client.productTag; - this.productImage = client.productImage; +import { Prisma, PrismaClient } from '@prisma/client'; +import { OrderByCondition, WhereCondition } from '#types/conditions.type.js'; +import { ProductDTO } from '#types/dtos.type.js'; +import { FindOptions } from '#types/options.type.js'; + +export class ProductRepository { + private readonly product; + private readonly productTag; + private readonly productImage; + constructor(private readonly prismaClient: PrismaClient) { + this.product = prismaClient.product; + this.productTag = prismaClient.productTag; + this.productImage = prismaClient.productImage; } - count = async keyword => { - const searchOption = keyword + count = async (keyword: string) => { + const searchOption: WhereCondition = keyword ? { where: { OR: [{ name: { contains: keyword } }, { description: { contains: keyword } }], @@ -19,18 +27,18 @@ export class ProductRepo { return count; }; - findMany = async ({ orderBy, page, pageSize, keyword }) => { - let sortOption; + findMany = async ({ orderBy, page, pageSize, keyword }: FindOptions) => { + let sortOption: OrderByCondition; switch (orderBy) { case 'like': - sortOption = { orderBy: { likeCount: 'desc' } }; + sortOption = { orderBy: { likeCount: Prisma.SortOrder.desc } }; break; case 'recent': default: - sortOption = { orderBy: { createdAt: 'desc' } }; + sortOption = { orderBy: { createdAt: Prisma.SortOrder.desc } }; } - const searchOption = keyword + const searchOption: WhereCondition = keyword ? { where: { OR: [{ name: { contains: keyword } }, { description: { contains: keyword } }], @@ -48,7 +56,7 @@ export class ProductRepo { return products; }; - findById = async id => { + findById = async (id: string) => { const product = await this.product.findUnique({ where: { id }, include: { @@ -62,7 +70,7 @@ export class ProductRepo { return product; }; - create = async body => { + create = async (body: ProductDTO) => { const { tags, file, ...data } = body; const product = await this.product.create({ data }); const newTags = tags.map(tag => ({ tag, productId: product.id })); @@ -76,15 +84,21 @@ export class ProductRepo { return { product, image }; }; - update = async (id, body) => { - const { tags, images, ...data } = body; + update = async (id: string, body: ProductDTO) => { + const { tags, file, ...data } = body; + const fileData = { originalName: file.originalname, fileName: file.filename, productId: id }; + const product = await this.product.update({ where: { id }, data: { ...data, productTags: { deleteMany: {}, - create: tags.map(tag => ({ tag })), + create: tags.map(tag => ({ tag, productId: id })), + }, + productImages: { + deleteMany: {}, + create: fileData, }, }, }); @@ -92,14 +106,14 @@ export class ProductRepo { return product; }; - deleteById = async id => { + deleteById = async (id: string) => { const product = await this.product.delete({ where: { id } }); return product; }; - like = async (productId, userId) => { - const product = await this.product.udpate({ + like = async (productId: string, userId: string) => { + const product = await this.product.update({ where: { id: productId }, data: { likeUsers: { connect: { id: userId } }, @@ -110,8 +124,8 @@ export class ProductRepo { return product; }; - unlike = async (productId, userId) => { - const product = await this.product.udpate({ + unlike = async (productId: string, userId: string) => { + const product = await this.product.update({ where: { id: productId }, data: { likeUsers: { disconnect: { id: userId } }, diff --git a/src/repositories/user.repository.ts b/src/repositories/user.repository.ts new file mode 100644 index 00000000..92c364ff --- /dev/null +++ b/src/repositories/user.repository.ts @@ -0,0 +1,76 @@ +import { Prisma, PrismaClient } from '@prisma/client'; +import { WhereCondition } from '#types/conditions.type.js'; +import { UserDTO } from '#types/dtos.type.js'; +import { FindOptions } from '#types/options.type.js'; + +export class UserRepository { + private readonly user; + constructor(private readonly prismaClient: PrismaClient) { + this.user = prismaClient.user; + } + + count = async (keyword: string) => { + const searchOption: WhereCondition = keyword ? { where: { nickname: { contains: keyword } } } : {}; + + const count = await this.user.count(searchOption); + + return count; + }; + + findMany = async ({ orderBy, page, pageSize, keyword }: FindOptions) => { + let sortOption; + switch (orderBy) { + case 'recent': + default: + sortOption = { orderBy: { createdAt: Prisma.SortOrder.desc } }; + } + + const searchOption: WhereCondition = keyword ? { where: { nickname: { contains: keyword } } } : {}; + + const users = await this.user.findMany({ + ...searchOption, + ...sortOption, + take: pageSize, + skip: page, + }); + + return users; + }; + + findById = async (id: string) => { + const user = await this.user.findUnique({ + where: { id }, + }); + + return user; + }; + + findByEmail = async (email: string) => { + const user = await this.user.findUnique({ + where: { email }, + }); + + return user; + }; + + findBySignInForm = async (body: UserDTO) => { + const { email, password } = body; + const user = await this.user.findFirst({ + where: { email, password }, + }); + + return user; + }; + + create = async (data: UserDTO) => { + const user = await this.user.create({ data }); + + return user; + }; + + update = async (id: string, data: UserDTO) => { + const user = await this.user.update({ where: { id }, data }); + + return user; + }; +} diff --git a/src/routes/articles.route.js b/src/routes/articles.route.js deleted file mode 100644 index 79f6713a..00000000 --- a/src/routes/articles.route.js +++ /dev/null @@ -1,29 +0,0 @@ -import express from 'express'; -import { verifyAccessToken } from '../middlewares/auth.js'; -import postgresArticleController from '../postgresql/containers/article.container.js'; -import postgresCommentController from '../postgresql/containers/comment.container.js'; - -export const articleRouter = express.Router(); - -articleRouter - .route('/') - .get(postgresArticleController.getArticles) - .post(verifyAccessToken, postgresArticleController.postArticle); - -articleRouter - .route('/:id') - .get(postgresArticleController.getArticleById) - .patch(verifyAccessToken, postgresArticleController.patchArticle) - .delete(verifyAccessToken, postgresArticleController.deleteArticle); - -articleRouter - .route('/:id/comments') - .get(postgresCommentController.getCommentsOfArticle) - .post(verifyAccessToken, postgresCommentController.postCommentOfArticle); - -articleRouter - .route('/:id/like', verifyAccessToken) - .post(postgresArticleController.postArticleLike) - .delete(postgresArticleController.deleteArticleLike); - -export default articleRouter; diff --git a/src/routes/articles.route.ts b/src/routes/articles.route.ts new file mode 100644 index 00000000..64dcf74c --- /dev/null +++ b/src/routes/articles.route.ts @@ -0,0 +1,26 @@ +import express from 'express'; +import articleController from '#containers/article.container.js'; +import commentController from '#containers/comment.container.js'; +import tokenVerifier from '#containers/verify.container.js'; + +export const articleRouter = express.Router(); + +articleRouter.route('/').get(articleController.getArticles).post(tokenVerifier.verifyAccessToken, articleController.postArticle); + +articleRouter + .route('/:id') + .get(articleController.getArticleById) + .patch(tokenVerifier.verifyAccessToken, articleController.patchArticle) + .delete(tokenVerifier.verifyAccessToken, articleController.deleteArticle); + +articleRouter + .route('/:id/comments') + .get(commentController.getCommentsOfArticle) + .post(tokenVerifier.verifyAccessToken, commentController.postCommentOfArticle); + +articleRouter + .route('/:id/like') + .post(tokenVerifier.verifyAccessToken, articleController.postArticleLike) + .delete(tokenVerifier.verifyAccessToken, articleController.deleteArticleLike); + +export default articleRouter; diff --git a/src/routes/auth.route.js b/src/routes/auth.route.js deleted file mode 100644 index 36fe9c60..00000000 --- a/src/routes/auth.route.js +++ /dev/null @@ -1,23 +0,0 @@ -import express from 'express'; -import { verifyAccessToken, verifyRefreshToken } from '../middlewares/auth.js'; -import hashPassword from '../middlewares/hash.js'; -import postgresAuthController from '../postgresql/containers/auth.container.js'; -import compareExp from '../utils/compareExp.js'; - -export const authRouter = express.Router(); - -authRouter.use('/', (req, res, next) => { - console.log(req.cookies); - next(); -}); -authRouter.post('/signUp', hashPassword, postgresAuthController.signUp); -authRouter.post('/signIn', postgresAuthController.signIn); -authRouter.get('/me', verifyAccessToken, postgresAuthController.getMe); -authRouter.post('/refresh', verifyRefreshToken, postgresAuthController.refreshToken); - -authRouter.get('/test', verifyAccessToken, (req, res) => { - console.log('user: ', req.user); - compareExp(req.user.exp); -}); - -export default authRouter; diff --git a/src/routes/auth.route.ts b/src/routes/auth.route.ts new file mode 100644 index 00000000..b50475cc --- /dev/null +++ b/src/routes/auth.route.ts @@ -0,0 +1,17 @@ +import express from 'express'; +import authController from '#containers/auth.container.js'; +import tokenVerifier from '#containers/verify.container.js'; +import hashPassword from '#middlewares/hash.js'; + +export const authRouter = express.Router(); + +authRouter.use('/', (req, res, next) => { + console.log(req.cookies); + next(); +}); +authRouter.post('/signUp', hashPassword, authController.signUp); +authRouter.post('/signIn', authController.signIn); +authRouter.get('/me', tokenVerifier.verifyAccessToken, authController.getMe); +authRouter.post('/refresh', tokenVerifier.verifyRefreshToken, authController.refreshToken); + +export default authRouter; diff --git a/src/routes/comments.route.js b/src/routes/comments.route.js deleted file mode 100644 index ed7379bf..00000000 --- a/src/routes/comments.route.js +++ /dev/null @@ -1,13 +0,0 @@ -import express from 'express'; -import { verifyAccessToken } from '../middlewares/auth.js'; -import postgresCommentController from '../postgresql/containers/comment.container.js'; - -export const commentRouter = express.Router(); - -commentRouter - .route('/:id') - .patch(verifyAccessToken, postgresCommentController.patchComment) - .put(verifyAccessToken, postgresCommentController.putComment) - .delete(verifyAccessToken, postgresCommentController.deleteComment); - -export default commentRouter; diff --git a/src/routes/comments.route.ts b/src/routes/comments.route.ts new file mode 100644 index 00000000..bae9b01c --- /dev/null +++ b/src/routes/comments.route.ts @@ -0,0 +1,13 @@ +import express from 'express'; +import commentController from '#containers/comment.container.js'; +import tokenVerifier from '#containers/verify.container.js'; + +export const commentRouter = express.Router(); + +commentRouter + .route('/:id') + .patch(tokenVerifier.verifyAccessToken, commentController.patchComment) + .put(tokenVerifier.verifyAccessToken, commentController.putComment) + .delete(tokenVerifier.verifyAccessToken, commentController.deleteComment); + +export default commentRouter; diff --git a/src/routes/dev.route.js b/src/routes/dev.route.js deleted file mode 100644 index c6a55a6d..00000000 --- a/src/routes/dev.route.js +++ /dev/null @@ -1,21 +0,0 @@ -import express from 'express'; -import postgresCommentController from '../postgresql/containers/comment.container.js'; -import postgresUserController from '../postgresql/containers/user.container.js'; - -export const devRouter = express.Router(); - -devRouter.get('/users', postgresUserController.getUsersDev); - -devRouter.get('/comments', postgresCommentController.getCommentsDev); - -devRouter.get('/error', async (req, res) => { - throw new Error('TEST ERROR'); -}); - -devRouter.post('/files', (req, res, next) => { - console.log(req.file); - const path = `/dev/files/${req.file.filename}`; - res.json({ message: 'File Uploaded' }); -}); - -export default devRouter; diff --git a/src/routes/dev.route.ts b/src/routes/dev.route.ts new file mode 100644 index 00000000..500faf49 --- /dev/null +++ b/src/routes/dev.route.ts @@ -0,0 +1,40 @@ +import express from 'express'; +import authController from '#containers/auth.container.js'; +import commentController from '#containers/comment.container.js'; +import userController from '#containers/user.container.js'; +import tokenVerifier from '#containers/verify.container.js'; +import { getStorage } from '#middlewares/asyncLocalStorage.js'; + +export const devRouter = express.Router(); + +devRouter.get('/users', userController.getUsersDev); + +devRouter.get('/comments', commentController.getCommentsDev); + +devRouter.get('/error', async (req, res) => { + throw new Error('TEST ERROR'); +}); + +devRouter.post('/files', (req, res, next) => { + console.log(req.file); + const path = `/dev/files/${req.file!.filename}`; + res.json({ message: 'File Uploaded' }); +}); + +devRouter.post( + '/refresh', + (req, res, next) => { + console.log(req.cookies); + next(); + }, + tokenVerifier.verifyRefreshToken, + (req, res, next) => { + const storage = getStorage(); + console.log('🚀 ~ storage:', storage); + + next(); + }, + authController.refreshToken, +); + +export default devRouter; diff --git a/src/routes/products.route.js b/src/routes/products.route.js deleted file mode 100644 index 613b44c5..00000000 --- a/src/routes/products.route.js +++ /dev/null @@ -1,40 +0,0 @@ -import express from 'express'; -import { verifyAccessToken } from '../middlewares/auth.js'; -import validateProduct from '../middlewares/product.validation.js'; -import postgresCommentController from '../postgresql/containers/comment.container.js'; -import postgresProductController from '../postgresql/containers/product.container.js'; -// import { productController as mongocbProductController } from '../mongodb/containers/product.container.js'; - -export const productRouter = express.Router(); - -// mongoDB (deprecated) -// productRouter.route('/').get(mongodbProductController.getProducts).post(mongodbProductController.postProduct); -// productRouter -// .route('/:id') -// .get(mongodbProductController.getProductById) -// .patch(mongodbProductController.patchProduct) -// .delete(mongodbProductController.deleteProduct); - -// postgreSQL -productRouter - .route('/') - .get(postgresProductController.getProducts) - .post(verifyAccessToken, validateProduct, postgresProductController.postProduct); - -productRouter - .route('/:id') - .get(postgresProductController.getProductById) - .patch(verifyAccessToken, validateProduct, postgresProductController.patchProduct) - .delete(verifyAccessToken, postgresProductController.deleteProduct); - -productRouter - .route('/:id/comments') - .get(postgresCommentController.getCommentsOfProduct) - .post(verifyAccessToken, postgresCommentController.postCommentOfProduct); - -productRouter - .route('/:id/like', verifyAccessToken) - .post(postgresProductController.postProductLike) - .delete(postgresProductController.deleteProductLike); - -export default productRouter; diff --git a/src/routes/products.route.ts b/src/routes/products.route.ts new file mode 100644 index 00000000..492de990 --- /dev/null +++ b/src/routes/products.route.ts @@ -0,0 +1,30 @@ +import express from 'express'; +import commentController from '#containers/comment.container.js'; +import productController from '#containers/product.container.js'; +import tokenVerifier from '#containers/verify.container.js'; +import validateProduct from '#middlewares/product.validation.js'; + +export const productRouter = express.Router(); + +productRouter + .route('/') + .get(productController.getProducts) + .post(tokenVerifier.verifyAccessToken, validateProduct, productController.postProduct); + +productRouter + .route('/:id') + .get(productController.getProductById) + .patch(tokenVerifier.verifyAccessToken, validateProduct, productController.patchProduct) + .delete(tokenVerifier.verifyAccessToken, productController.deleteProduct); + +productRouter + .route('/:id/comments') + .get(commentController.getCommentsOfProduct) + .post(tokenVerifier.verifyAccessToken, commentController.postCommentOfProduct); + +productRouter + .route('/:id/like') + .post(tokenVerifier.verifyAccessToken, productController.postProductLike) + .delete(tokenVerifier.verifyAccessToken, productController.deleteProductLike); + +export default productRouter; diff --git a/src/services/article.service.ts b/src/services/article.service.ts new file mode 100644 index 00000000..5f57d7cf --- /dev/null +++ b/src/services/article.service.ts @@ -0,0 +1,70 @@ +import { ArticleRepository } from '#repositories/article.repository.js'; +import { ArticleDTO } from '#types/dtos.type.js'; +import { FindOptions } from '#types/options.type.js'; +import assertExist from '#utils/assertExist.js'; + +export class ArticleService { + constructor(private readonly articleRepository: ArticleRepository) {} + + getPaginatedArticles = async ({ orderBy, page, pageSize, keyword }: FindOptions) => { + const totalCount = await this.articleRepository.count(keyword); + + const list = await this.articleRepository.findMany({ + orderBy, + page, + pageSize, + keyword, + }); + + return { totalCount, list }; + }; + + getArticle = async (id: string) => { + const article = await this.articleRepository.findById(id); + assertExist(article); + + return article; + }; + + postArticle = async (body: ArticleDTO) => { + const article = await this.articleRepository.create(body); + + return article; + }; + + patchArticle = async (id: string, body: ArticleDTO) => { + const target = await this.articleRepository.findById(id); + assertExist(target); + + const updated = await this.articleRepository.update(id, body); + + return updated; + }; + + deleteArticle = async (id: string) => { + const target = await this.articleRepository.findById(id); + assertExist(target); + + const article = await this.articleRepository.deleteById(id); + + return article; + }; + + postArticleLike = async (articleId: string, userId: string) => { + const target = await this.articleRepository.findById(articleId); + assertExist(target); + + const article = await this.articleRepository.like(articleId, userId); + + return { ...article, isLiked: true }; + }; + + deleteArticleLike = async (articleId: string, userId: string) => { + const target = await this.articleRepository.findById(articleId); + assertExist(target); + + const article = await this.articleRepository.unlike(articleId, userId); + + return { ...article, isLiked: false }; + }; +} diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts new file mode 100644 index 00000000..62dec26c --- /dev/null +++ b/src/services/auth.service.ts @@ -0,0 +1,76 @@ +import { getStorage } from '#middlewares/asyncLocalStorage.js'; +import { UserRepository } from '#repositories/user.repository.js'; +import { UserDTO } from '#types/dtos.type.js'; +import assertExist from '#utils/assertExist.js'; +import compareExp from '#utils/compareExp.js'; +import MESSAGES from '#utils/constants/messages.js'; +import createToken from '#utils/createToken.js'; +import filterSensitiveData from '#utils/filterSensitiveData.js'; +import hashingPassword from '#utils/hashingPassword.js'; +import { Unauthorized } from '#utils/http-errors.js'; + +export class AuthService { + constructor(private readonly userRepository: UserRepository) {} + + getUserById = async (id: string) => { + const user = await this.userRepository.findById(id); + assertExist(user); + + return filterSensitiveData(user); + }; + + createUser = async (body: UserDTO) => { + const exist = await this.userRepository.findByEmail(body.email); + if (exist) return true; + + const user = await this.userRepository.create(body); + + return filterSensitiveData(user); + }; + + signIn = async (body: UserDTO) => { + const user = await this.userRepository.findByEmail(body.email); + if (!user) { + throw new Unauthorized(MESSAGES.WRONG_CREDENTIAL); + } + + const { salt, password } = user; + const hashedPassword = hashingPassword(body.password, salt); + + if (hashedPassword != password) { + throw new Unauthorized(MESSAGES.WRONG_CREDENTIAL); + } + + const accessToken = createToken(user, 'access'); + // NOTE refreshToken 발급 후 최근 발급된 토큰을 유저 정보에 저장 + const refreshToken = createToken(user, 'refresh'); + user.refreshToken = refreshToken; + await this.userRepository.update(user.id, user); + + return { user: { ...filterSensitiveData(user), accessToken }, refreshToken }; + }; + + getNewToken = async () => { + const storage = getStorage(); + const { userId, exp, refreshToken } = storage; + + const user = await this.userRepository.findById(userId); + assertExist(user); + if (user.refreshToken !== refreshToken) { + throw new Unauthorized(MESSAGES.INVALID_TOKEN); + } + + // NOTE 리프레시 토큰의 남은 시간이 2시간 이내일경우 + const timeRemaining = compareExp(exp); + if (timeRemaining < 3600 * 2) { + // NOTE 새 리프레시 토큰을 발급하고 이를 업데이트 + const refreshToken = createToken(user, 'refresh'); + user.refreshToken = refreshToken; + await this.userRepository.update(user.id, user); + } + + const accessToken = createToken(user, 'access'); + + return { ...filterSensitiveData(user), accessToken }; + }; +} diff --git a/src/services/comment.service.ts b/src/services/comment.service.ts new file mode 100644 index 00000000..84882b37 --- /dev/null +++ b/src/services/comment.service.ts @@ -0,0 +1,58 @@ +import { CommentRepository } from '#repositories/comment.repository.js'; +import { CommentDTO } from '#types/dtos.type.js'; +import { CommentFindOptions } from '#types/options.type.js'; +import assertExist from '#utils/assertExist.js'; + +export class CommentService { + constructor(private readonly commentRepository: CommentRepository) {} + + getComments = async () => { + const comments = await this.commentRepository.findMany(); + + return comments; + }; + + getPaginatedComments = async ({ id, limit, cursor, type }: CommentFindOptions) => { + const resBody = await this.commentRepository.findManyAndCursor({ + id, + limit, + cursor, + type, + }); + + return resBody; + }; + + postComment = async (body: CommentDTO) => { + const comment = await this.commentRepository.create(body); + + return comment; + }; + + patchComment = async (id: string, body: { content: string }) => { + const target = await this.commentRepository.findById(id); + assertExist(target); + + const comment = await this.commentRepository.update(id, body); + + return comment; + }; + + putComment = async (id: string, body: CommentDTO) => { + const target = await this.commentRepository.findById(id); + assertExist(target); + + const comment = await this.commentRepository.update(id, body); + + return comment; + }; + + deleteComment = async (id: string) => { + const target = await this.commentRepository.findById(id); + assertExist(target); + + const comment = await this.commentRepository.deleteById(id); + + return comment; + }; +} diff --git a/src/services/product.service.ts b/src/services/product.service.ts new file mode 100644 index 00000000..72f96eb3 --- /dev/null +++ b/src/services/product.service.ts @@ -0,0 +1,75 @@ +import { ProductRepository } from '#repositories/product.repository.js'; +import { ProductDTO } from '#types/dtos.type.js'; +import { FindOptions } from '#types/options.type.js'; +import assertExist from '#utils/assertExist.js'; + +export class ProductService { + constructor(private readonly productRepository: ProductRepository) {} + + getPaginatedProducts = async ({ orderBy, page, pageSize, keyword }: FindOptions) => { + const totalCount = await this.productRepository.count(keyword); + + const list = await this.productRepository.findMany({ + orderBy, + page, + pageSize, + keyword, + }); + + return { totalCount, list }; + }; + + getProduct = async (id: string) => { + const product = await this.productRepository.findById(id); + assertExist(product); + + const tags = product.productTags.map(tagObj => tagObj.tag); + const { productTags, owner, ...filtered } = product; + const result = { ...filtered, tags, ownerNickname: owner?.nickname }; + + return result; + }; + + postProduct = async (body: ProductDTO) => { + const { product, image } = await this.productRepository.create(body); + const imageUrl = `/files/${image.fileName}`; + + return { product, imageUrl }; + }; + + patchProduct = async (id: string, body: ProductDTO) => { + const target = await this.productRepository.findById(id); + assertExist(target); + + const product = await this.productRepository.update(id, body); + + return product; + }; + + deleteProduct = async (id: string) => { + const target = await this.productRepository.findById(id); + assertExist(target); + + const product = await this.productRepository.deleteById(id); + + return product; + }; + + postProductLike = async (productId: string, userId: string) => { + const target = await this.productRepository.findById(productId); + assertExist(target); + + const product = await this.productRepository.like(productId, userId); + + return { product, isLiked: true }; + }; + + deleteProductLike = async (productId: string, userId: string) => { + const target = await this.productRepository.findById(productId); + assertExist(target); + + const product = await this.productRepository.unlike(productId, userId); + + return { product, isLiked: false }; + }; +} diff --git a/src/services/user.service.ts b/src/services/user.service.ts new file mode 100644 index 00000000..1ecff2fe --- /dev/null +++ b/src/services/user.service.ts @@ -0,0 +1,28 @@ +import { User } from '@prisma/client'; +import { UserRepository } from '#repositories/user.repository.js'; +import { FindOptions } from '#types/options.type.js'; +import assertExist from '#utils/assertExist.js'; + +export class UserService { + constructor(private readonly userRepository: UserRepository) {} + + getPaginatedUsers = async ({ orderBy, page, pageSize, keyword }: FindOptions) => { + const totalCount = await this.userRepository.count(keyword); + + const list = await this.userRepository.findMany({ + orderBy, + page, + pageSize, + keyword, + }); + + return { totalCount, list }; + }; + + getUserById = async (id: string): Promise => { + const user = await this.userRepository.findById(id); + assertExist(user); + + return user; + }; +} diff --git a/src/types/common.type.ts b/src/types/common.type.ts new file mode 100644 index 00000000..203d7afe --- /dev/null +++ b/src/types/common.type.ts @@ -0,0 +1,9 @@ +export interface IStorage { + [key: string]: any; +} + +export interface UserToken { + userId: string; + exp: number; + iat: number; +} diff --git a/src/types/conditions.type.ts b/src/types/conditions.type.ts new file mode 100644 index 00000000..aa160162 --- /dev/null +++ b/src/types/conditions.type.ts @@ -0,0 +1,12 @@ +export interface WhereCondition { + where?: object; +} + +export interface OrderByCondition { + orderBy?: object; +} + +export interface CursorCondition { + skip?: number; + cursor?: { id: string }; +} diff --git a/src/types/dtos.type.ts b/src/types/dtos.type.ts new file mode 100644 index 00000000..864eb17f --- /dev/null +++ b/src/types/dtos.type.ts @@ -0,0 +1,16 @@ +import { Article, Comment, Product, User } from '@prisma/client'; + +export interface ModelBase { + id: string; + createdAt: Date; + updatedAt: Date; +} + +export interface ArticleDTO extends Omit {} +export interface UserDTO extends Omit {} +export interface SignDTO extends Pick {} +export interface CommentDTO extends Omit {} +export interface ProductDTO extends Omit { + tags: string[]; + file: { originalname: string; filename: string }; +} diff --git a/src/types/options.type.ts b/src/types/options.type.ts new file mode 100644 index 00000000..1e017760 --- /dev/null +++ b/src/types/options.type.ts @@ -0,0 +1,24 @@ +export interface OffsetPaginationOptions { + page: number; + pageSize: number; +} + +export interface CursorPaginationOptions { + cursor: string; + limit: number; +} + +export interface FindOptions extends OffsetPaginationOptions { + orderBy: string; + keyword: string; +} + +export enum CommentType { + Article = 'Article', + Product = 'Product', +} + +export interface CommentFindOptions extends CursorPaginationOptions { + id: string; + type: CommentType; +} diff --git a/src/types/request.type.ts b/src/types/request.type.ts new file mode 100644 index 00000000..d9dbcb3c --- /dev/null +++ b/src/types/request.type.ts @@ -0,0 +1,24 @@ +import type { Request as expressRequest } from 'express'; +import { NextFunction, Response } from 'express-serve-static-core'; + +export interface Request + extends expressRequest< + T extends { params: infer ParamsType } ? ParamsType : {}, + T extends { response: infer ResponseType } ? ResponseType : {}, + T extends { body: infer BodyType } ? BodyType : {}, + T extends { query: infer QueryType } ? QueryType : {} + > { + user?: { + userId: string; + exp: number; + iat: number; + }; +} + +export interface RequestHandler { + (req: Request, res: Response, next: NextFunction): void | Promise; +} + +export interface ErrorRequestHandler { + (err: any, req: Request, res: Response, next: NextFunction): void | Promise; +} diff --git a/src/utils/assertExist.ts b/src/utils/assertExist.ts new file mode 100644 index 00000000..05085b78 --- /dev/null +++ b/src/utils/assertExist.ts @@ -0,0 +1,9 @@ +import { ModelBase } from '#types/dtos.type.js'; +import MESSAGES from '#utils/constants/messages.js'; +import { NotFound } from '#utils/http-errors.js'; + +export default function assertExist(target: T | null): asserts target is T { + if (target === null) { + throw new NotFound(MESSAGES.NOT_FOUND); + } +} diff --git a/src/utils/compareExp.js b/src/utils/compareExp.ts similarity index 90% rename from src/utils/compareExp.js rename to src/utils/compareExp.ts index 0935d637..f3623133 100644 --- a/src/utils/compareExp.js +++ b/src/utils/compareExp.ts @@ -1,6 +1,6 @@ import secondsToHMS from './secToHMS.js'; -export default function compareExp(exp) { +export default function compareExp(exp: number) { const currentTime = Math.floor(Date.now() / 1000); if (currentTime > exp) { diff --git a/src/utils/constants.js b/src/utils/constants.js deleted file mode 100644 index 9d2d30d3..00000000 --- a/src/utils/constants.js +++ /dev/null @@ -1,18 +0,0 @@ -export default class CONSTANTS { - static MESSAGES = Object.freeze({ - NOID: 'Cannot find given id.', - IDFORMAT: 'Invalid id format.', - }); - static HTTP_STATUS = Object.freeze({ - SUCCESS: 200, - CREATED: 201, - ACCEPTED: 202, - NON_AUTHORITATIVE_INFORMATION: 203, - NO_CONTENT: 204, - BAD_REQUEST: 400, - UNAUTHORIZED: 401, - FORBIDDEN: 403, - NOT_FOUND: 404, - SERVER_ERROR: 500, - }); -} diff --git a/src/utils/constants/http-codes.ts b/src/utils/constants/http-codes.ts new file mode 100644 index 00000000..5ccd3e73 --- /dev/null +++ b/src/utils/constants/http-codes.ts @@ -0,0 +1,14 @@ +enum HTTP_CODES { + OK = 200, + CREATED = 201, + ACCEPTED = 202, + NON_AUTHORITATIVE_INFORMATION = 203, + NO_CONTENT = 204, + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + FORBIDDEN = 403, + NOT_FOUND = 404, + SERVER_ERROR = 500, +} + +export default HTTP_CODES; diff --git a/src/utils/constants/messages.ts b/src/utils/constants/messages.ts new file mode 100644 index 00000000..ed072173 --- /dev/null +++ b/src/utils/constants/messages.ts @@ -0,0 +1,14 @@ +enum MESSAGES { + BAD_REQUEST = '잘못된 요청입니다.', + UNAUTHORIZED = '로그인이 필요합니다.', + FORBIDDEN = '접근 권한이 없습니다.', + NOT_FOUND = '찾을 수 없는 리소스입니다.', + INTERNAL_ERROR = '서버 내부 오류가 발생했습니다.', + + IDFORMAT = 'ID 형식이 올바르지 않습니다.', + IS_EXIST = '이미 존재하는 리소스입니다.', + WRONG_CREDENTIAL = '아이디 혹은 비밀번호가 틀렸습니다.', + INVALID_TOKEN = '토큰이 잘못되었습니다.', +} + +export default MESSAGES; diff --git a/src/utils/createToken.js b/src/utils/createToken.js deleted file mode 100644 index a4f90123..00000000 --- a/src/utils/createToken.js +++ /dev/null @@ -1,26 +0,0 @@ -import jwt from 'jsonwebtoken'; -import { jwtSecret } from '../configs/auth.config.js'; - -export default function createToken(user, type) { - jwt; - const payload = { userId: user.id }; - let options; - switch (type) { - case 'access': - options = { expiresIn: EXPIRE_TIME.ACCESS }; - break; - case 'refresh': - options = { expiresIn: EXPIRE_TIME.REFRESH }; - break; - default: - } - - const token = jwt.sign(payload, jwtSecret, options); - - return token; -} - -const EXPIRE_TIME = Object.freeze({ - ACCESS: '1h', - REFRESH: '2w', -}); diff --git a/src/utils/createToken.ts b/src/utils/createToken.ts new file mode 100644 index 00000000..73815bfd --- /dev/null +++ b/src/utils/createToken.ts @@ -0,0 +1,21 @@ +import { User } from '@prisma/client'; +import jwt from 'jsonwebtoken'; +import { accessExpireTime, jwtSecret, refreshExpireTime } from '#configs/jwt.config.js'; + +export default function createToken(user: User, type: string) { + const payload = { userId: user.id }; + let options; + switch (type) { + case 'access': + options = { expiresIn: accessExpireTime }; + break; + case 'refresh': + options = { expiresIn: refreshExpireTime }; + break; + default: + } + + const token = jwt.sign(payload, jwtSecret, options); + + return token; +} diff --git a/src/utils/error.js b/src/utils/error.js deleted file mode 100644 index 7f6fdd68..00000000 --- a/src/utils/error.js +++ /dev/null @@ -1,25 +0,0 @@ -class TypeError extends Error { - constructor(message) { - super(message); - this.name = 'TypeError'; - this.statusCode = 400; - } -} - -class ValidationError extends Error { - constructor(message) { - super(message); - this.name = 'ValidationError'; - this.statusCode = 400; - } -} - -class CastError extends Error { - constructor(message) { - super(message); - this.name = 'CastError'; - this.statusCode = 404; - } -} - -export { TypeError, ValidationError, CastError }; diff --git a/src/utils/filterSensitiveData.js b/src/utils/filterSensitiveData.js deleted file mode 100644 index 382dbc2d..00000000 --- a/src/utils/filterSensitiveData.js +++ /dev/null @@ -1,5 +0,0 @@ -export default function filterSensitiveData(data) { - const { password, salt, refreshToken, ...rest } = data; - - return rest; -} diff --git a/src/utils/filterSensitiveData.ts b/src/utils/filterSensitiveData.ts new file mode 100644 index 00000000..7c368003 --- /dev/null +++ b/src/utils/filterSensitiveData.ts @@ -0,0 +1,7 @@ +import { User } from '@prisma/client'; + +export default function filterSensitiveData(data: User) { + const { password, salt, refreshToken, ...rest } = data; + + return rest; +} diff --git a/src/utils/hashingPassword.js b/src/utils/hashingPassword.ts similarity index 72% rename from src/utils/hashingPassword.js rename to src/utils/hashingPassword.ts index 5b415442..4b97da61 100644 --- a/src/utils/hashingPassword.js +++ b/src/utils/hashingPassword.ts @@ -1,6 +1,6 @@ import crypto from 'crypto'; -export default function hashingPassword(password, salt) { +export default function hashingPassword(password: string, salt: string) { const iterations = 25000; const keyLength = 64; diff --git a/src/utils/http-errors.ts b/src/utils/http-errors.ts new file mode 100644 index 00000000..0136333e --- /dev/null +++ b/src/utils/http-errors.ts @@ -0,0 +1,51 @@ +export class HttpError extends Error { + status: number; + name: string = 'HttpError'; + + constructor(message: string, status: number) { + super(message); + this.status = status; + // NOTE 프로토타입 체인을 설정하지 않으면 instanceof 등의 연산에서 문제가 발생할 수 있음 + Object.setPrototypeOf(this, HttpError.prototype); + } +} + +export class BadRequest extends HttpError { + constructor(message: string) { + super(message, 400); + this.name = 'BadRequest'; + Object.setPrototypeOf(this, BadRequest.prototype); + } +} + +export class Unauthorized extends HttpError { + constructor(message: string) { + super(message, 401); + this.name = 'Unauthorized'; + Object.setPrototypeOf(this, Unauthorized.prototype); + } +} + +export class Forbidden extends HttpError { + constructor(message: string) { + super(message, 403); + this.name = 'Forbidden'; + Object.setPrototypeOf(this, Forbidden.prototype); + } +} + +export class NotFound extends HttpError { + constructor(message: string) { + super(message, 404); + this.name = 'NotFound'; + Object.setPrototypeOf(this, NotFound.prototype); + } +} + +export class InternalServerError extends HttpError { + constructor(message: string) { + super(message, 500); + this.name = 'InternalServerError'; + Object.setPrototypeOf(this, InternalServerError.prototype); + } +} diff --git a/src/utils/secToHMS.js b/src/utils/secToHMS.ts similarity index 67% rename from src/utils/secToHMS.js rename to src/utils/secToHMS.ts index aa8b064e..4cb9a870 100644 --- a/src/utils/secToHMS.js +++ b/src/utils/secToHMS.ts @@ -1,9 +1,9 @@ -export default function secondsToHMS(seconds) { +export default function secondsToHMS(seconds: number) { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const remainingSeconds = seconds % 60; - const formatNumber = num => num.toString().padStart(2, '0'); + const formatNumber = (num: number) => num.toString().padStart(2, '0'); const result = `${formatNumber(hours)}:${formatNumber(minutes)}:${formatNumber(remainingSeconds)}`; diff --git a/src/struct.js b/src/utils/struct.ts similarity index 88% rename from src/struct.js rename to src/utils/struct.ts index 6818f318..d5928d6a 100644 --- a/src/struct.js +++ b/src/utils/struct.ts @@ -2,9 +2,8 @@ import isEmail from 'is-email'; import isUuid from 'is-uuid'; import * as s from 'superstruct'; -export const MongodbId = s.size(s.string(), 24, 24); -export const Uuid = s.define('Uuid', value => isUuid.v4(value)); -export const Email = s.define('Email', isEmail); +export const Uuid = s.define('Uuid', value => isUuid.v4(value as string)); +export const Email = s.define('Email', isEmail as s.Validator); export const Cursor = s.optional(Uuid); export const SignIn = s.object({ @@ -46,6 +45,7 @@ export const PatchUser = s.object({ salt: s.optional(s.string()), refreshToken: s.optional(s.string()), }); + export const PatchProduct = s.partial(CreateProduct); export const PatchArticle = s.partial(CreateArticle); export const PatchComment = s.object({ content: s.string() }); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..34243b39 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,126 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "lib": ["ESNext"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, + // "jsx": "preserve", /* Specify what JSX code is generated. */ + "experimentalDecorators": true /* Enable experimental support for legacy experimental decorators. */, + "emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */, + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "Node16" /* Specify what module code is generated. */, + "rootDir": "src" /* Specify the root folder within your source files. */, + "moduleResolution": "node16" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + "paths": { + "#configs/*": ["./src/configs/*"], + "#connection/*": ["./src/connection/*"], + "#containers/*": ["./src/containers/*"], + "#controllers/*": ["./src/controllers/*"], + "#interfaces/*": ["./src/interfaces/*"], + "#middlewares/*": ["./src/middlewares/*"], + "#repositories/*": ["./src/repositories/*"], + "#routes/*": ["./src/routes/*"], + "#services/*": ["./src/services/*"], + "#types/*": ["./src/types/*"], + "#utils/*": ["./src/utils/*"], + "@/*": ["./*"] + } /* Specify a set of entries that re-map imports to additional lookup locations. */, + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, + // "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "dist" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, + // "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + // "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src/**/*", "types/**/*.d.ts"], + "exclude": ["node_modules", "dist", "prettier.config.mjs", "eslint.config.js"] +} diff --git a/uploads/667b87f8e42bc9ff30324da49948b6ee b/uploads/667b87f8e42bc9ff30324da49948b6ee deleted file mode 100644 index 88f99f4c..00000000 Binary files a/uploads/667b87f8e42bc9ff30324da49948b6ee and /dev/null differ diff --git a/uploads/7db8617c421bef93024118d5aefc6ceb b/uploads/7db8617c421bef93024118d5aefc6ceb deleted file mode 100644 index 88f99f4c..00000000 Binary files a/uploads/7db8617c421bef93024118d5aefc6ceb and /dev/null differ diff --git a/uploads/bb587dc2c4de3efa884dbf46e76dfc6f b/uploads/bb587dc2c4de3efa884dbf46e76dfc6f deleted file mode 100644 index 88f99f4c..00000000 Binary files a/uploads/bb587dc2c4de3efa884dbf46e76dfc6f and /dev/null differ diff --git a/uploads/cdc71dfcc74deb47a9d140f162c81d56 b/uploads/cdc71dfcc74deb47a9d140f162c81d56 deleted file mode 100644 index 88f99f4c..00000000 Binary files a/uploads/cdc71dfcc74deb47a9d140f162c81d56 and /dev/null differ diff --git a/uploads/d204a41efe0786bb49a83e3415b9bd31 b/uploads/d204a41efe0786bb49a83e3415b9bd31 deleted file mode 100644 index 88f99f4c..00000000 Binary files a/uploads/d204a41efe0786bb49a83e3415b9bd31 and /dev/null differ diff --git a/uploads/fc5f07ca1b22c5d987c250d13be0e9fb b/uploads/fc5f07ca1b22c5d987c250d13be0e9fb deleted file mode 100644 index 88f99f4c..00000000 Binary files a/uploads/fc5f07ca1b22c5d987c250d13be0e9fb and /dev/null differ