Compare commits
19 Commits
6942a2de8f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a04ff9a898 | |||
| 5585fb2557 | |||
| 5c61a7a67d | |||
| dca781418f | |||
| 42563e6f7c | |||
| da57c927ef | |||
| b581f76690 | |||
| 2902b0323e | |||
| a41f16132d | |||
| e352a26c66 | |||
| de6d8cd8fe | |||
| 70ad6ae4a6 | |||
| a4646da6ab | |||
| 96b06cf92d | |||
| 4f73991360 | |||
| 0db3d677b2 | |||
| 3b55f82130 | |||
| 2636816241 | |||
| ce914f91fd |
@@ -0,0 +1,45 @@
|
|||||||
|
name: Build and Push Image
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
types: [closed]
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
if: gitea.event.pull_request.merged == true
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Log in to Gitea Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: gitea.nelson-household.com # Replace with your Gitea domain
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.RUNNER_TOKEN }}
|
||||||
|
|
||||||
|
- name: Delete Old Images and Containers
|
||||||
|
run: |
|
||||||
|
docker ps -a -q -f "name=tcg-collectors-server" | xargs -I {} docker rm -f {} || true
|
||||||
|
docker images --format "{{.Repository}}:{{.Tag}}" | grep "tcg-collectors-server" | xargs -I {} docker rmi -f {} || true
|
||||||
|
|
||||||
|
- name: Build and Push Image
|
||||||
|
run: |
|
||||||
|
docker build \
|
||||||
|
--build-arg DATABASE_URL=${{ secrets.DATABASE_URL }} \
|
||||||
|
--build-arg DB_HOST=${{ secrets.DB_HOST }} \
|
||||||
|
--build-arg DB_USERNAME=${{ secrets.DB_USERNAME }} \
|
||||||
|
--build-arg DB_PASSWORD=${{ secrets.DB_PASSWORD }} \
|
||||||
|
--build-arg DB_PORT=${{ secrets.DB_PORT }} \
|
||||||
|
--build-arg DB_DATABASE=${{ secrets.DB_DATABASE }} \
|
||||||
|
--build-arg JWT_SECRET=${{ secrets.JWT_SECRET }} \
|
||||||
|
-t gitea.nelson-household.com/hard-at-work/tcg-collectors-server/tcg-collectors-server:${{ github.sha }} .
|
||||||
|
docker push gitea.nelson-household.com/hard-at-work/tcg-collectors-server/tcg-collectors-server:${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Execute Migrations
|
||||||
|
run: |
|
||||||
|
docker run gitea.nelson-household.com/hard-at-work/tcg-collectors-server/tcg-collectors-server:${{ github.sha }} npm run migrate up
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
name: Test Workflow
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-and-lint:
|
||||||
|
# Use the label matching your registered runner
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Or specify a custom image directly if your runner supports it
|
||||||
|
container:
|
||||||
|
image: node:24-alpine
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install --force
|
||||||
|
|
||||||
|
- name: Run linter
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: npm test
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
FROM node:24-alpine
|
||||||
|
|
||||||
|
ARG DATABASE_URL
|
||||||
|
ARG DB_HOST
|
||||||
|
ARG DB_USERNAME
|
||||||
|
ARG DB_PASSWORD
|
||||||
|
ARG DB_PORT
|
||||||
|
ARG DB_DATABASE
|
||||||
|
ARG JWT_SECRET
|
||||||
|
|
||||||
|
ENV DATABASE_URL=${DATABASE_URL}
|
||||||
|
ENV DB_HOST=${DB_HOST}
|
||||||
|
ENV DB_USERNAME=${DB_USERNAME}
|
||||||
|
ENV DB_PASSWORD=${DB_PASSWORD}
|
||||||
|
ENV DB_PORT=${DB_PORT}
|
||||||
|
ENV DB_DATABASE=${DB_DATABASE}
|
||||||
|
ENV JWT_SECRET=${JWT_SECRET}
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
RUN npm install --force && \
|
||||||
|
echo "DATABASE_URL=${DATABASE_URL}" >> .env && \
|
||||||
|
echo "DB_HOST=${DB_HOST}" >> .env && \
|
||||||
|
echo "DB_USERNAME=${DB_USERNAME}" >> .env && \
|
||||||
|
echo "DB_PASSWORD=${DB_PASSWORD}" >> .env && \
|
||||||
|
echo "DB_PORT=${DB_PORT}" >> .env && \
|
||||||
|
echo "DB_DATABASE=${DB_DATABASE}" >> .env && \
|
||||||
|
echo "JWT_SECRET=${JWT_SECRET}" >> .env
|
||||||
|
|
||||||
|
CMD ["npm", "run", "docker:start"]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// eslint.config.js (Advanced alternative)
|
||||||
|
import prettierPlugin from 'eslint-plugin-prettier'
|
||||||
|
import prettierConfig from 'eslint-config-prettier'
|
||||||
|
import prettierOptions from './prettier.config.js' // Import your config directly
|
||||||
|
import importPlugin from 'eslint-plugin-import'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
prettier: prettierPlugin,
|
||||||
|
import: importPlugin,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...prettierConfig.rules,
|
||||||
|
'prettier/prettier': ['error', prettierOptions],
|
||||||
|
'no-sequences': 'off',
|
||||||
|
'no-console': 'warn',
|
||||||
|
'import/first': 'error',
|
||||||
|
'import/newline-after-import': 'error',
|
||||||
|
'import/no-duplicates': 'error',
|
||||||
|
'import/no-unresolved': 'error',
|
||||||
|
'import/no-unused-modules': ['warn', { missingExports: true }],
|
||||||
|
quotes: ['error', 'single'],
|
||||||
|
semi: 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"name": "tcg-collectors-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A basic Express application with PostgreSQL integration.",
|
||||||
|
"main": "app.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nodemon src/app.js",
|
||||||
|
"start": "NODE_ENV=production node src/app.js",
|
||||||
|
"docker:start": "NODE_ENV=production node --env-file=.env src/app.js",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"migrate": "node-pg-migrate -m ./src/migrations",
|
||||||
|
"test": "jest --forceExit"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"express",
|
||||||
|
"node.js",
|
||||||
|
"postgres",
|
||||||
|
"postgresql",
|
||||||
|
"database"
|
||||||
|
],
|
||||||
|
"author": "Your Name",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
|
"body-parser": "^2.2.2",
|
||||||
|
"cors": "^2.8.6",
|
||||||
|
"csv-parser": "^3.0.0",
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"express": "^4.22.2",
|
||||||
|
"express-validator": "^7.3.2",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"node-pg": "^1.0.1",
|
||||||
|
"p-limit": "^7.3.0",
|
||||||
|
"pg": "^8.20.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.29.0",
|
||||||
|
"@babel/preset-env": "^7.29.5",
|
||||||
|
"babel-jest": "^30.4.1",
|
||||||
|
"eslint": "^10.3.0",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-import": "^2.32.0",
|
||||||
|
"eslint-plugin-prettier": "^5.5.5",
|
||||||
|
"jest": "^29.0.0",
|
||||||
|
"node-pg-migrate": "^8.0.4",
|
||||||
|
"nodemon": "^3.0.0",
|
||||||
|
"prettier": "^3.8.3",
|
||||||
|
"supertest": "^7.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// prettier.config.js
|
||||||
|
export default {
|
||||||
|
trailingComma: 'es5',
|
||||||
|
tabWidth: 2,
|
||||||
|
semi: false, // Crucial: Sets semicolon removal.
|
||||||
|
useTabs: true,
|
||||||
|
singleQuote: true,
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
import { loadEnvFile } from 'node:process' // load envs asap
|
||||||
|
|
||||||
|
import express from 'express'
|
||||||
|
import path from 'path'
|
||||||
|
import cors from 'cors'
|
||||||
|
import healthCheckRoutes from './routes/healthcheck/index.js'
|
||||||
|
import authRoutes from './routes/auth/index.js'
|
||||||
|
import userRoutes from './routes/users/index.js'
|
||||||
|
import roleRoutes from './routes/roles/index.js'
|
||||||
|
import locationRoutes from './routes/locations/index.js'
|
||||||
|
import imageRoutes from './routes/images/index.js'
|
||||||
|
import collectionRoutes from './routes/collections/index.js'
|
||||||
|
import setRoutes from './routes/sets/index.js'
|
||||||
|
import itemRoutes from './routes/items/index.js'
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
|
loadEnvFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
const port = process.env.PORT || 3000
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(express.json()) // For parsing application/json
|
||||||
|
app.use(express.urlencoded({ extended: true })) // For parsing URL-encoded form data
|
||||||
|
app.use(cors())
|
||||||
|
|
||||||
|
// Mount Routes
|
||||||
|
app.use('/healthcheck', healthCheckRoutes)
|
||||||
|
app.use('/auth', authRoutes)
|
||||||
|
app.use('/users', userRoutes)
|
||||||
|
app.use('/roles', roleRoutes)
|
||||||
|
app.use('/locations', locationRoutes)
|
||||||
|
app.use('/images', imageRoutes)
|
||||||
|
app.use('/collections', collectionRoutes)
|
||||||
|
app.use('/sets', setRoutes)
|
||||||
|
app.use('/items', itemRoutes)
|
||||||
|
|
||||||
|
// Default Route (Catch-all for undefined routes)
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
res.status(404).send('Sorry, the resource you requested could not be found.')
|
||||||
|
})
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Server listening on port ${port}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default app
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// config/index.js
|
||||||
|
import { Pool } from 'pg'
|
||||||
|
|
||||||
|
const database = {
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
user: process.env.DB_USERNAME,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_DATABASE,
|
||||||
|
port: parseInt(process.env.DB_PORT, 10),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pool = new Pool(database)
|
||||||
|
|
||||||
|
export const jwtEnv = {
|
||||||
|
secret: process.env.JWT_SECRET,
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export const up = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
CREATE TABLE images (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
file TEXT,
|
||||||
|
image BYTEA,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const down = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
DROP TABLE images;
|
||||||
|
`)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export const up = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
CREATE TABLE collections (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
name TEXT,
|
||||||
|
image_id UUID,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
FOREIGN KEY (image_id) REFERENCES images(id)
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const down = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
DROP TABLE collections;
|
||||||
|
`)
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export const up = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
CREATE TABLE items (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
collection_id UUID,
|
||||||
|
image_id UUID,
|
||||||
|
productId TEXT,
|
||||||
|
name TEXT,
|
||||||
|
cleanName TEXT,
|
||||||
|
extCardText TEXT,
|
||||||
|
marketPrice TEXT,
|
||||||
|
extRarity TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
FOREIGN KEY (collection_id) REFERENCES collections(id),
|
||||||
|
FOREIGN KEY (image_id) REFERENCES images(id)
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const down = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
DROP TABLE items;
|
||||||
|
`)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export const up = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
CREATE TABLE locations (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
name TEXT UNIQUE NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const down = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
DROP TABLE locations;
|
||||||
|
`)
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
export const up = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
CREATE TABLE sets (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
collection_id UUID NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
location_id UUID,
|
||||||
|
image_id UUID,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
FOREIGN KEY (collection_id) REFERENCES collections(id),
|
||||||
|
FOREIGN KEY (location_id) REFERENCES locations(id),
|
||||||
|
FOREIGN KEY (image_id) REFERENCES images(id)
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const down = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
DROP TABLE sets;
|
||||||
|
`)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export const up = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
CREATE TABLE roles (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
name TEXT UNIQUE NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const down = (pmg) => {
|
||||||
|
pgm.sql(`
|
||||||
|
DROP TABLE roles;
|
||||||
|
`)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export const up = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
CREATE TABLE users (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
username TEXT UNIQUE NOT NULL,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
role_id UUID,
|
||||||
|
email TEXT UNIQUE NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
FOREIGN KEY (role_id) REFERENCES roles(id)
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const down = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
DROP TABLE users;
|
||||||
|
`)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export const up = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE "items"
|
||||||
|
ADD COLUMN "set_id" UUID,
|
||||||
|
ADD CONSTRAINT "fk_set_id"
|
||||||
|
FOREIGN KEY ("set_id")
|
||||||
|
REFERENCES "sets"("id")
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const down = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE "items"
|
||||||
|
DROP COLUMN "set_id",
|
||||||
|
DROP CONSTRAINT "fk_set_id";
|
||||||
|
`)
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
export const up = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE collections
|
||||||
|
ADD COLUMN "user_id" UUID,
|
||||||
|
ADD CONSTRAINT fk_collections_user_id
|
||||||
|
FOREIGN KEY (user_id)
|
||||||
|
REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
`)
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE sets
|
||||||
|
ADD COLUMN "user_id" UUID,
|
||||||
|
ADD CONSTRAINT fk_sets_user_id
|
||||||
|
FOREIGN KEY (user_id)
|
||||||
|
REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const down = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE collections
|
||||||
|
DROP COLUMN "user_id",
|
||||||
|
DROP CONSTRAINT fk_collections_user_id;
|
||||||
|
`)
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE sets
|
||||||
|
DROP COLUMN "user_id",
|
||||||
|
DROP CONSTRAINT fk_sets_user_id;
|
||||||
|
`)
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
export const up = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE roles
|
||||||
|
ALTER COLUMN id DROP DEFAULT,
|
||||||
|
ALTER COLUMN id SET DATA TYPE UUID USING (gen_random_uuid()),
|
||||||
|
ALTER COLUMN id SET DEFAULT gen_random_uuid();
|
||||||
|
`)
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE images
|
||||||
|
ALTER COLUMN id DROP DEFAULT,
|
||||||
|
ALTER COLUMN id SET DATA TYPE UUID USING (gen_random_uuid()),
|
||||||
|
ALTER COLUMN id SET DEFAULT gen_random_uuid();
|
||||||
|
`)
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE collections
|
||||||
|
ALTER COLUMN id DROP DEFAULT,
|
||||||
|
ALTER COLUMN id SET DATA TYPE UUID USING (gen_random_uuid()),
|
||||||
|
ALTER COLUMN id SET DEFAULT gen_random_uuid();
|
||||||
|
`)
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE items
|
||||||
|
ALTER COLUMN id DROP DEFAULT,
|
||||||
|
ALTER COLUMN id SET DATA TYPE UUID USING (gen_random_uuid()),
|
||||||
|
ALTER COLUMN id SET DEFAULT gen_random_uuid();
|
||||||
|
`)
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE locations
|
||||||
|
ALTER COLUMN id DROP DEFAULT,
|
||||||
|
ALTER COLUMN id SET DATA TYPE UUID USING (gen_random_uuid()),
|
||||||
|
ALTER COLUMN id SET DEFAULT gen_random_uuid();
|
||||||
|
`)
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE sets
|
||||||
|
ALTER COLUMN id DROP DEFAULT,
|
||||||
|
ALTER COLUMN id SET DATA TYPE UUID USING (gen_random_uuid()),
|
||||||
|
ALTER COLUMN id SET DEFAULT gen_random_uuid();
|
||||||
|
`)
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE users
|
||||||
|
ALTER COLUMN id DROP DEFAULT,
|
||||||
|
ALTER COLUMN id SET DATA TYPE UUID USING (gen_random_uuid()),
|
||||||
|
ALTER COLUMN id SET DEFAULT gen_random_uuid();
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const down = (pmg) => {
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE roles
|
||||||
|
ALTER COLUMN id DROP DEFAULT;
|
||||||
|
`)
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE images
|
||||||
|
ALTER COLUMN id DROP DEFAULT;
|
||||||
|
`)
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE collections
|
||||||
|
ALTER COLUMN id DROP DEFAULT;
|
||||||
|
`)
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE items
|
||||||
|
ALTER COLUMN id DROP DEFAULT;
|
||||||
|
`)
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE locations
|
||||||
|
ALTER COLUMN id DROP DEFAULT;
|
||||||
|
`)
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE sets
|
||||||
|
ALTER COLUMN id DROP DEFAULT;
|
||||||
|
`)
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE users
|
||||||
|
ALTER COLUMN id DROP DEFAULT;
|
||||||
|
`)
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import jwt from 'jsonwebtoken'
|
||||||
|
import bcrypt from 'bcryptjs'
|
||||||
|
import { pool, jwtEnv } from '../../config/index.js'
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
// Route to handle login and issue JWT token
|
||||||
|
router.post('/login', async (req, res) => {
|
||||||
|
const { username, password } = req.body
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = await pool.connect()
|
||||||
|
const queryText = 'SELECT * FROM users WHERE username = $1'
|
||||||
|
const result = await client.query(queryText, [username])
|
||||||
|
|
||||||
|
if (!result.rows.length) {
|
||||||
|
return res.status(401).json({ message: 'Invalid credentials' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = result.rows[0]
|
||||||
|
|
||||||
|
// Compare passwords
|
||||||
|
const passwordMatch = await bcrypt.compare(password, user.password)
|
||||||
|
|
||||||
|
if (!passwordMatch) {
|
||||||
|
return res.status(401).json({ message: 'Invalid credentials' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue JWT token
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ id: user.id, username: user.username },
|
||||||
|
jwtEnv.secret,
|
||||||
|
{ expiresIn: '1h' }
|
||||||
|
)
|
||||||
|
|
||||||
|
res.json({ token })
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.status(500).json({ message: 'Internal server error' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Route to refresh JWT token
|
||||||
|
router.post('/refresh-token', async (req, res) => {
|
||||||
|
const { refreshToken } = req.body
|
||||||
|
|
||||||
|
if (!refreshToken) {
|
||||||
|
return res.status(401).json({ message: 'Refresh token is required' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verify the refresh token
|
||||||
|
const decoded = jwt.verify(refreshToken, jwtEnv.secret)
|
||||||
|
|
||||||
|
// Create a new access token (JWT)
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ id: decoded.id, username: decoded.username },
|
||||||
|
jwtEnv.secret,
|
||||||
|
{ expiresIn: '1h' }
|
||||||
|
)
|
||||||
|
|
||||||
|
res.json({ token })
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.status(403).json({ message: 'Invalid refresh token' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { pool } from '../../config/index.js'
|
||||||
|
import { check, validationResult } from 'express-validator'
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
// Get all collections
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query('SELECT * FROM collections')
|
||||||
|
res.json(result.rows)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.status(500).send('Server error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get a single collection by id
|
||||||
|
router.get('/:id', async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await pool.query('SELECT * FROM collections WHERE id = $1', [
|
||||||
|
id,
|
||||||
|
])
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).send('Collection not found')
|
||||||
|
}
|
||||||
|
res.json(result.rows[0])
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.status(500).send('Server error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a new collection
|
||||||
|
router.post(
|
||||||
|
'/',
|
||||||
|
[
|
||||||
|
check('name', 'Name is required').not().isEmpty(),
|
||||||
|
check('image_id', 'Image id is required').not().isEmpty(),
|
||||||
|
],
|
||||||
|
async (req, res) => {
|
||||||
|
const errors = validationResult(req)
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, image_id } = req.body
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
'INSERT INTO collections (name, image_id, created_at, updated_at) VALUES ($1, $2, NOW(), NOW()) RETURNING *',
|
||||||
|
[name, image_id]
|
||||||
|
)
|
||||||
|
res.status(201).json(result.rows[0])
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.status(500).send('Server error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update an existing collection
|
||||||
|
router.put(
|
||||||
|
'/:id',
|
||||||
|
[
|
||||||
|
check('name', 'Name is required').not().isEmpty(),
|
||||||
|
check('image_id', 'Image id is required').not().isEmpty(),
|
||||||
|
],
|
||||||
|
async (req, res) => {
|
||||||
|
const errors = validationResult(req)
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, image_id } = req.body
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pool.query(
|
||||||
|
'UPDATE collections SET name = $1, image_id = $2, updated_at = NOW() WHERE id = $3',
|
||||||
|
[name, image_id, id]
|
||||||
|
)
|
||||||
|
res.send('Collection updated')
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.status(500).send('Server error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Delete an existing collection
|
||||||
|
router.delete('/:id', async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pool.query('DELETE FROM collections WHERE id = $1', [id])
|
||||||
|
res.send('Collection deleted')
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.status(500).send('Server error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import express from 'express'
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
router.get('/', (req, res) => res.json({ status: 'UP' }))
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// src/routes/healthcheck/test.js
|
||||||
|
|
||||||
|
import request from 'supertest'
|
||||||
|
import app from '../../app'
|
||||||
|
|
||||||
|
describe('Health Check Endpoint', () => {
|
||||||
|
it('should respond with status up', async () => {
|
||||||
|
const response = await request(app).get('/healthcheck')
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.body).toEqual({ status: 'UP' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return a JSON response', async () => {
|
||||||
|
const response = await request(app).get('/healthcheck')
|
||||||
|
|
||||||
|
expect(response.type).toBe('application/json')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { pool } from '../../config/index.js'
|
||||||
|
import { check, validationResult } from 'express-validator'
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
// CREATE AN IMAGE
|
||||||
|
router.post(
|
||||||
|
'/',
|
||||||
|
[check('file').isString(), check('image').isBase64()],
|
||||||
|
async (req, res) => {
|
||||||
|
try {
|
||||||
|
const errors = validationResult(req)
|
||||||
|
if (!errors.isEmpty())
|
||||||
|
return res.status(400).json({ errors: errors.array() })
|
||||||
|
|
||||||
|
const { file, image } = req.body
|
||||||
|
|
||||||
|
const client = await pool.connect()
|
||||||
|
const result = await client.query(
|
||||||
|
'INSERT INTO images (file, image, created_at, updated_at) VALUES ($1, $2, NOW(), NOW()) RETURNING *',
|
||||||
|
[file, image]
|
||||||
|
)
|
||||||
|
client.release()
|
||||||
|
|
||||||
|
return res.status(201).json(result.rows[0])
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.stack)
|
||||||
|
return res.status(500).json({ error: 'Server error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// READ AN IMAGE
|
||||||
|
router.get('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
const client = await pool.connect()
|
||||||
|
const result = await client.query('SELECT * FROM images WHERE id = $1', [
|
||||||
|
id,
|
||||||
|
])
|
||||||
|
client.release()
|
||||||
|
|
||||||
|
if (result.rows.length === 0)
|
||||||
|
return res.status(404).json({ error: 'Image not found' })
|
||||||
|
|
||||||
|
return res.status(200).json(result.rows[0])
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.stack)
|
||||||
|
return res.status(500).json({ error: 'Server error' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// UPDATE AN IMAGE
|
||||||
|
router.put(
|
||||||
|
'/:id',
|
||||||
|
[check('file').optional().isString(), check('image').optional().isBase64()],
|
||||||
|
async (req, res) => {
|
||||||
|
try {
|
||||||
|
const errors = validationResult(req)
|
||||||
|
if (!errors.isEmpty())
|
||||||
|
return res.status(400).json({ errors: errors.array() })
|
||||||
|
|
||||||
|
const { id } = req.params
|
||||||
|
const { file, image } = req.body
|
||||||
|
|
||||||
|
const client = await pool.connect()
|
||||||
|
let query = 'UPDATE images SET'
|
||||||
|
const values = []
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
query += ' file = $1,'
|
||||||
|
values.push(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (image) {
|
||||||
|
query += ' image = $2,'
|
||||||
|
values.push(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing comma and space from query string
|
||||||
|
query = query.slice(0, -1)
|
||||||
|
|
||||||
|
query += ', updated_at = NOW() WHERE id = $3 RETURNING *'
|
||||||
|
values.push(id)
|
||||||
|
|
||||||
|
const result = await client.query(query, values)
|
||||||
|
client.release()
|
||||||
|
|
||||||
|
if (result.rows.length === 0)
|
||||||
|
return res.status(404).json({ error: 'Image not found' })
|
||||||
|
|
||||||
|
return res.status(200).json(result.rows[0])
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.stack)
|
||||||
|
return res.status(500).json({ error: 'Server error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// DELETE AN IMAGE
|
||||||
|
router.delete('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
const client = await pool.connect()
|
||||||
|
const result = await client.query(
|
||||||
|
'DELETE FROM images WHERE id = $1 RETURNING *',
|
||||||
|
[id]
|
||||||
|
)
|
||||||
|
client.release()
|
||||||
|
|
||||||
|
if (result.rows.length === 0)
|
||||||
|
return res.status(404).json({ error: 'Image not found' })
|
||||||
|
|
||||||
|
return res.status(200).json(result.rows[0])
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.stack)
|
||||||
|
return res.status(500).json({ error: 'Server error' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { pool } from '../../config/index.js'
|
||||||
|
import { check, validationResult } from 'express-validator'
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
// Get all items
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
const client = await pool.connect()
|
||||||
|
try {
|
||||||
|
const result = await client.query('SELECT * FROM items')
|
||||||
|
res.json(result.rows)
|
||||||
|
} finally {
|
||||||
|
client.release()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get a single item by id
|
||||||
|
router.get('/:id', async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
const client = await pool.connect()
|
||||||
|
try {
|
||||||
|
const result = await client.query('SELECT * FROM items WHERE id = $1', [id])
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ message: 'Item not found' })
|
||||||
|
}
|
||||||
|
res.json(result.rows[0])
|
||||||
|
} finally {
|
||||||
|
client.release()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a new item
|
||||||
|
router.post(
|
||||||
|
'/',
|
||||||
|
[
|
||||||
|
check('collection_id').not().isEmpty(),
|
||||||
|
check('image_id').not().isEmpty(),
|
||||||
|
check('productId').not().isEmpty(),
|
||||||
|
check('name').not().isEmpty(),
|
||||||
|
check('cleanName').not().isEmpty(),
|
||||||
|
check('extCardText').not().isEmpty(),
|
||||||
|
check('marketPrice').not().isEmpty(),
|
||||||
|
check('extRarity').not().isEmpty(),
|
||||||
|
check('set_id').not().isEmpty(), // Add validation for set_id
|
||||||
|
],
|
||||||
|
async (req, res) => {
|
||||||
|
const errors = validationResult(req)
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() })
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
collection_id,
|
||||||
|
image_id,
|
||||||
|
productId,
|
||||||
|
name,
|
||||||
|
cleanName,
|
||||||
|
extCardText,
|
||||||
|
marketPrice,
|
||||||
|
extRarity,
|
||||||
|
set_id,
|
||||||
|
} = req.body
|
||||||
|
const client = await pool.connect()
|
||||||
|
try {
|
||||||
|
const result = await client.query(
|
||||||
|
'INSERT INTO items (collection_id, image_id, productId, name, cleanName, extCardText, marketPrice, extRarity, set_id, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW()) RETURNING *',
|
||||||
|
[
|
||||||
|
collection_id,
|
||||||
|
image_id,
|
||||||
|
productId,
|
||||||
|
name,
|
||||||
|
cleanName,
|
||||||
|
extCardText,
|
||||||
|
marketPrice,
|
||||||
|
extRarity,
|
||||||
|
set_id,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
res.status(201).json(result.rows[0])
|
||||||
|
} finally {
|
||||||
|
client.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update an existing item
|
||||||
|
router.put('/:id', async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
const updates = {}
|
||||||
|
if (req.body.collection_id) updates.collection_id = req.body.collection_id
|
||||||
|
if (req.body.image_id) updates.image_id = req.body.image_id
|
||||||
|
if (req.body.productId) updates.productId = req.body.productId
|
||||||
|
if (req.body.name) updates.name = req.body.name
|
||||||
|
if (req.body.cleanName) updates.cleanName = req.body.cleanName
|
||||||
|
if (req.body.extCardText) updates.extCardText = req.body.extCardText
|
||||||
|
if (req.body.marketPrice) updates.marketPrice = req.body.marketPrice
|
||||||
|
if (req.body.extRarity) updates.extRarity = req.body.extRarity
|
||||||
|
const client = await pool.connect()
|
||||||
|
try {
|
||||||
|
if (Object.keys(updates).length > 0) {
|
||||||
|
const updateFields = Object.keys(updates)
|
||||||
|
.map((key) => `${key} = $${Object.keys(updates).indexOf(key) + 2}`)
|
||||||
|
.join(', ')
|
||||||
|
updates.updated_at = 'NOW()'
|
||||||
|
const result = await client.query(
|
||||||
|
`UPDATE items SET ${updateFields}, updated_at = $${Object.keys(updates).length + 1} WHERE id = $1 RETURNING *`,
|
||||||
|
[id, ...Object.values(updates)]
|
||||||
|
)
|
||||||
|
return res.json(result.rows[0])
|
||||||
|
}
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ message: 'Item not found' })
|
||||||
|
} else {
|
||||||
|
return res.status(204).json()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
client.release()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Delete an item
|
||||||
|
router.delete('/:id', async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
const client = await pool.connect()
|
||||||
|
try {
|
||||||
|
const result = await client.query('DELETE FROM items WHERE id = $1', [id])
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ message: 'Item not found' })
|
||||||
|
}
|
||||||
|
res.json({ message: 'Item deleted' })
|
||||||
|
} finally {
|
||||||
|
client.release()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a new item from CSV
|
||||||
|
router.post(
|
||||||
|
'/csv',
|
||||||
|
[
|
||||||
|
check('collection_id').not().isEmpty(),
|
||||||
|
check('set_id').not().isEmpty(), // Add validation for set_id
|
||||||
|
],
|
||||||
|
async (req, res) => {
|
||||||
|
const errors = validationResult(req)
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() })
|
||||||
|
}
|
||||||
|
const { collection_id, set_id } = req.body
|
||||||
|
const csvBuffer = req.files.file.buffer.toString('utf8')
|
||||||
|
const csvLines = csvBuffer
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => line.trim().length > 0)
|
||||||
|
const client = await pool.connect()
|
||||||
|
try {
|
||||||
|
// Insert each row into the items table
|
||||||
|
for (const line of csvLines) {
|
||||||
|
const [
|
||||||
|
productId,
|
||||||
|
name,
|
||||||
|
cleanName,
|
||||||
|
imageUrl,
|
||||||
|
categoryId,
|
||||||
|
groupId,
|
||||||
|
url,
|
||||||
|
modifiedOn,
|
||||||
|
imageCount,
|
||||||
|
extCardText,
|
||||||
|
extUPC,
|
||||||
|
lowPrice,
|
||||||
|
midPrice,
|
||||||
|
highPrice,
|
||||||
|
marketPrice,
|
||||||
|
directLowPrice,
|
||||||
|
subTypeName,
|
||||||
|
extNumber,
|
||||||
|
extRarity,
|
||||||
|
extCardType,
|
||||||
|
extHP,
|
||||||
|
extStage,
|
||||||
|
extAttack1,
|
||||||
|
extWeakness,
|
||||||
|
extRetreatCost,
|
||||||
|
extAttack2,
|
||||||
|
extResistance,
|
||||||
|
] = line.split(',').map((value) => value.trim())
|
||||||
|
// Fetch the image from imageUrl
|
||||||
|
const imageResponse = await fetch(imageUrl)
|
||||||
|
const imageBuffer = await imageResponse.buffer()
|
||||||
|
|
||||||
|
// Insert the image into the images table
|
||||||
|
const insertImageResult = await client.query(
|
||||||
|
'INSERT INTO images (file, image) VALUES ($1::TEXT, $2::BYTEA) RETURNING id',
|
||||||
|
[`${productId}.jpg`, imageBuffer]
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
'INSERT INTO items (collection_id, image_id, productId, name, cleanName, extCardText, marketPrice, extRarity, set_id, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())',
|
||||||
|
[
|
||||||
|
collection_id,
|
||||||
|
insertImageResult.rows[0].id,
|
||||||
|
productId,
|
||||||
|
name,
|
||||||
|
cleanName,
|
||||||
|
extCardText,
|
||||||
|
marketPrice,
|
||||||
|
extRarity,
|
||||||
|
set_id,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
res.status(201).json({ message: 'Items created' })
|
||||||
|
} finally {
|
||||||
|
client.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { pool } from '../../config/index.js'
|
||||||
|
import { check, validationResult } from 'express-validator'
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
// Create a new location
|
||||||
|
router.post(
|
||||||
|
'/',
|
||||||
|
[check('name', 'Name is required').not().isEmpty()],
|
||||||
|
async (req, res) => {
|
||||||
|
const errors = validationResult(req)
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() })
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { name } = req.body
|
||||||
|
await pool.query(
|
||||||
|
'INSERT INTO locations (name, created_at, updated_at) VALUES ($1, NOW(), NOW())',
|
||||||
|
[name]
|
||||||
|
)
|
||||||
|
res.send({ message: 'Location created' })
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message)
|
||||||
|
res.status(500).send('Server error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get all locations
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query('SELECT * FROM locations')
|
||||||
|
res.send(result.rows)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message)
|
||||||
|
res.status(500).send('Server error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get a single location by ID
|
||||||
|
router.get('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query('SELECT * FROM locations WHERE id = $1', [
|
||||||
|
req.params.id,
|
||||||
|
])
|
||||||
|
if (result.rows.length === 0)
|
||||||
|
return res.status(404).send('Location not found')
|
||||||
|
res.send(result.rows[0])
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message)
|
||||||
|
res.status(500).send('Server error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update a location
|
||||||
|
router.put(
|
||||||
|
'/:id',
|
||||||
|
[check('name', 'Name is required').not().isEmpty()],
|
||||||
|
async (req, res) => {
|
||||||
|
const errors = validationResult(req)
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() })
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { name } = req.body
|
||||||
|
await pool.query(
|
||||||
|
'UPDATE locations SET name = $1, updated_at = NOW() WHERE id = $2',
|
||||||
|
[name, req.params.id]
|
||||||
|
)
|
||||||
|
res.send({ message: 'Location updated' })
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message)
|
||||||
|
res.status(500).send('Server error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Delete a location
|
||||||
|
router.delete('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await pool.query('DELETE FROM locations WHERE id = $1', [req.params.id])
|
||||||
|
res.send({ message: 'Location deleted' })
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message)
|
||||||
|
res.status(500).send('Server error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { pool } from '../../config/index.js'
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
// Create a new role
|
||||||
|
router.post('/', async (req, res) => {
|
||||||
|
const { name } = req.body
|
||||||
|
const client = await pool.connect()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN')
|
||||||
|
const result = await client.query(
|
||||||
|
'INSERT INTO roles (name, created_at, updated_at) VALUES ($1, NOW(), NOW()) RETURNING *',
|
||||||
|
[name]
|
||||||
|
)
|
||||||
|
await client.query('COMMIT')
|
||||||
|
res.status(201).json(result.rows[0])
|
||||||
|
} catch (err) {
|
||||||
|
await client.query('ROLLBACK')
|
||||||
|
res.status(500).json({ error: err.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get all roles
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query('SELECT * FROM roles')
|
||||||
|
res.json(result.rows)
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get a single role by id
|
||||||
|
router.get('/:id', async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
try {
|
||||||
|
const result = await pool.query('SELECT * FROM roles WHERE id = $1', [id])
|
||||||
|
if (result.rows.length === 0)
|
||||||
|
return res.status(404).json({ error: 'Role not found' })
|
||||||
|
res.json(result.rows[0])
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update a role by id
|
||||||
|
router.put('/:id', async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
const { name } = req.body
|
||||||
|
const client = await pool.connect()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN')
|
||||||
|
const result = await client.query(
|
||||||
|
'UPDATE roles SET name = $1, updated_at = NOW() WHERE id = $2 RETURNING *',
|
||||||
|
[name, id]
|
||||||
|
)
|
||||||
|
if (result.rows.length === 0)
|
||||||
|
return res.status(404).json({ error: 'Role not found' })
|
||||||
|
await client.query('COMMIT')
|
||||||
|
res.json(result.rows[0])
|
||||||
|
} catch (err) {
|
||||||
|
await client.query('ROLLBACK')
|
||||||
|
res.status(500).json({ error: err.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Delete a role by id
|
||||||
|
router.delete('/:id', async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
const client = await pool.connect()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN')
|
||||||
|
const result = await client.query('DELETE FROM roles WHERE id = $1', [id])
|
||||||
|
if (result.rows.length === 0)
|
||||||
|
return res.status(404).json({ error: 'Role not found' })
|
||||||
|
await client.query('COMMIT')
|
||||||
|
res.json(result.rows[0])
|
||||||
|
} catch (err) {
|
||||||
|
await client.query('ROLLBACK')
|
||||||
|
res.status(500).json({ error: err.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { pool } from '../../config/index.js'
|
||||||
|
import { check, validationResult } from 'express-validator'
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
// Get all sets
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query('SELECT * FROM sets')
|
||||||
|
res.json(result.rows)
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a new set
|
||||||
|
router.post(
|
||||||
|
'/',
|
||||||
|
[
|
||||||
|
check('collection_id').not().isEmpty(),
|
||||||
|
check('name').not().isEmpty(),
|
||||||
|
check('location_id').not().isEmpty(),
|
||||||
|
check('image_id').not().isEmpty(),
|
||||||
|
],
|
||||||
|
async (req, res) => {
|
||||||
|
const errors = validationResult(req)
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
'INSERT INTO sets (collection_id, name, location_id, image_id, created_at, updated_at) VALUES ($1, $2, $3, $4, NOW(), NOW()) RETURNING *',
|
||||||
|
[
|
||||||
|
req.body.collection_id,
|
||||||
|
req.body.name,
|
||||||
|
req.body.location_id,
|
||||||
|
req.body.image_id,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
res.status(201).json(result.rows[0])
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: error.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get a single set by ID
|
||||||
|
router.get('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query('SELECT * FROM sets WHERE id = $1', [
|
||||||
|
req.params.id,
|
||||||
|
])
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ message: 'Set not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(result.rows[0])
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update a set by ID
|
||||||
|
router.put('/:id', [check('name').not().isEmpty()], async (req, res) => {
|
||||||
|
const errors = validationResult(req)
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
'UPDATE sets SET name = $1, updated_at = NOW() WHERE id = $2 RETURNING *',
|
||||||
|
[req.body.name, req.params.id]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ message: 'Set not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(result.rows[0])
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Delete a set by ID
|
||||||
|
router.delete('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
'DELETE FROM sets WHERE id = $1 RETURNING *',
|
||||||
|
[req.params.id]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ message: 'Set not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(result.rows[0])
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import bcrypt from 'bcryptjs'
|
||||||
|
import { pool } from '../../config/index.js'
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all users.
|
||||||
|
*/
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
const { rows } = await pool.query('SELECT * FROM users')
|
||||||
|
res.json(rows)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new user.
|
||||||
|
*
|
||||||
|
* @param {string} name - The user's name
|
||||||
|
* @param {string} email - The user's email address
|
||||||
|
* @param {string} password - The user's password (must be hashed)
|
||||||
|
* @param {string} role_name - The name of the role to assign to the user
|
||||||
|
*/
|
||||||
|
router.post('/', async (req, res) => {
|
||||||
|
const { username, email, password, role_name } = req.body
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
return res.status(400).send('Password is required')
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Hash the password using bcrypt
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 10)
|
||||||
|
// Query for the UUID of the role by name
|
||||||
|
const roleResult = await pool.query(
|
||||||
|
'SELECT id FROM roles WHERE name = $1',
|
||||||
|
[role_name]
|
||||||
|
)
|
||||||
|
if (roleResult.rows.length === 0) {
|
||||||
|
throw new Error(`Role ${role_name} not found`)
|
||||||
|
}
|
||||||
|
const roleId = roleResult.rows[0].id
|
||||||
|
|
||||||
|
// Insert the new user into the database
|
||||||
|
const result = await pool.query(
|
||||||
|
'INSERT INTO users (username, email, password, role_id, created_at, updated_at) VALUES ($1, $2, $3, $4, NOW(), NOW()) RETURNING *',
|
||||||
|
[username, email, hashedPassword, roleId]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Return the newly created user
|
||||||
|
res.status(201).json(result.rows[0])
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).send(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing user.
|
||||||
|
*
|
||||||
|
* @param {number} id - The ID of the user to update
|
||||||
|
* @param {string} name - The new name for the user
|
||||||
|
* @param {string} email - The new email address for the user (optional)
|
||||||
|
* @param {string} password - The new password for the user (optional, must be hashed)
|
||||||
|
* @param {string} role_name - The name of the new role to assign to the user (optional)
|
||||||
|
*/
|
||||||
|
router.put('/:id', async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
const { name, email, password, role_name } = req.body
|
||||||
|
|
||||||
|
const client = await pool.connect()
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN')
|
||||||
|
|
||||||
|
// Query for the UUID of the role by name if provided
|
||||||
|
let roleId = null
|
||||||
|
if (role_name) {
|
||||||
|
const roleResult = await client.query(
|
||||||
|
'SELECT id FROM roles WHERE name = $1',
|
||||||
|
[role_name]
|
||||||
|
)
|
||||||
|
if (roleResult.rows.length === 0) {
|
||||||
|
throw new Error(`Role ${role_name} not found`)
|
||||||
|
}
|
||||||
|
roleId = roleResult.rows[0].id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the user in the database
|
||||||
|
let queryText = 'UPDATE users SET name = $1'
|
||||||
|
const values = [name]
|
||||||
|
|
||||||
|
if (email) {
|
||||||
|
queryText += ', email = $2'
|
||||||
|
values.push(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password) {
|
||||||
|
queryText += ', password = $3'
|
||||||
|
values.push(await bcrypt.hash(password, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roleId !== null) {
|
||||||
|
queryText += ', role_id = $4'
|
||||||
|
values.push(roleId)
|
||||||
|
}
|
||||||
|
|
||||||
|
queryText += ' , updated_at = NOW() WHERE id = $5 RETURNING *'
|
||||||
|
values.push(id)
|
||||||
|
|
||||||
|
const result = await client.query(queryText, values)
|
||||||
|
|
||||||
|
// Return the updated user
|
||||||
|
res.json(result.rows[0])
|
||||||
|
|
||||||
|
await client.query('COMMIT')
|
||||||
|
} catch (err) {
|
||||||
|
await client.query('ROLLBACK')
|
||||||
|
res.status(500).send(err)
|
||||||
|
} finally {
|
||||||
|
client.release()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an existing user.
|
||||||
|
*
|
||||||
|
* @param {number} id - The ID of the user to delete
|
||||||
|
*/
|
||||||
|
router.delete('/:id', async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
const client = await pool.connect()
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN')
|
||||||
|
|
||||||
|
// Delete the user from the database
|
||||||
|
const result = await client.query(
|
||||||
|
'DELETE FROM users WHERE id = $1 RETURNING *',
|
||||||
|
[id]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Return the deleted user
|
||||||
|
res.json(result.rows[0])
|
||||||
|
|
||||||
|
await client.query('COMMIT')
|
||||||
|
} catch (err) {
|
||||||
|
await client.query('ROLLBACK')
|
||||||
|
res.status(500).send(err)
|
||||||
|
} finally {
|
||||||
|
client.release()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
Reference in New Issue
Block a user