From: charleswrayjr Date: Fri, 26 Sep 2025 22:13:22 +0000 (-0500) Subject: Adding Socket.io for messaging and getting things ready for a more robust UI. X-Git-Url: https://git.phasecustomsoft.com/static/gitweb.js?a=commitdiff_plain;h=12bac71c657d227f9c9fd3d0341d37a453449e56;p=phs-api.git Adding Socket.io for messaging and getting things ready for a more robust UI. --- diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml old mode 100644 new mode 100755 diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml old mode 100644 new mode 100755 index 975388a..cf76012 --- a/.idea/dictionaries/project.xml +++ b/.idea/dictionaries/project.xml @@ -1,7 +1,9 @@ + appspecific certfile + dport easyrsa genkey ifconfig @@ -10,6 +12,7 @@ nopass ovpn passout + pgmigrations phasecustomsoft phsdb pkitool diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml old mode 100644 new mode 100755 diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml old mode 100644 new mode 100755 diff --git a/.idea/zencoder-chat-index.xml b/.idea/zencoder-chat-index.xml old mode 100644 new mode 100755 diff --git a/app.js b/app.js index fa39c5a..4346b14 100755 --- a/app.js +++ b/app.js @@ -1,36 +1,59 @@ -const createError = require('http-errors'); -const express = require('express'); +const createError = require( 'http-errors' ); +const express = require( 'express' ); const compression = require( 'express-compression' ); const bodyParser = require( 'body-parser' ); -const cookieParser = require('cookie-parser'); +const cookieParser = require( 'cookie-parser' ); const log4js = require( './src/middleware/loggerMiddleWare' ); const passport = require( 'passport' ); let hookJWTStrategy = require( './src/middleware/passport' ); const cors = require( 'cors' ); +const socketIo = require( 'socket.io' ); const port = normalizePort( process.env.PORT || '3601' ); -const runOnce = require ( './src/middleware/custom-startup' ); +const runOnce = require( './src/middleware/custom-startup' ); const Routes = require( './src/routes' ); const phsdb = require( './src/phsdb' ); -const fs = require('fs'); -const path = require('path'); -const Docker = require('dockerode'); -const docker = new Docker({ socketPath: '/var/run/docker.sock' }); +const fs = require( 'fs' ); +const path = require( 'path' ); +const Docker = require( 'dockerode' ); +const db = require( './src/models' ); +const docker = new Docker( { socketPath:'/var/run/docker.sock' } ); const sshConfig = { host:'192.168.1.62', port:22, username:'charles', privateKey:fs.readFileSync( '/home/node/.ssh/git-ui' ), }; -const buffer = require('buffer'); +const buffer = require( 'buffer' ); global.Blob = buffer.Blob; const app = express(); -const http = require( 'http' ).createServer( app ); +const server = require( 'http' ).createServer( app ); +// noinspection JSValidateTypes +const io = socketIo( server, { + cors:{ + origin:[ + 'http://localhost:30007', + 'http://phs-home:3000', + 'http://localhost:23601', + 'http://phs-api:3601', + 'http://localhost:30008', + 'http://phs-admin:3000', + 'http://localhost:8101', + 'https://admin.phasecustomsoft.com', + 'https://app.phasecustomsoft.com', + 'https://api.phasecustomsoft.com', + 'https://www.phasecustomsoft.com' + ], + credentials:true, + methods:['GET', 'POST', 'PUT', 'DELETE'], + allowedHeaders:['Content-Type', 'Authorization', 'Origin', 'X-Requested-With', 'Accept', 'Access-Control-Allow-Origin', 'Cookie', 'phs-app-version'] + } +} ); -app.set('etag', false); +app.set( 'etag', false ); -app.use((req, res, next) => { - res.set('Cache-Control', 'no-store'); +app.use( ( req, res, next ) => { + res.set( 'Cache-Control', 'no-store' ); next(); -}); +} ); global.phsdb = phsdb; global.fs = fs; @@ -45,9 +68,9 @@ app.use( bodyParser.json( { } ) ); app.use( bodyParser.urlencoded( { extended:true, - limit: '50mb' + limit:'50mb' } ) ); -app.use(cookieParser()); +app.use( cookieParser() ); //Setting up log4js const logger = log4js.default; @@ -70,11 +93,11 @@ global.logger = logger; // For automatic logging of all requests app.use( log4js.express ); -logger.info('PHS_ENV: ' + process.env.PHS_ENV ); -logger.info('NODE_ENV: ' + process.env.NODE_ENV ); -logger.info('Testing log levels: info, debug and error. Only shows for PHS_ENV=DEV' ); -logger.info('Test log level: info' ); // Should show in all NODE_ENV settings -logger.debug('Test log level: debug' ); // Should not show in PRODUCTION +logger.info( 'PHS_ENV: ' + process.env.PHS_ENV ); +logger.info( 'NODE_ENV: ' + process.env.NODE_ENV ); +logger.info( 'Testing log levels: info, debug and error. Only shows for PHS_ENV=DEV' ); +logger.info( 'Test log level: info' ); // Should show in all NODE_ENV settings +logger.debug( 'Test log level: debug' ); // Should not show in PRODUCTION //Initialize passport app.use( passport.initialize() ); @@ -87,48 +110,29 @@ runOnce().catch( err => logger.error( err ) ); app.use( compression() ); app.use( express.json() ); app.use( express.urlencoded( { extended:false } ) ); -app.use(cookieParser()); +app.use( cookieParser() ); // 2FA Cors: -app.use(cors({ - exposedHeaders: ['LastFetchDateTime', 'Authentication'], - origin: [ - // Home - 'http://localhost:30008', // local dev - 'http://localhost:23601', - 'http://phs-home:3000', // import / export jobs - 'https://phs-home.phasecustomsoft.com', - 'https://phs-admin.phasecustomsoft.com', - 'https://www.phasecustomsoft.com', - // Customer (local dev) - 'http://localhost:30006', // local dev - 'http://rt2-customer:3000', // import / export jobs - 'https://customer-f.phasecustomsoft.com', - 'https://customer-t.phasecustomsoft.com', - 'https://customer-demo.phasecustomsoft.com', - 'https://customer.phasecustomsoft.com', - // Admin (local dev) - 'http://localhost:8100', +app.use( cors( { + exposedHeaders:['LastFetchDateTime', 'Authentication'], + origin:[ 'http://localhost:30007', - 'https://admin-t.phasecustomsoft.com', - 'https://admin-f.phasecustomsoft.com', - 'https://admin-demo.phasecustomsoft.com', - 'https://admin.phasecustomsoft.com', - // App (local dev) + 'http://phs-home:3000', + 'http://localhost:23601', + 'http://phs-api:3601', + 'http://localhost:30008', + 'http://phs-admin:3000', 'http://localhost:8101', - 'https://app-t.phasecustomsoft.com', - 'https://app-f.phasecustomsoft.com', - 'https://app-demo.phasecustomsoft.com', + 'https://admin.phasecustomsoft.com', 'https://app.phasecustomsoft.com', - // Public website - 'https://www-t.phasecustomsoft.com', + 'https://api.phasecustomsoft.com', 'https://www.phasecustomsoft.com' ], - credentials: true, - methods: ['GET', 'POST', 'PUT', 'DELETE'], - allowedHeaders: ['Content-Type', 'Authorization', 'Origin', 'X-Requested-With', 'Accept', 'Access-Control-Allow-Origin', 'Cookie', 'phs-app-version'], - optionsSuccessResponds: true, -})); + credentials:true, + methods:['GET', 'POST', 'PUT', 'DELETE'], + allowedHeaders:['Content-Type', 'Authorization', 'Origin', 'X-Requested-With', 'Accept', 'Access-Control-Allow-Origin', 'Cookie', 'phs-app-version'], + optionsSuccessResponds:true +} ) ); //Send the current server date/time for each request app.use( function ( req, res, next ) { @@ -136,29 +140,78 @@ app.use( function ( req, res, next ) { next(); } ); +// Socket.io for real-time chat and reactions +io.on( 'connection', ( socket ) => { + const userId = socket.handshake.query.userId; + logger.info( `Socket.io connection established with userId: ${ userId || 'undefined' }` ); + if (userId) { + socket.join( userId ); + } else { + logger.warn( 'Socket.io connection with missing userId' ); + } + socket.on( 'chatMessage', async ( { content, recipientId, groupId, fileIds } ) => { + try { + const message_data = { + sender_id:userId, + group_id:groupId ? parseInt( groupId ) : null, + recipient_id:recipientId ? parseInt( recipientId ) : null, + content, + file_ids:fileIds || [] + }; + const message = await db.message.create( message_data ); + if (groupId) { + const members = await phsdb.query( 'SELECT user_id FROM phase.message_group_members WHERE group_id = $1', [groupId] ); + members.forEach( m => io.to( m.user_id.toString() ).emit( 'newMessage', message ) ); + } else if (recipientId) { + io.to( recipientId.toString() ).emit( 'newMessage', message ); + if (userId) io.to( userId.toString() ).emit( 'newMessage', message ); + } + } catch (err) { + logger.error( `Socket chatMessage error: ${ err.message }` ); + socket.emit( 'error', { error:err.message } ); + } + } ); + socket.on( 'messageReaction', async ( { messageId, reaction } ) => { + try { + const reaction_data = await db.message_reactions.add( messageId, userId, reaction ); + const message = await db.message.find_one( { id:parseInt( messageId ) } ); + if (message.group_id) { + const members = await phsdb.query( 'SELECT user_id FROM phase.message_group_members WHERE group_id = $1', [message.group_id] ); + members.forEach( m => io.to( m.user_id.toString() ).emit( 'newReaction', { messageId, reaction_data } ) ); + } else if (message.recipient_id) { + io.to( message.recipient_id.toString() ).emit( 'newReaction', { messageId, reaction_data } ); + if (userId) io.to( userId.toString() ).emit( 'newReaction', { messageId, reaction_data } ); + } + } catch (err) { + logger.error( `Socket messageReaction error: ${ err.message }` ); + socket.emit( 'error', { error:err.message } ); + } + } ); +} ); + app.use( '/', Routes.APIRoutes( passport ) ); app.set( 'port', port ); //Create HTTP Server for App // noinspection JSCheckFunctionSignatures -http.listen( port ); -http.on( 'error', onError ); -http.on( 'listening', onListening ); +server.listen( port ); +server.on( 'error', onError ); +server.on( 'listening', onListening ); -process.on('uncaughtException', function(err) { +process.on( 'uncaughtException', function ( err ) { logger.error( '*************uncaughtException******' ); logger.error( err ); -}); +} ); -process.on('unhandledRejection', (err) => { +process.on( 'unhandledRejection', ( err ) => { logger.error( '*************unhandledRejection******' ); - logger.error(err.name, err.message); -}); + logger.error( err.name, err.message ); +} ); // catch 404 and forward to the error handler app.use( function ( req, res, next ) { - if (req.url === '/.well-known/appspecific/com.chrome.devtools.json' ) next(); + if (req.url === '/.well-known/appspecific/com.chrome.devtools.json') next(); else { next( createError( 404 ) ); } @@ -212,7 +265,7 @@ function onError( error ) { */ function onListening() { - const addr = http.address(); + const addr = server.address(); const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; logger.info( 'Listening on ' + bind ); } @@ -227,7 +280,7 @@ const errorHandler = ( err, req, res, next ) => { res.locals.error = req.app.get( 'env' ) === 'DEVELOPMENT' ? err : {}; // render the error page res.status( err.status || 500 ); - res.send(err); + res.send( err ); } else next(); }; diff --git a/package-lock.json b/package-lock.json index a0b3a3b..c7ddfda 100755 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "pg": "^8.16.3", "pg-promise": "^11.15.0", "pug": "3.0.3", + "socket.io": "^4.8.1", "ssh2": "^1.17.0", "swagger-ui-express": "^5.0.1" }, @@ -1194,6 +1195,11 @@ "@sinonjs/commons": "^3.0.1" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", @@ -1245,6 +1251,14 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -1854,6 +1868,14 @@ } ] }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -2700,6 +2722,93 @@ "once": "^1.4.0" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -6218,6 +6327,146 @@ "node": ">=8" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -7148,6 +7397,26 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index b279d6e..93a7e4c 100755 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "pg": "^8.16.3", "pg-promise": "^11.15.0", "pug": "3.0.3", + "socket.io": "^4.8.1", "ssh2": "^1.17.0", "swagger-ui-express": "^5.0.1" }, diff --git a/run-api.sh b/run-api.sh index 641e66c..a969a79 100755 --- a/run-api.sh +++ b/run-api.sh @@ -10,11 +10,11 @@ if [ "$env" == "TEST" ]; then npm install; npx pm2 start api.config.cjs --no-daemon --env TEST elif [ "$env" == "PRODUCTION" ]; then echo "PHS API Production Stack" -# npm install; npm run start-debug - npm install; - npx babel src --out-dir src --extensions .jsx; - cd /usr/src/app - npx pm2 start api.config.cjs --no-daemon --env PRODUCTION + npm install; npm run start-debug +# npm install; +# npx babel src --out-dir src --extensions .jsx; +# cd /usr/src/app +# npx pm2 start api.config.cjs --no-daemon --env PRODUCTION else echo "PHS API Dev Stack" npm install; npm run start-debug diff --git a/src/config/secret_key_gen.js b/src/config/secret_key_gen.js index 998ed7c..de206ab 100755 --- a/src/config/secret_key_gen.js +++ b/src/config/secret_key_gen.js @@ -2,7 +2,7 @@ const crypto = require('crypto'); const jwt = require('jsonwebtoken'); const config = require( '../config/default.json' ); -// Generate (16 bytes, 128 bits) secret key. Copy output to to the proper key in default.json. +// Generate (16 bytes, 128 bits) secret key. Copy output to the proper key in default.json. const secretKey = crypto.randomBytes(16).toString('hex'); logger.debug('key: ' + secretKey); diff --git a/src/controllers/address.controller.js b/src/controllers/address.controller.js old mode 100644 new mode 100755 diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js old mode 100644 new mode 100755 index 8549cb1..68b39b9 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -4,25 +4,24 @@ const db = require( '../models' ); const createError = require( 'http-errors' ); -const jwt = require('jsonwebtoken'); -const config = require('../config/default.json'); -const phs_env = process.env.PHS_ENV; +const jwt = require( 'jsonwebtoken' ); +const config = require( '../config/default.json' ); const updateUserAndReturn = async ( validUser, res ) => { const token = await validUser.createToken(); - await setCookie('phase_request_token', { user_id: validUser.id, request_token: token }, res); + await setCookie( 'phase_request_token', { user_id:validUser.id, request_token:token }, res ); res.status( 200 ).send( { success:true, user:validUser, token } ); }; -const setCookie = (cookieKey, payload, res) => { - const token = jwt.sign(payload, config.keys.secret2fa, { algorithm: 'HS256', expiresIn: '1y' }); - res.cookie(cookieKey, token, { - maxAge: 31536000000, - httpOnly: true, - secure: (phs_env !== 'DEV'), - sameSite: 'lax', - domain: '.phasecustomsoft.com' - }); // expire in 1 year: 31536000000 +const setCookie = ( cookieKey, payload, res ) => { + const token = jwt.sign( payload, config.keys.secret2fa, { algorithm:'HS256', expiresIn:'1y' } ); + res.cookie( cookieKey, token, { + maxAge:24 * 60 * 60 * 1000, // 1 day + httpOnly:true, + secure:true, + sameSite:'lax', + domain:'.phasecustomsoft.com' + } ); // expire in 1 year: 31536000000 }; module.exports = { @@ -47,4 +46,29 @@ module.exports = { next( e ); } }, + + /** + * Logout a user by clearing the JWT cookie + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async logout( req, res, next ) { + try { + // Clear the phase_request_token cookie + res.clearCookie( 'phase_request_token', { + maxAge:24 * 60 * 60 * 1000, // 1 day + httpOnly:true, + secure:true, + domain:'.phasecustomsoft.com', + sameSite:'lax' + } ); + logger.info( `User logged out: ${ req.user?.id || 'unknown' }` ); + res.json( { message:'Logged out successfully' } ); + } catch (error) { + logger.error( `Logout error: ${ error.message }` ); + next( createError( error.status || 500, error.message ) ); + } + } }; \ No newline at end of file diff --git a/src/controllers/authentication.controller.js b/src/controllers/authentication.controller.js old mode 100644 new mode 100755 diff --git a/src/controllers/docker.controller.js b/src/controllers/docker.controller.js old mode 100644 new mode 100755 diff --git a/src/controllers/file_users.controller.js b/src/controllers/file_users.controller.js new file mode 100755 index 0000000..479b720 --- /dev/null +++ b/src/controllers/file_users.controller.js @@ -0,0 +1,82 @@ +/** + * @file File users controller for handling file-users relation API requests + * @module FileUsersController + */ + +const db = require( '../models' ); +const createError = require( 'http-errors' ); + +/** + * File users controller + * @type {Object} + */ +module.exports = { + /** + * Add a file-user relation + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async add_relation( req, res, next ) { + try { + const { file_id, user_id } = req.body; + const file = await db.file.find_one( { id: parseInt( file_id ) } ); + if ( !file ) return next( createError( 404, 'File not found' ) ); + if ( file.user_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) { + return next( createError( 403, 'Unauthorized to add relation to this file' ) ); + } + const user = await db.user.find_one( { id: parseInt( user_id ) } ); + if ( !user ) return next( createError( 404, 'User not found' ) ); + const relation = await db.file_users.add_relation( file_id, user_id ); + res.json( relation ); + } catch ( error ) { + logger.error( `Add file-user relation error: ${ error.message }` ); + next( createError( error.status || 400, error.message ) ); + } + }, + + /** + * Remove a file-user relation + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async remove_relation( req, res, next ) { + try { + const { file_id, user_id } = req.params; + const file = await db.file.find_one( { id: parseInt( file_id ) } ); + if ( !file ) return next( createError( 404, 'File not found' ) ); + if ( file.user_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) { + return next( createError( 403, 'Unauthorized to remove relation from this file' ) ); + } + const user = await db.user.find_one( { id: parseInt( user_id ) } ); + if ( !user ) return next( createError( 404, 'User not found' ) ); + const relation = await db.file_users.remove_relation( file_id, user_id ); + res.json( relation || { message: 'Relation removed' } ); + } catch ( error ) { + logger.error( `Remove file-user relation error: ${ error.message }` ); + next( createError( error.status || 400, error.message ) ); + } + }, + + /** + * Find file-user relations by file ID + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async find_by_file_id( req, res, next ) { + try { + const { file_id } = req.params; + const { limit = '100', offset = '0' } = req.query; + const relations = await db.file_users.find_by_file_id( parseInt( file_id ), [], null, parseInt( limit ), parseInt( offset ) ); + res.json( relations ); + } catch ( error ) { + logger.error( `Find file-user relations by file ID error: ${ error.message }` ); + next( createError( error.status || 500, error.message ) ); + } + } +}; \ No newline at end of file diff --git a/src/controllers/files.controller.js b/src/controllers/files.controller.js new file mode 100755 index 0000000..d6b5cfa --- /dev/null +++ b/src/controllers/files.controller.js @@ -0,0 +1,191 @@ +/** + * @file Files controller for handling file-related API requests + * @module FilesController + */ + +const db = require( '../models' ); +const createError = require( 'http-errors' ); +const { upload } = require( '../middleware/multer' ); + +/** + * Files controller + * @type {Object} + */ +module.exports = { + /** + * Multer middleware for file uploads + * @type {Function||any} + */ + uploadFile: upload.array( 'files', 10 ), + + /** + * Create a new file + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async create( req, res, next ) { + try { + const { visibility, specific_users } = req.body; + const files = req.files || []; + const results = []; + for ( const file of files ) { + const file_data = { + user_id: req.user.id, + file_path: `/uploads/${ file.filename }`, + file_type: file.mimetype.startsWith( 'image' ) ? 'image' : 'video', + visibility: visibility || 'family', + specific_users: specific_users ? JSON.parse( specific_users ) : [], + created_by_id: req.user.id + }; + const uploaded_file = await db.file.create( file_data ); + results.push( uploaded_file ); + } + res.json( results.length === 1 ? results[ 0 ] : results ); + } catch ( error ) { + logger.error( `Create file error: ${ error.message }` ); + next( createError( error.status || 409, error.message ) ); + } + }, + + /** + * Find file by file path + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async find_by_file_path( req, res, next ) { + try { + const { file_path } = req.params; + const file = await db.file.find_by_file_path( file_path ); + if ( !file ) return next( createError( 404, 'File not found' ) ); + res.json( file ); + } catch ( error ) { + logger.error( `Find file by file path error: ${ error.message }` ); + next( createError( error.status || 500, error.message ) ); + } + }, + + /** + * Find one file by ID + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async find_one( req, res, next ) { + try { + const { id } = req.params; + const file = await db.file.find_one( { id: parseInt( id ) } ); + if ( !file ) return next( createError( 404, 'File not found' ) ); + res.json( file ); + } catch ( error ) { + logger.error( `Find file error: ${ error.message }` ); + next( createError( error.status || 500, error.message ) ); + } + }, + + /** + * Find many files + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async find_many( req, res, next ) { + try { + const { limit = '100', offset = '0', ...where } = req.query; + where.is_deleted = false; + if ( req.user ) { + where[ '$or' ] = [ + { visibility: 'family' }, + { user_id: req.user.id }, + { specific_users: { $contains: [ req.user.id ] } } + ]; + } + const files = await db.file.find_many( where, [], null, parseInt( limit ), parseInt( offset ) ); + res.json( files ); + } catch ( error ) { + logger.error( `Find many files error: ${ error.message }` ); + next( createError( error.status || 500, error.message ) ); + } + }, + + /** + * Update a file + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async update( req, res, next ) { + try { + const { id } = req.params; + const file_data = req.body; + const file = await db.file.instance().find_one( { id: parseInt( id ) } ); + if ( !file ) return next( createError( 404, 'File not found' ) ); + if ( file.user_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) { + return next( createError( 403, 'Unauthorized to update this file' ) ); + } + const updated_file = await file.update( file_data ); + res.json( updated_file ); + } catch ( error ) { + logger.error( `Update file error: ${ error.message }` ); + next( createError( error.status || 400, error.message ) ); + } + }, + + /** + * Soft delete a file + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async soft_delete( req, res, next ) { + try { + const { id } = req.params; + const { deleted_by_id } = req.body; + const file = await db.file.instance().find_one( { id: parseInt( id ) } ); + if ( !file ) return next( createError( 404, 'File not found' ) ); + if ( file.user_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) { + return next( createError( 403, 'Unauthorized to delete this file' ) ); + } + const deleted_file = await file.soft_delete( deleted_by_id || req.user.id ); + res.json( deleted_file ); + } catch ( error ) { + logger.error( `Soft delete file error: ${ error.message }` ); + next( createError( error.status || 400, error.message ) ); + } + }, + + /** + * Serve a file + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async get_file( req, res, next ) { + try { + const { id } = req.params; + const file = await db.file.find_one( { id: parseInt( id ) } ); + if ( !file ) return next( createError( 404, 'File not found' ) ); + if ( file.visibility === 'personal' && file.user_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) { + return next( createError( 403, 'Unauthorized to access this file' ) ); + } + if ( file.visibility === 'specific' ) { + const allowed_users = await db.file_users.find_by_file_id( file.id ); + if ( !allowed_users.some( u => u.user_id === req.user.id ) && !req.user.roles.includes( 'admin' ) ) { + return next( createError( 403, 'Unauthorized to access this file' ) ); + } + } + res.sendFile( file.file_path, { root: '.' } ); + } catch ( error ) { + logger.error( `Get file error: ${ error.message }` ); + next( createError( error.status || 500, error.message ) ); + } + } +}; \ No newline at end of file diff --git a/src/controllers/git.controller.js b/src/controllers/git.controller.js index d6c2619..4a54936 100755 --- a/src/controllers/git.controller.js +++ b/src/controllers/git.controller.js @@ -17,7 +17,8 @@ module.exports = { conn.exec( commands.join( ' && ' ), ( err, stream ) => { if (err) { conn.end(); - return res.status( 500 ).json( { error:'SSH command failed' } ); + const error = new createError( 500, `SSH command failed` ); + next( error ); } let output = ''; stream.on( 'data', ( data ) => (output += data) ); @@ -27,12 +28,14 @@ module.exports = { if (code === 0) { res.json( { message:`Repository ${ name }.git created successfully` } ); } else { - res.status( 500 ).json( { error:`Command failed: ${ output }` } ); + const error = new createError( 500, `Command failed: ${ output }` ); + next( error ); } } ); } ); } ).on( 'error', ( err ) => { - res.status( 500 ).json( { error:`SSH connection failed: ${ err.message }` } ); + const error = new createError( 500, `SSH connection failed: ${ err.message }` ); + next( error ); } ).connect( sshConfig ); }, getRepos:async ( req, res, next ) => { const conn = new Client(); @@ -41,7 +44,8 @@ module.exports = { conn.exec( command, ( err, stream ) => { if (err) { conn.end(); - return res.status( 500 ).json( { error:'SSH command failed' } ); + const error = new createError( 500, `SSH command failed` ); + next( error ); } let output = ''; stream.on( 'data', ( data ) => (output += data) ); @@ -54,17 +58,20 @@ module.exports = { }) ); res.json( { repos } ); } else { - res.status( 500 ).json( { error:`Command failed: ${ output }` } ); + const error = new createError( 500, `Command failed: ${ output }` ); + next( error ); } } ); } ); } ).on( 'error', ( err ) => { - res.status( 500 ).json( { error:`SSH connection failed: ${ err.message }` } ); + const error = new createError( 500, `SSH connection failed: ${ err.message }` ); + next( error ); } ).connect( sshConfig ); }, deleteRepo:async ( req, res, next ) => { const { name } = req.params; if (!name.match( /^[a-zA-Z0-9_-]+$/ )) { - return res.status( 400 ).json( { error:'Invalid repository name' } ); + const error = new createError( 400, 'Invalid repository name' ); + next( error ); } const conn = new Client(); @@ -73,7 +80,8 @@ module.exports = { conn.exec( command, ( err, stream ) => { if (err) { conn.end(); - return res.status( 500 ).json( { error:'SSH command failed' } ); + const error = new createError( 500, `SSH command failed` ); + next( error ); } let output = ''; stream.on( 'data', ( data ) => (output += data) ); @@ -83,20 +91,24 @@ module.exports = { if (code === 0) { res.json( { message:`Repository ${ name } deleted successfully` } ); } else { - res.status( 500 ).json( { error:`Command failed: ${ output }` } ); + const error = new createError( 500, `Command failed: ${ output }` ); + next( error ); } } ); } ); } ).on( 'error', ( err ) => { - res.status( 500 ).json( { error:`SSH connection failed: ${ err.message }` } ); + const error = new createError( 500, `SSH connection failed: ${ err.message }` ); + next( error ); } ).connect( sshConfig ); }, cloneRepo:async ( req, res, next ) => { const { repoName, deployPath, user } = req.body; if (!repoName || !deployPath || !user) { - return res.status( 400 ).json( { error:'Repository name, deployment path, and user are required' } ); + const error = new createError( 400, 'Repository name, deployment path, and user are required' ); + next( error ); } if (!repoName.match( /^[a-zA-Z0-9_-]+$/ )) { - return res.status( 400 ).json( { error:'Invalid repository name' } ); + const error = new createError( 400, 'Invalid repository name' ); + next( error ); } const conn = new Client(); @@ -105,7 +117,8 @@ module.exports = { conn.exec( commands.join( ' && ' ), ( err, stream ) => { if (err) { conn.end(); - return res.status( 500 ).json( { error:'SSH command failed' } ); + const error = new createError( 500, `SSH command failed` ); + next( error ); } let output = ''; stream.on( 'data', ( data ) => (output += data) ); @@ -115,12 +128,14 @@ module.exports = { if (code === 0) { res.json( { message:`Repository ${ repoName } cloned to ${ deployPath }` } ); } else { - res.status( 500 ).json( { error:`Command failed: ${ output }` } ); + const error = new createError( 500, `Command failed: ${ output }` ); + next( error ); } } ); } ); } ).on( 'error', ( err ) => { - res.status( 500 ).json( { error:`SSH connection failed: ${ err.message }` } ); + const error = new createError( 500, `SSH connection failed: ${ err.message }` ); + next( error ); } ).connect( sshConfig ); }, }; \ No newline at end of file diff --git a/src/controllers/media.controller.js b/src/controllers/media.controller.js deleted file mode 100644 index d4fdde2..0000000 --- a/src/controllers/media.controller.js +++ /dev/null @@ -1,128 +0,0 @@ -/** - * @file Media controller for handling media-related API requests - */ - -const db = require('../models'); -const createError = require('http-errors'); - -/** - * Media controller - * @type {Object} - */ -module.exports = { - /** - * Create a new media - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @param {Function} next - Express next middleware function - * @returns {Promise} - */ - async create(req, res, next) { - try { - const media_data = req.body; - const media = await db.media.create(media_data); - res.json(media); - } catch (error) { - logger.error(`Create media error: ${error.message}`); - next(createError(error.status || 409, error.message)); - } - }, - - /** - * Find media by file path - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @param {Function} next - Express next middleware function - * @returns {Promise} - */ - async find_by_file_path(req, res, next) { - try { - const { file_path } = req.params; - const media = await db.media.find_by_file_path(file_path); - if (!media) return next(createError(404, 'Media not found')); - res.json(media); - } catch (error) { - logger.error(`Find media by file path error: ${error.message}`); - next(createError(error.status || 500, error.message)); - } - }, - - /** - * Find one media by ID - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @param {Function} next - Express next middleware function - * @returns {Promise} - */ - async find_one(req, res, next) { - try { - const { id } = req.params; - const media = await db.media.find_one({ id: parseInt(id) }); - if (!media) return next(createError(404, 'Media not found')); - res.json(media); - } catch (error) { - logger.error(`Find media error: ${error.message}`); - next(createError(error.status || 500, error.message)); - } - }, - - /** - * Find many media - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @param {Function} next - Express next middleware function - * @returns {Promise} - */ - async find_many(req, res, next) { - try { - const { limit = '100', offset = '0', ...where } = req.query; - const medias = await db.media.find_many(where, [], null, parseInt(limit), parseInt(offset)); - res.json(medias); - } catch (error) { - logger.error(`Find many media error: ${error.message}`); - next(createError(error.status || 500, error.message)); - } - }, - - /** - * Update a media - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @param {Function} next - Express next middleware function - * @returns {Promise} - */ - async update(req, res, next) { - try { - const { id } = req.params; - const media_data = req.body; - const media = await db.media.instance().find_one({ id: parseInt(id) }); - if (!media) return next(createError(404, 'Media not found')); - const updated_media = await media.update(media_data); - res.json(updated_media); - } catch (error) { - logger.error(`Update media error: ${error.message}`); - next(createError(error.status || 400, error.message)); - } - }, - - /** - * Soft delete a media - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @param {Function} next - Express next middleware function - * @returns {Promise} - */ - async soft_delete(req, res, next) { - try { - const { id } = req.params; - const { deleted_by_id } = req.body; - const media = await db.media.instance().find_one({ id: parseInt(id) }); - if (!media) return next(createError(404, 'Media not found')); - const deleted_media = await media.soft_delete(deleted_by_id); - res.json(deleted_media); - } catch (error) { - logger.error(`Soft delete media error: ${error.message}`); - next(createError(error.status || 400, error.message)); - } - } -}; \ No newline at end of file diff --git a/src/controllers/message.controller.js b/src/controllers/message.controller.js old mode 100644 new mode 100755 index b72e7ce..d81026c --- a/src/controllers/message.controller.js +++ b/src/controllers/message.controller.js @@ -1,10 +1,10 @@ /** * @file Message controller for handling message-related API requests + * @module MessageController */ -const db = require('../models'); -const createError = require('http-errors'); -const logger = global.logger; +const db = require( '../models' ); +const createError = require( 'http-errors' ); /** * Message controller @@ -18,14 +18,33 @@ module.exports = { * @param {Function} next - Express next middleware function * @returns {Promise} */ - async create(req, res, next) { + async create( req, res, next ) { try { - const message_data = req.body; - const message = await db.message.create(message_data); - res.json(message); - } catch (error) { - logger.error(`Create message error: ${error.message}`); - next(createError(error.status || 409, error.message)); + const { sender_id, group_id, recipient_id, content, files = [] } = req.body; + const message_data = { + sender_id: sender_id || req.user.id, + group_id: group_id ? parseInt( group_id ) : null, + recipient_id: recipient_id ? parseInt( recipient_id ) : null, + content, + file_ids: [] + }; + for ( const file of files ) { + const file_data = { + user_id: req.user.id, + file_path: file.path, + file_type: file.mimetype.startsWith( 'image' ) ? 'image' : 'video', + visibility: group_id ? 'family' : recipient_id ? 'specific' : 'family', + specific_users: recipient_id ? [ recipient_id ] : [], + created_by_id: req.user.id + }; + const uploaded_file = await db.file.create( file_data ); + message_data.file_ids.push( uploaded_file.id ); + } + const message = await db.message.create( message_data ); + res.json( message ); + } catch ( error ) { + logger.error( `Create message error: ${ error.message }` ); + next( createError( error.status || 409, error.message ) ); } }, @@ -36,15 +55,15 @@ module.exports = { * @param {Function} next - Express next middleware function * @returns {Promise} */ - async find_by_group_id(req, res, next) { + async find_by_group_id( req, res, next ) { try { const { group_id } = req.params; - const { limit = 100, offset = 0 } = req.query; - const messages = await db.message.find_by_group_id(parseInt(group_id), [], null, parseInt(limit), parseInt(offset)); - res.json(messages); - } catch (error) { - logger.error(`Find messages by group ID error: ${error.message}`); - next(createError(error.status || 500, error.message)); + const { limit = '100', offset = '0' } = req.query; + const messages = await db.message.find_by_group_id( parseInt( group_id ), [], null, parseInt( limit ), parseInt( offset ) ); + res.json( messages ); + } catch ( error ) { + logger.error( `Find messages by group ID error: ${ error.message }` ); + next( createError( error.status || 500, error.message ) ); } }, @@ -55,15 +74,15 @@ module.exports = { * @param {Function} next - Express next middleware function * @returns {Promise} */ - async find_by_recipient_id(req, res, next) { + async find_by_recipient_id( req, res, next ) { try { const { recipient_id } = req.params; - const { limit = 100, offset = 0 } = req.query; - const messages = await db.message.find_by_recipient_id(parseInt(recipient_id), [], null, parseInt(limit), parseInt(offset)); - res.json(messages); - } catch (error) { - logger.error(`Find messages by recipient ID error: ${error.message}`); - next(createError(error.status || 500, error.message)); + const { limit = '100', offset = '0' } = req.query; + const messages = await db.message.find_by_recipient_id( parseInt( recipient_id ), [], null, parseInt( limit ), parseInt( offset ) ); + res.json( messages ); + } catch ( error ) { + logger.error( `Find messages by recipient ID error: ${ error.message }` ); + next( createError( error.status || 500, error.message ) ); } }, @@ -74,16 +93,15 @@ module.exports = { * @param {Function} next - Express next middleware function * @returns {Promise} */ - async find_one(req, res, next) { + async find_one( req, res, next ) { try { const { id } = req.params; - const message = await db.message.find_one({ id: parseInt(id) }); - if (!message) return next(createError(404, 'Message not found')); - res.json(message); - } - catch (error) { - logger.error(`Find message error: ${error.message}`); - next(createError(error.status || 500, error.message)); + const message = await db.message.find_one( { id: parseInt( id ) } ); + if ( !message ) return next( createError( 404, 'Message not found' ) ); + res.json( message ); + } catch ( error ) { + logger.error( `Find message error: ${ error.message }` ); + next( createError( error.status || 500, error.message ) ); } }, @@ -94,14 +112,27 @@ module.exports = { * @param {Function} next - Express next middleware function * @returns {Promise} */ - async find_many(req, res, next) { + async find_many( req, res, next ) { try { - const { limit = 100, offset = 0, ...where } = req.query; - const messages = await db.message.find_many(where, [], null, parseInt(limit), parseInt(offset)); - res.json(messages); - } catch (error) { - logger.error(`Find many messages error: ${error.message}`); - next(createError(error.status || 500, error.message)); + const { limit = '100', offset = '0', ...where } = req.query; + if ( req.user ) { + where[ '$or' ] = [ + { + group_id: { + $in: `SELECT group_id + FROM phase.message_group_members + WHERE user_id = ${ req.user.id }` + } + }, + { recipient_id: req.user.id }, + { sender_id: req.user.id } + ]; + } + const messages = await db.message.find_many( where, [], null, parseInt( limit ), parseInt( offset ) ); + res.json( messages ); + } catch ( error ) { + logger.error( `Find many messages error: ${ error.message }` ); + next( createError( error.status || 500, error.message ) ); } }, @@ -112,16 +143,38 @@ module.exports = { * @param {Function} next - Express next middleware function * @returns {Promise} */ - async mark_as_read(req, res, next) { + async mark_as_read( req, res, next ) { try { const { id } = req.params; - const message = await db.message.instance().find_one({ id: parseInt(id) }); - if (!message) return next(createError(404, 'Message not found')); + const message = await db.message.instance().find_one( { id: parseInt( id ) } ); + if ( !message ) return next( createError( 404, 'Message not found' ) ); + if ( message.recipient_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) { + return next( createError( 403, 'Unauthorized to mark this message as read' ) ); + } const updated_message = await message.mark_as_read(); - res.json(updated_message); - } catch (error) { - logger.error(`Mark message as read error: ${error.message}`); - next(createError(error.status || 400, error.message)); + res.json( updated_message ); + } catch ( error ) { + logger.error( `Mark message as read error: ${ error.message }` ); + next( createError( error.status || 400, error.message ) ); + } + }, + + /** + * Add a reaction to a message + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async add_reaction( req, res, next ) { + try { + const { messageId } = req.params; + const { reaction } = req.body; + const reaction_data = await db.message_reactions.add( messageId, req.user.id, reaction ); + res.json( reaction_data ); + } catch ( error ) { + logger.error( `Add message reaction error: ${ error.message }` ); + next( createError( error.status || 400, error.message ) ); } } }; \ No newline at end of file diff --git a/src/controllers/message_files.controller.js b/src/controllers/message_files.controller.js new file mode 100755 index 0000000..f0d7fc7 --- /dev/null +++ b/src/controllers/message_files.controller.js @@ -0,0 +1,82 @@ +/** + * @file Message files controller for handling message-files relation API requests + * @module MessageFilesController + */ + +const db = require( '../models' ); +const createError = require( 'http-errors' ); + +/** + * Message files controller + * @type {Object} + */ +module.exports = { + /** + * Add a message-file relation + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async add_relation( req, res, next ) { + try { + const { message_id, file_id } = req.body; + const message = await db.message.find_one( { id: parseInt( message_id ) } ); + if ( !message ) return next( createError( 404, 'Message not found' ) ); + if ( message.sender_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) { + return next( createError( 403, 'Unauthorized to add relation to this message' ) ); + } + const file = await db.file.find_one( { id: parseInt( file_id ) } ); + if ( !file ) return next( createError( 404, 'File not found' ) ); + const relation = await db.message_files.add_relation( message_id, file_id ); + res.json( relation ); + } catch ( error ) { + logger.error( `Add message-file relation error: ${ error.message }` ); + next( createError( error.status || 400, error.message ) ); + } + }, + + /** + * Remove a message-file relation + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async remove_relation( req, res, next ) { + try { + const { message_id, file_id } = req.params; + const message = await db.message.find_one( { id: parseInt( message_id ) } ); + if ( !message ) return next( createError( 404, 'Message not found' ) ); + if ( message.sender_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) { + return next( createError( 403, 'Unauthorized to remove relation from this message' ) ); + } + const file = await db.file.find_one( { id: parseInt( file_id ) } ); + if ( !file ) return next( createError( 404, 'File not found' ) ); + const relation = await db.message_files.remove_relation( message_id, file_id ); + res.json( relation || { message: 'Relation removed' } ); + } catch ( error ) { + logger.error( `Remove message-file relation error: ${ error.message }` ); + next( createError( error.status || 400, error.message ) ); + } + }, + + /** + * Find message-file relations by message ID + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @returns {Promise} + */ + async find_by_message_id( req, res, next ) { + try { + const { message_id } = req.params; + const { limit = '100', offset = '0' } = req.query; + const relations = await db.message_files.find_by_message_id( parseInt( message_id ), [], null, parseInt( limit ), parseInt( offset ) ); + res.json( relations ); + } catch ( error ) { + logger.error( `Find message-file relations by message ID error: ${ error.message }` ); + next( createError( error.status || 500, error.message ) ); + } + } +}; \ No newline at end of file diff --git a/src/controllers/message_group.controller.js b/src/controllers/message_group.controller.js old mode 100644 new mode 100755 index 55909d3..5b5d4b8 --- a/src/controllers/message_group.controller.js +++ b/src/controllers/message_group.controller.js @@ -2,9 +2,8 @@ * @file Message group controller for handling message group-related API requests */ -const db = require('../models'); -const createError = require('http-errors'); -const logger = global.logger; +const db = require( '../models' ); +const createError = require( 'http-errors' ); /** * Message group controller @@ -18,14 +17,14 @@ module.exports = { * @param {Function} next - Express next middleware function * @returns {Promise} */ - async create(req, res, next) { + async create( req, res, next ) { try { const message_group_data = req.body; - const message_group = await db.message_group.create(message_group_data); - res.json(message_group); - } catch (error) { - logger.error(`Create message group error: ${error.message}`); - next(createError(error.status || 409, error.message)); + const message_group = await db.message_group.create( message_group_data ); + res.json( message_group ); + } catch ( error ) { + logger.error( `Create message group error: ${ error.message }` ); + next( createError( error.status || 409, error.message ) ); } }, @@ -36,15 +35,15 @@ module.exports = { * @param {Function} next - Express next middleware function * @returns {Promise} */ - async find_one(req, res, next) { + async find_one( req, res, next ) { try { const { id } = req.params; - const message_group = await db.message_group.find_one({ id: parseInt(id) }); - if (!message_group) return next(createError(404, 'Message group not found')); - res.json(message_group); - } catch (error) { - logger.error(`Find message group error: ${error.message}`); - next(createError(error.status || 500, error.message)); + const message_group = await db.message_group.find_one( { id: parseInt( id ) } ); + if ( !message_group ) return next( createError( 404, 'Message group not found' ) ); + res.json( message_group ); + } catch ( error ) { + logger.error( `Find message group error: ${ error.message }` ); + next( createError( error.status || 500, error.message ) ); } }, @@ -55,14 +54,14 @@ module.exports = { * @param {Function} next - Express next middleware function * @returns {Promise} */ - async find_many(req, res, next) { + async find_many( req, res, next ) { try { - const { limit = 100, offset = 0, ...where } = req.query; - const message_groups = await db.message_group.find_many(where, [], null, parseInt(limit), parseInt(offset)); - res.json(message_groups); - } catch (error) { - logger.error(`Find many message groups error: ${error.message}`); - next(createError(error.status || 500, error.message)); + const { limit = '100', offset = '0', ...where } = req.query; + const message_groups = await db.message_group.find_many( where, [], null, parseInt( limit ), parseInt( offset ) ); + res.json( message_groups ); + } catch ( error ) { + logger.error( `Find many message groups error: ${ error.message }` ); + next( createError( error.status || 500, error.message ) ); } }, @@ -73,17 +72,17 @@ module.exports = { * @param {Function} next - Express next middleware function * @returns {Promise} */ - async update(req, res, next) { + async update( req, res, next ) { try { const { id } = req.params; const message_group_data = req.body; - const message_group = await db.message_group.instance().find_one({ id: parseInt(id) }); - if (!message_group) return next(createError(404, 'Message group not found')); - const updated_message_group = await message_group.update(message_group_data); - res.json(updated_message_group); - } catch (error) { - logger.error(`Update message group error: ${error.message}`); - next(createError(error.status || 400, error.message)); + const message_group = await db.message_group.instance().find_one( { id: parseInt( id ) } ); + if ( !message_group ) return next( createError( 404, 'Message group not found' ) ); + const updated_message_group = await message_group.update( message_group_data ); + res.json( updated_message_group ); + } catch ( error ) { + logger.error( `Update message group error: ${ error.message }` ); + next( createError( error.status || 400, error.message ) ); } } }; \ No newline at end of file diff --git a/src/controllers/message_group_members.controller.js b/src/controllers/message_group_members.controller.js old mode 100644 new mode 100755 diff --git a/src/controllers/phone_number.controller.js b/src/controllers/phone_number.controller.js old mode 100644 new mode 100755 diff --git a/src/controllers/post.controller.js b/src/controllers/post.controller.js old mode 100644 new mode 100755 diff --git a/src/controllers/post_files.controller.js b/src/controllers/post_files.controller.js old mode 100644 new mode 100755 diff --git a/src/controllers/post_users.controller.js b/src/controllers/post_users.controller.js old mode 100644 new mode 100755 diff --git a/src/controllers/role.controller.js b/src/controllers/role.controller.js old mode 100644 new mode 100755 diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js old mode 100644 new mode 100755 index cfa9418..dc62f79 --- a/src/controllers/user.controller.js +++ b/src/controllers/user.controller.js @@ -59,7 +59,7 @@ module.exports = { const { limit = '100', offset = '0', ...where } = req.query; const users = await db.user.find_many( where, [], null, parseInt( limit ), parseInt( offset ) ); if (!users.length) return next( createError( 404, 'No users found' ) ); - res.status( 200 ).send( users.map( u => new (require( '../models/user.model' )( u ).toJSON()) ) ); + res.status( 200 ).send( users ); } catch (error) { logger.error( `Index users error: ${ error.message }` ); next( createError( error.status || 500, error.message ) ); @@ -190,7 +190,7 @@ module.exports = { async get_current_user( req, res, next ) { try { const { id } = req.user; - const user = await db.user.instance().find_one( { id: id } ); + const user = await db.user.instance().find_one( { id:id } ); if (!user) return next( createError( 404, 'User not found' ) ); res.status( 200 ).send( user ); } catch (error) { diff --git a/src/controllers/user_addresses.controller.js b/src/controllers/user_addresses.controller.js old mode 100644 new mode 100755 diff --git a/src/controllers/user_phone_numbers.controller.js b/src/controllers/user_phone_numbers.controller.js old mode 100644 new mode 100755 diff --git a/src/controllers/user_roles.controller.js b/src/controllers/user_roles.controller.js old mode 100644 new mode 100755 diff --git a/src/controllers/vpn.controller.js b/src/controllers/vpn.controller.js old mode 100644 new mode 100755 diff --git a/src/models/address.model.js b/src/models/address.model.js old mode 100644 new mode 100755 diff --git a/src/models/authentication.model.js b/src/models/authentication.model.js old mode 100644 new mode 100755 index 66eb22f..8de62f6 --- a/src/models/authentication.model.js +++ b/src/models/authentication.model.js @@ -5,7 +5,7 @@ const { Model, NotFoundError, ValidationError, FailedToCreateError } = require( './model' ); /** - * @typedef {Object} Authentication + * @typedef {Object} Authentication_props * @property {number} id - Authentication ID (primary key) * @property {number} user_id - User ID (foreign key) * @property {string} password - Hashed password @@ -25,12 +25,32 @@ const { Model, NotFoundError, ValidationError, FailedToCreateError } = require( /** * Authentication model class + * @class * @extends Model + * @property {number} id - Authentication ID (primary key) + * @property {number} user_id - User ID (foreign key) + * @property {string} password - Hashed password + * @property {string} password_salt - Password salt + * @property {string|null} password_verification_token - Verification token + * @property {Date|null} password_verification_token_expiry - Token expiry + * @property {Date|null} last_password_failure_date - Last failed login attempt + * @property {number} password_failures_since_last_success - Failure count + * @property {boolean} is_deleted - Soft delete flag + * @property {number|null} deleted_by_id - ID of user who deleted this record + * @property {Date|null} deleted_at - Deletion timestamp + * @property {string|null} password_reset_token - Reset token + * @property {Date|null} password_reset_expire_date - Reset token expiry + * @property {boolean|null} is_locked - Account lock status + * @property {Date|null} locked_date - Lock timestamp + * */ class Authentication extends Model { /** * Create an Authentication instance - * @param {Partial} [props] - Authentication properties + * @param {Partial} [props] - Authentication properties + * @returns {Model||Object} Authentication instance + * @throws {ValidationError} If required fields are missing + * @throws {FailedToCreateError} If creation fails */ constructor( props ) { super( props ); @@ -78,7 +98,13 @@ class Authentication extends Model { this.default_order_by = 'ORDER BY auth.user_id ASC'; this.instance = _props => new Authentication( _props ); this.toJSON = () => { - const { password, password_salt, password_verification_token, password_reset_token, ...safeData } = super.toJSON(); + const { + password, + password_salt, + password_verification_token, + password_reset_token, + ...safeData + } = super.toJSON(); return safeData; }; }; @@ -123,7 +149,7 @@ class Authentication extends Model { * Find authentication record by user ID * @param {number|string} user_id - User ID * @param {string[]} [excludes=['password', 'password_salt']] - Fields to exclude from result - * @returns {Promise} Authentication instance or null + * @returns {Promise|null>} Authentication instance or null */ static async find_by_user_id( user_id, excludes = ['password', 'password_salt'] ) { return await new Authentication().find_one( { user_id }, excludes ); @@ -142,7 +168,7 @@ class Authentication extends Model { /** * Soft delete authentication record * @param {number|string} deleted_by_id - ID of user performing deletion - * @returns {Promise} Updated authentication instance + * @returns {Promise>} Updated authentication instance * @throws {ValidationError} If deleted_by_id is invalid * @throws {NotFoundError} If record not found */ @@ -160,7 +186,7 @@ class Authentication extends Model { /** * Lock user account - * @returns {Promise} Updated authentication instance + * @returns {Promise>} Updated authentication instance * @throws {NotFoundError} If record not found */ async lock_account() { @@ -172,7 +198,7 @@ class Authentication extends Model { /** * Unlock user account - * @returns {Promise} Updated authentication instance + * @returns {Promise>} Updated authentication instance * @throws {NotFoundError} If record not found */ async unlock_account() { diff --git a/src/models/comment_reactions.model.js b/src/models/comment_reactions.model.js new file mode 100755 index 0000000..326b611 --- /dev/null +++ b/src/models/comment_reactions.model.js @@ -0,0 +1,109 @@ +/** + * @file Comment reactions model for phase.comment_reactions table + * @module CommentReaction + */ + +const { Model, ValidationError, FailedToCreateError } = require( './model' ); + +/** + * @typedef {Object} CommentReaction + * @property {number} comment_id - Comment ID (foreign key to comments) + * @property {number} user_id - User ID (foreign key to users) + * @property {string} reaction - Reaction type (e.g., 'like', 'heart', 'laugh') + */ + +/** + * Comment reactions model class + * @extends Model + */ +class CommentReaction extends Model { + /** + * Create a CommentReaction instance + * @param {Partial} [props] - Comment reaction properties + */ + constructor( props ) { + super( props ); + /** @type {string} Database table name */ + this.table = 'phase.comment_reactions'; + /** @type {string} Table alias for queries */ + this.prepend = 'cr'; + /** @type {string[]} Allowed columns for queries */ + this.default_columns = [ 'comment_id', 'user_id', 'reaction' ]; + /** @type {string[]} Columns excluded from updates */ + this.update_exclude_columns = [ 'comment_id', 'user_id' ]; + /** @type {string} Base query for single record retrieval */ + this.base_query = `SELECT cr.comment_id, cr.user_id, cr.reaction + FROM phase.comment_reactions cr`; + /** @type {string} Base query for multiple record retrieval */ + this.base_list_query = this.base_query; + /** @type {string} Default ORDER BY clause */ + this.default_order_by = 'ORDER BY cr.comment_id ASC'; + /** @type {Function} Function to instantiate a model */ + this.instance = _props => new CommentReaction( _props ); + } + + /** + * Add a comment reaction + * @param {number|string} comment_id - Comment ID + * @param {number|string} user_id - User ID + * @param {string} reaction - Reaction type + * @returns {Promise} Created or updated reaction instance + * @throws {ValidationError} If required fields are missing + * @throws {FailedToCreateError} If creation fails + */ + static async add( comment_id, user_id, reaction ) { + const comment_id_int = parseInt( comment_id, 10 ); + const user_id_int = parseInt( user_id, 10 ); + if ( isNaN( comment_id_int ) || isNaN( user_id_int ) || !reaction ) { + throw new ValidationError( 'comment_id, user_id, and reaction are required' ); + } + const query_str = ` + INSERT INTO phase.comment_reactions (comment_id, user_id, reaction) + VALUES ($1, $2, $3) + ON CONFLICT (comment_id, user_id) DO UPDATE SET reaction = EXCLUDED.reaction + RETURNING *; + `; + const values = [ comment_id_int, user_id_int, reaction ]; + const result = await phsdb.query( query_str, values, { plain: true } ); + if ( !result ) throw new FailedToCreateError( 'Failed to add comment reaction' ); + return new CommentReaction( result ); + } + + /** + * Remove a comment reaction + * @param {number|string} comment_id - Comment ID + * @param {number|string} user_id - User ID + * @returns {Promise} Deleted reaction instance or null + * @throws {ValidationError} If IDs are invalid + */ + static async remove( comment_id, user_id ) { + const comment_id_int = parseInt( comment_id, 10 ); + const user_id_int = parseInt( user_id, 10 ); + if ( isNaN( comment_id_int ) || isNaN( user_id_int ) ) { + throw new ValidationError( 'comment_id and user_id must be valid integers' ); + } + const query_str = `DELETE + FROM phase.comment_reactions + WHERE comment_id = $1 + AND user_id = $2 + RETURNING *;`; + const values = [ comment_id_int, user_id_int ]; + const result = await phsdb.query( query_str, values, { plain: true } ); + return result ? new CommentReaction( result ) : null; + } + + /** + * Find comment reactions by comment ID + * @param {number|string} comment_id - Comment ID + * @param {string[]} [excludes=[]] - Fields to exclude from result + * @param {{name: string, direction?: 'asc'|'desc'}|null} [order_by=null] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of reactions + */ + static async find_by_comment_id( comment_id, excludes = [], order_by = null, limit = 100, offset = 0 ) { + return await new CommentReaction().find_many( { comment_id }, excludes, order_by, limit, offset ); + } +} + +module.exports = CommentReaction; \ No newline at end of file diff --git a/src/models/comments.model.js b/src/models/comments.model.js new file mode 100755 index 0000000..a0c49ff --- /dev/null +++ b/src/models/comments.model.js @@ -0,0 +1,94 @@ +/** + * @file Comments model for phase.comments table + * @module Comment + */ + +const { Model, ValidationError, FailedToCreateError } = require( './model' ); + +/** + * @typedef {Object} Comment + * @property {number} id - Comment ID (primary key) + * @property {number} post_id - Post ID (foreign key to posts) + * @property {number} user_id - User ID (foreign key to users) + * @property {number|null} parent_id - Parent comment ID (foreign key to comments) + * @property {string} content - Comment content + * @property {Date} created_at - Creation timestamp + */ + +/** + * Comments model class + * @extends Model + */ +class Comment extends Model { + /** + * Create a Comment instance + * @param {Partial} [props] - Comment properties + */ + constructor( props ) { + super( props ); + /** @type {string} Database table name */ + this.table = 'phase.comments'; + /** @type {string} Table alias for queries */ + this.prepend = 'c'; + /** @type {string[]} Allowed columns for queries */ + this.default_columns = [ 'id', 'post_id', 'user_id', 'parent_id', 'content', 'created_at' ]; + /** @type {string[]} Columns excluded from updates */ + this.update_exclude_columns = [ 'id', 'post_id', 'user_id', 'created_at' ]; + /** @type {string} Base query for single record retrieval */ + this.base_query = ` + SELECT c.id, + c.post_id, + c.user_id, + c.parent_id, + c.content, + c.created_at, + concat(u.first_name, ' ', u.last_name) as user_name + FROM phase.comments c + JOIN phase.users u ON c.user_id = u.id + `; + /** @type {string} Base query for multiple record retrieval */ + this.base_list_query = this.base_query; + /** @type {string} Default ORDER BY clause */ + this.default_order_by = 'ORDER BY c.created_at DESC'; + /** @type {Function} Function to instantiate a model */ + this.instance = _props => new Comment( _props ); + } + + /** + * Create a new comment + * @param {Omit} comment_data - Comment data + * @returns {Promise} Created comment instance + * @throws {ValidationError} If required fields are missing + * @throws {FailedToCreateError} If creation fails + */ + static async create( comment_data ) { + const { post_id, user_id, parent_id, content } = comment_data; + if ( !post_id || !user_id || !content ) { + throw new ValidationError( 'Missing required fields: post_id, user_id, content' ); + } + const query_str = ` + INSERT INTO phase.comments (post_id, user_id, parent_id, content) + VALUES ($1, $2, $3, $4) + RETURNING *; + `; + const values = [ post_id, user_id, parent_id || null, content ]; + const result = await phsdb.query( query_str, values, { plain: true } ); + if ( !result ) throw new FailedToCreateError( 'Failed to create comment' ); + return new Comment( result ); + } + + /** + * Find comments by post ID + * @param {number|string} post_id - Post ID + * @param {string[]} [excludes=[]] - Fields to exclude from result + * @param {{name: string, direction?: 'asc'|'desc'}|null} [order_by=null] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of comments + */ + static async find_by_post_id( post_id, excludes = [], order_by = null, limit = 100, offset = 0 ) { + return await new Comment().find_many( { post_id }, excludes, order_by, limit, offset ); + } +} + +module.exports = Comment; \ No newline at end of file diff --git a/src/models/file.model.js b/src/models/file.model.js new file mode 100755 index 0000000..2c1a165 --- /dev/null +++ b/src/models/file.model.js @@ -0,0 +1,135 @@ +/** + * @file Files model for phase.files table + * @module File + */ + +const { Model, NotFoundError, ValidationError, FailedToCreateError } = require( './model' ); + +/** + * @typedef {Object} File + * @property {number} id - File ID (primary key) + * @property {number} user_id - User ID (foreign key) + * @property {string} file_path - File path + * @property {string} file_type - File type (e.g., 'image', 'video') + * @property {string} visibility - Visibility (e.g., 'personal', 'family', 'specific') + * @property {number|null} created_by_id - ID of user who created this file + * @property {Date} created_at - Creation timestamp + * @property {boolean} is_deleted - Soft delete flag + * @property {number|null} deleted_by_id - ID of user who deleted this file + * @property {Date|null} deleted_at - Deletion timestamp + */ + +/** + * Files model class + * @extends Model + */ +class File extends Model { + /** + * Create a File instance + * @param {Partial} [props] - File properties + */ + constructor( props ) { + super( props ); + /** @type {string} Database table name */ + this.table = 'phase.files'; + /** @type {string} Table alias for queries */ + this.prepend = 'f'; + /** @type {string[]} Allowed columns for queries */ + this.default_columns = ['id', 'user_id', 'file_path', 'file_type', 'visibility', 'created_by_id', 'created_at', 'is_deleted', 'deleted_by_id', 'deleted_at']; + /** @type {string[]} Columns excluded from updates */ + this.update_exclude_columns = ['id', 'created_at', 'is_deleted', 'deleted_at', 'deleted_by_id']; + /** @type {string} Base query for single record retrieval */ + this.base_query = ` + SELECT f.id, + f.user_id, + f.file_path, + f.file_type, + f.visibility, + f.created_by_id, + f.created_at, + f.is_deleted, + f.deleted_by_id, + f.deleted_at, + concat(u.first_name, ' ', u.last_name) as user_name + FROM phase.files f + JOIN phase.users u ON f.user_id = u.id + `; + /** @type {string} Base query for multiple record retrieval */ + this.base_list_query = this.base_query; + /** @type {string} Default ORDER BY clause */ + this.default_order_by = 'ORDER BY f.created_at DESC'; + /** @type {Function} Function to instantiate a model */ + this.instance = _props => new File( _props ); + } + + /** + * Create a new file + * @param {Omit & {specific_users?: number[]}} file_data - File data + * @returns {Promise} Created file instance + * @throws {ValidationError} If required fields are missing + * @throws {FailedToCreateError} If creation fails + */ + static async create( file_data ) { + const { + user_id, + file_path, + file_type, + visibility = 'family', + created_by_id = null, + specific_users = [] + } = file_data; + if (!user_id || !file_path || !file_type) { + throw new ValidationError( 'Missing required fields: user_id, file_path, file_type' ); + } + return await this.prototype.with_transaction( async ( client ) => { + const query_str = ` + INSERT INTO phase.files (user_id, file_path, file_type, visibility, created_by_id) + VALUES ($1, $2, $3, $4, $5) + RETURNING *; + `; + const values = [user_id, file_path, file_type, visibility, created_by_id || user_id]; + const result = await phsdb.client_query( client, query_str, values, { plain:true } ); + if (!result) throw new FailedToCreateError( 'Failed to create file' ); + const file = new File( result ); + if (visibility === 'specific' && specific_users?.length) { + const user_query = `INSERT INTO phase.file_users (file_id, user_id) + VALUES ($1, $2);`; + for (const user_id of specific_users) { + await phsdb.client_query( client, user_query, [file.id, user_id] ); + } + } + return file; + } ); + } + + /** + * Find file by file path + * @param {string} file_path - File path to search for + * @param {string[]} [excludes=[]] - Fields to exclude from result + * @returns {Promise} File instance or null + */ + static async find_by_file_path( file_path, excludes = [] ) { + return await new File().find_one( { file_path }, excludes ); + } + + /** + * Soft delete file + * @param {number|string} deleted_by_id - ID of user performing deletion + * @returns {Promise} Updated file instance + * @throws {ValidationError} If deleted_by_id is invalid + * @throws {NotFoundError} If record not found + */ + async soft_delete( deleted_by_id ) { + const deleted_by_id_int = parseInt( deleted_by_id, 10 ); + if (isNaN( deleted_by_id_int )) { + throw new ValidationError( 'deleted_by_id must be a valid integer' ); + } + return await this.update( { + is_deleted:true, + deleted_at:new Date().toISOString(), + deleted_by_id:deleted_by_id_int + } ); + } +} + +module.exports = File; \ No newline at end of file diff --git a/src/models/file_users.model.js b/src/models/file_users.model.js new file mode 100755 index 0000000..149fc04 --- /dev/null +++ b/src/models/file_users.model.js @@ -0,0 +1,104 @@ +/** + * @file File users model for phase.file_users table + * @module FileUser + */ + +const { Model, ValidationError, FailedToCreateError } = require( './model' ); + +/** + * @typedef {Object} FileUser + * @property {number} file_id - File ID (foreign key to files) + * @property {number} user_id - User ID (foreign key to users) + */ + +/** + * File users model class + * @extends Model + */ +class FileUser extends Model { + /** + * Create a FileUser instance + * @param {Partial} [props] - File user properties + */ + constructor( props ) { + super( props ); + /** @type {string} Database table name */ + this.table = 'phase.file_users'; + /** @type {string} Table alias for queries */ + this.prepend = 'fu'; + /** @type {string[]} Allowed columns for queries */ + this.default_columns = [ 'file_id', 'user_id' ]; + /** @type {string[]} Columns excluded from updates */ + this.update_exclude_columns = [ 'file_id', 'user_id' ]; + /** @type {string} Base query for single record retrieval */ + this.base_query = `SELECT fu.file_id, fu.user_id + FROM phase.file_users fu`; + /** @type {string} Base query for multiple record retrieval */ + this.base_list_query = this.base_query; + /** @type {string} Default ORDER BY clause */ + this.default_order_by = 'ORDER BY fu.file_id ASC'; + /** @type {Function} Function to instantiate a model */ + this.instance = _props => new FileUser( _props ); + } + + /** + * Add a file-user relation + * @param {number|string} file_id - File ID + * @param {number|string} user_id - User ID + * @returns {Promise} Created relation instance + * @throws {ValidationError} If IDs are invalid + * @throws {FailedToCreateError} If creation fails + */ + static async add_relation( file_id, user_id ) { + const file_id_int = parseInt( file_id, 10 ); + const user_id_int = parseInt( user_id, 10 ); + if ( isNaN( file_id_int ) || isNaN( user_id_int ) ) { + throw new ValidationError( 'file_id and user_id must be valid integers' ); + } + const query_str = `INSERT INTO phase.file_users (file_id, user_id) + VALUES ($1, $2) + RETURNING *;`; + const values = [ file_id_int, user_id_int ]; + const result = await phsdb.query( query_str, values, { plain: true } ); + if ( !result ) throw new FailedToCreateError( 'Failed to add file-user relation' ); + return new FileUser( result ); + } + + /** + * Remove a file-user relation + * @param {number|string} file_id - File ID + * @param {number|string} user_id - User ID + * @returns {Promise} Deleted relation instance or null + * @throws {ValidationError} If IDs are invalid + */ + static async remove_relation( file_id, user_id ) { + const file_id_int = parseInt( file_id, 10 ); + const user_id_int = parseInt( user_id, 10 ); + if ( isNaN( file_id_int ) || isNaN( user_id_int ) ) { + throw new ValidationError( 'file_id and user_id must be valid integers' ); + } + const query_str = `DELETE + FROM phase.file_users + WHERE file_id = $1 + AND user_id = $2 + RETURNING *;`; + const values = [ file_id_int, user_id_int ]; + const result = await phsdb.query( query_str, values, { plain: true } ); + return result ? new FileUser( result ) : null; + } + + /** + * Find file-user relations by file ID + * @param {number|string} file_id - File ID + * @param {string[]} [excludes=[]] - Fields to exclude from result + * @param {{name: string, direction?: 'asc'|'desc'}|null} [order_by=null] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of relations + */ + static async find_by_file_id( file_id, excludes = [], order_by = null, limit = 100, offset = 0 ) { + return await new FileUser().find_many( { file_id }, excludes, order_by, limit, offset ); + } +} + +module.exports = FileUser; \ No newline at end of file diff --git a/src/models/index.js b/src/models/index.js old mode 100644 new mode 100755 index 59e2c8e..635ff5b --- a/src/models/index.js +++ b/src/models/index.js @@ -1,7 +1,8 @@ -// noinspection ExceptionCaughtLocallyJS +// noinspection JSUnusedGlobalSymbols /** * @file Unified database access interface + * @module Database */ const model_cache = new Map(); @@ -12,639 +13,1649 @@ const { NotFoundError } = require( './model' ); * @param {string} name - Model name * @returns {Function} Model class */ -const get_model = (name) => { - if (!model_cache.has(name)) { - model_cache.set(name, require(`./${name}.model`)); +const get_model = ( name ) => { + if (!model_cache.has( name )) { + model_cache.set( name, require( `./${ name }.model` ) ); } - return model_cache.get(name); + return model_cache.get( name ); }; /** * @returns {Function} User model class */ -const get_user_model = () => get_model('user'); +const get_user_model = () => get_model( 'user' ); /** * @returns {Function} Phone number model class */ -const get_phone_number_model = () => get_model('phone_number'); +const get_phone_number_model = () => get_model( 'phone_number' ); /** * @returns {Function} User phone numbers model class */ -const get_user_phone_numbers_model = () => get_model('user_phone_numbers'); +const get_user_phone_numbers_model = () => get_model( 'user_phone_numbers' ); /** * @returns {Function} Address model class */ -const get_address_model = () => get_model('address'); +const get_address_model = () => get_model( 'address' ); /** * @returns {Function} User addresses model class */ -const get_user_addresses_model = () => get_model('user_addresses'); +const get_user_addresses_model = () => get_model( 'user_addresses' ); /** * @returns {Function} Authentication model class */ -const get_authentication_model = () => get_model('authentication'); +const get_authentication_model = () => get_model( 'authentication' ); /** * @returns {Function} Role model class */ -const get_role_model = () => get_model('role'); +const get_role_model = () => get_model( 'role' ); /** * @returns {Function} User roles model class */ -const get_user_roles_model = () => get_model('user_roles'); +const get_user_roles_model = () => get_model( 'user_roles' ); /** - * @returns {Function} Media model class + * @returns {Function} File model class */ -const get_media_model = () => get_model('media'); +const get_file_model = () => get_model( 'file' ); + +/** + * @returns {Function} File users model class + */ +const get_file_users_model = () => get_model( 'file_users' ); /** * @returns {Function} Post model class */ -const get_post_model = () => get_model('post'); +const get_post_model = () => get_model( 'post' ); + +/** + * @returns {Function} Post files model class + */ +const get_post_files_model = () => get_model( 'post_files' ); + +/** + * @returns {Function} Post users model class + */ +const get_post_users_model = () => get_model( 'post_users' ); + +/** + * @returns {Function} Post reactions model class + */ +const get_post_reactions_model = () => get_model( 'post_reactions' ); + +/** + * @returns {Function} Comments model class + */ +const get_comments_model = () => get_model( 'comments' ); + +/** + * @returns {Function} Comment reactions model class + */ +const get_comment_reactions_model = () => get_model( 'comment_reactions' ); /** * @returns {Function} Message group model class */ -const get_message_group_model = () => get_model('message_group'); +const get_message_group_model = () => get_model( 'message_group' ); /** * @returns {Function} Message group members model class */ -const get_message_group_members_model = () => get_model('message_group_members'); +const get_message_group_members_model = () => get_model( 'message_group_members' ); /** * @returns {Function} Message model class */ -const get_message_model = () => get_model('message'); +const get_message_model = () => get_model( 'message' ); + +/** + * @returns {Function} Message files model class + */ +const get_message_files_model = () => get_model( 'message_files' ); + +/** + * @returns {Function} Message reactions model class + */ +const get_message_reactions_model = () => get_model( 'message_reactions' ); /** * Unified database interface * @type {Object} */ const db = { - user: { - async create(user_data) { + user:{ + /** + * Create a new user + * @param {Object} user_data - User data + * @returns {Promise} Created user instance + * @throws {Error} If creation fails + */ + async create( user_data ) { try { - return await get_user_model().create(user_data); + return await get_user_model().create( user_data ); } catch (error) { - logger.error(`Failed to create user: ${error.message}`); + logger.error( `Failed to create user: ${ error.message }` ); throw error; } }, - async find_by_email(email, excludes) { + /** + * Find user by email + * @param {string} email - Email to search for + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} User instance or null + * @throws {Error} If the query fails + */ + async find_by_email( email, excludes ) { try { - return await get_user_model().find_by_email(email, excludes); + return await get_user_model().find_by_email( email, excludes ); } catch (error) { - logger.error(`Failed to find user by email: ${error.message}`); + logger.error( `Failed to find user by email: ${ error.message }` ); throw error; } }, - async find_by_nickname(nickname, excludes) { + /** + * Find user by nickname + * @param {string} nickname - Nickname to search for + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} User instance or null + * @throws {Error} If the query fails + */ + async find_by_nickname( nickname, excludes ) { try { - return await get_user_model().find_by_nickname(nickname, excludes); + return await get_user_model().find_by_nickname( nickname, excludes ); } catch (error) { - logger.error(`Failed to find user by nickname: ${error.message}`); + logger.error( `Failed to find user by nickname: ${ error.message }` ); throw error; } }, - async find_active(excludes, order_by, limit, offset) { + /** + * Find active users + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of user instances + * @throws {Error} If the query fails + */ + async find_active( excludes, order_by, limit, offset ) { try { - return await get_user_model().find_active(excludes, order_by, limit, offset); + return await get_user_model().find_active( excludes, order_by, limit, offset ); } catch (error) { - logger.error(`Failed to find active users: ${error.message}`); + logger.error( `Failed to find active users: ${ error.message }` ); throw error; } }, - async find_deleted(excludes, order_by, limit, offset) { + /** + * Find deleted users + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of user instances + * @throws {Error} If the query fails + */ + async find_deleted( excludes, order_by, limit, offset ) { try { - return await get_user_model().find_deleted(excludes, order_by, limit, offset); + return await get_user_model().find_deleted( excludes, order_by, limit, offset ); } catch (error) { - logger.error(`Failed to find deleted users: ${error.message}`); + logger.error( `Failed to find deleted users: ${ error.message }` ); throw error; } }, - async find_one(where, excludes) { + /** + * Find one user + * @param {Object} where - Conditions for the query + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} User instance or null + * @throws {Error} If the query fails + */ + async find_one( where, excludes ) { try { - return await new (get_user_model())().find_one(where, excludes); + return await new (get_user_model())().find_one( where, excludes ); } catch (error) { - logger.error(`Failed to find one user: ${error.message}`); + logger.error( `Failed to find one user: ${ error.message }` ); throw error; } }, - async find_many(where, excludes, order_by, limit, offset) { + /** + * Find many users + * @param {Object} [where] - Conditions for the query + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of user instances + * @throws {Error} If the query fails + */ + async find_many( where, excludes, order_by, limit, offset ) { try { - return await new (get_user_model())().find_many(where, excludes, order_by, limit, offset); + return await new (get_user_model())().find_many( where, excludes, order_by, limit, offset ); } catch (error) { - logger.error(`Failed to find many users: ${error.message}`); + logger.error( `Failed to find many users: ${ error.message }` ); throw error; } }, - async deactivate(id, deactivated_by_id) { + /** + * Deactivate a user + * @param {number} id - User ID + * @param {number} deactivated_by_id - ID of user performing deactivation + * @returns {Promise} Updated user instance + * @throws {NotFoundError} If user not found + * @throws {Error} If update fails + */ + async deactivate( id, deactivated_by_id ) { try { - const user = await new (get_user_model())().find_one({ id }, []); - if (!user) throw new NotFoundError('User not found'); - return await user.deactivate(deactivated_by_id); + const user = await new (get_user_model())().find_one( { id }, [] ); + if (!user) { // noinspection ExceptionCaughtLocallyJS + throw new NotFoundError( 'User not found' ); + } + return await user.deactivate( deactivated_by_id ); } catch (error) { - logger.error(`Failed to deactivate user: ${error.message}`); + logger.error( `Failed to deactivate user: ${ error.message }` ); throw error; } }, - async reactivate(id) { + /** + * Reactivate a user + * @param {number} id - User ID + * @returns {Promise} Updated user instance + * @throws {NotFoundError} If user not found + * @throws {Error} If update fails + */ + async reactivate( id ) { try { - const user = await new (get_user_model())().find_one({ id }, []); - if (!user) throw new NotFoundError('User not found'); + const user = await new (get_user_model())().find_one( { id }, [] ); + if (!user) { // noinspection ExceptionCaughtLocallyJS + throw new NotFoundError( 'User not found' ); + } return await user.reactivate(); } catch (error) { - logger.error(`Failed to reactivate user: ${error.message}`); + logger.error( `Failed to reactivate user: ${ error.message }` ); throw error; } }, - async soft_delete(id, deleted_by_id) { + /** + * Soft delete a user + * @param {number} id - User ID + * @param {number} deleted_by_id - ID of user performing deletion + * @returns {Promise} Updated user instance + * @throws {NotFoundError} If user not found + * @throws {Error} If update fails + */ + async soft_delete( id, deleted_by_id ) { try { - const user = await new (get_user_model())().find_one({ id }, []); - if (!user) throw new NotFoundError('User not found'); - return await user.soft_delete(deleted_by_id); + const user = await new (get_user_model())().find_one( { id }, [] ); + if (!user) { // noinspection ExceptionCaughtLocallyJS + throw new NotFoundError( 'User not found' ); + } + return await user.soft_delete( deleted_by_id ); } catch (error) { - logger.error(`Failed to soft delete user: ${error.message}`); + logger.error( `Failed to soft delete user: ${ error.message }` ); throw error; } }, - instance: () => new (get_user_model())() + instance:() => new (get_user_model())() }, - phone_number: { - async create(phone_data) { + phone_number:{ + /** + * Create a new phone number + * @param {Object} phone_data - Phone number data + * @returns {Promise} Created phone number instance + * @throws {Error} If creation fails + */ + async create( phone_data ) { try { - return await get_phone_number_model().create(phone_data); + return await get_phone_number_model().create( phone_data ); } catch (error) { - logger.error(`Failed to create phone number: ${error.message}`); + logger.error( `Failed to create phone number: ${ error.message }` ); throw error; } }, - async find_by_number(number, excludes) { + /** + * Find phone number by number + * @param {string} number - Phone number to search for + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} Phone number instance or null + * @throws {Error} If the query fails + */ + async find_by_number( number, excludes ) { try { - return await get_phone_number_model().find_by_number(number, excludes); + return await get_phone_number_model().find_by_number( number, excludes ); } catch (error) { - logger.error(`Failed to find phone number: ${error.message}`); + logger.error( `Failed to find phone number: ${ error.message }` ); throw error; } }, - async find_one(where, excludes) { + /** + * Find one phone number + * @param {Object} where - Conditions for the query + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} Phone number instance or null + * @throws {Error} If the query fails + */ + async find_one( where, excludes ) { try { - return await new (get_phone_number_model())().find_one(where, excludes); + return await new (get_phone_number_model())().find_one( where, excludes ); } catch (error) { - logger.error(`Failed to find one phone number: ${error.message}`); + logger.error( `Failed to find one phone number: ${ error.message }` ); throw error; } }, - async find_many(where, excludes, order_by, limit, offset) { + /** + * Find many phone numbers + * @param {Object} [where] - Conditions for the query + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of phone number instances + * @throws {Error} If the query fails + */ + async find_many( where, excludes, order_by, limit, offset ) { try { - return await new (get_phone_number_model())().find_many(where, excludes, order_by, limit, offset); + return await new (get_phone_number_model())().find_many( where, excludes, order_by, limit, offset ); } catch (error) { - logger.error(`Failed to find many phone numbers: ${error.message}`); + logger.error( `Failed to find many phone numbers: ${ error.message }` ); throw error; } }, - instance: () => new (get_phone_number_model())() + instance:() => new (get_phone_number_model())() }, - user_phone_numbers: { - async add_relation(user_id, phone_number_id) { + user_phone_numbers:{ + /** + * Add a user-phone number relation + * @param {number} user_id - User ID + * @param {number} phone_number_id - Phone number ID + * @returns {Promise} Created relation instance + * @throws {Error} If creation fails + */ + async add_relation( user_id, phone_number_id ) { try { - return await get_user_phone_numbers_model().add_relation(user_id, phone_number_id); + return await get_user_phone_numbers_model().add_relation( user_id, phone_number_id ); } catch (error) { - logger.error(`Failed to add user-phone number relation: ${error.message}`); + logger.error( `Failed to add user-phone number relation: ${ error.message }` ); throw error; } }, - async remove_relation(phone_number_id, user_id) { + /** + * Remove a user-phone number relation + * @param {number} phone_number_id - Phone number ID + * @param {number} user_id - User ID + * @returns {Promise} Deleted relation instance or null + * @throws {Error} If deletion fails + */ + async remove_relation( phone_number_id, user_id ) { try { - return await get_user_phone_numbers_model().remove_relation(phone_number_id, user_id); + return await get_user_phone_numbers_model().remove_relation( phone_number_id, user_id ); } catch (error) { - logger.error(`Failed to remove user-phone number relation: ${error.message}`); + logger.error( `Failed to remove user-phone number relation: ${ error.message }` ); throw error; } }, - async find_by_ids(user_id, phone_number_id, excludes) { + /** + * Find user-phone number relation by IDs + * @param {number} user_id - User ID + * @param {number} phone_number_id - Phone number ID + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} Relation instance or null + * @throws {Error} If the query fails + */ + async find_by_ids( user_id, phone_number_id, excludes ) { try { - return await get_user_phone_numbers_model().find_by_ids(user_id, phone_number_id, excludes); + return await get_user_phone_numbers_model().find_by_ids( user_id, phone_number_id, excludes ); } catch (error) { - logger.error(`Failed to find user-phone number by IDs: ${error.message}`); + logger.error( `Failed to find user-phone number by IDs: ${ error.message }` ); throw error; } }, - async find_by_user_id(user_id, excludes, order_by, limit, offset) { + /** + * Find user-phone number relations by user ID + * @param {number} user_id - User ID + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of relations + * @throws {Error} If the query fails + */ + async find_by_user_id( user_id, excludes, order_by, limit, offset ) { try { - return await get_user_phone_numbers_model().find_by_user_id(user_id, excludes, order_by, limit, offset); + return await get_user_phone_numbers_model().find_by_user_id( user_id, excludes, order_by, limit, offset ); } catch (error) { - logger.error(`Failed to find user-phone numbers by user ID: ${error.message}`); + logger.error( `Failed to find user-phone numbers by user ID: ${ error.message }` ); throw error; } }, - instance: () => new (get_user_phone_numbers_model())() + instance:() => new (get_user_phone_numbers_model())() }, - address: { - async create(address_data) { + address:{ + /** + * Create a new address + * @param {Object} address_data - Address data + * @returns {Promise} Created address instance + * @throws {Error} If creation fails + */ + async create( address_data ) { try { - return await get_address_model().create(address_data); + return await get_address_model().create( address_data ); } catch (error) { - logger.error(`Failed to create address: ${error.message}`); + logger.error( `Failed to create address: ${ error.message }` ); throw error; } }, - async find_by_zip_code(zip_code, excludes) { + /** + * Find address by zip code + * @param {string} zip_code - Zip code to search for + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} Address instance or null + * @throws {Error} If the query fails + */ + async find_by_zip_code( zip_code, excludes ) { try { - return await get_address_model().find_by_zip_code(zip_code, excludes); + return await get_address_model().find_by_zip_code( zip_code, excludes ); } catch (error) { - logger.error(`Failed to find address by zip code: ${error.message}`); + logger.error( `Failed to find address by zip code: ${ error.message }` ); throw error; } }, - async find_one(where, excludes) { + /** + * Find one address + * @param {Object} where - Conditions for the query + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} Address instance or null + * @throws {Error} If the query fails + */ + async find_one( where, excludes ) { try { - return await new (get_address_model())().find_one(where, excludes); + return await new (get_address_model())().find_one( where, excludes ); } catch (error) { - logger.error(`Failed to find one address: ${error.message}`); + logger.error( `Failed to find one address: ${ error.message }` ); throw error; } }, - async find_many(where, excludes, order_by, limit, offset) { + /** + * Find many addresses + * @param {Object} [where] - Conditions for the query + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of address instances + * @throws {Error} If the query fails + */ + async find_many( where, excludes, order_by, limit, offset ) { try { - return await new (get_address_model())().find_many(where, excludes, order_by, limit, offset); + return await new (get_address_model())().find_many( where, excludes, order_by, limit, offset ); } catch (error) { - logger.error(`Failed to find many addresses: ${error.message}`); + logger.error( `Failed to find many addresses: ${ error.message }` ); throw error; } }, - instance: () => new (get_address_model())() + instance:() => new (get_address_model())() }, - user_addresses: { - async add_relation(user_id, address_id) { + user_addresses:{ + /** + * Add a user-address relation + * @param {number} user_id - User ID + * @param {number} address_id - Address ID + * @returns {Promise} Created relation instance + * @throws {Error} If creation fails + */ + async add_relation( user_id, address_id ) { try { - return await get_user_addresses_model().add_relation(user_id, address_id); + return await get_user_addresses_model().add_relation( user_id, address_id ); } catch (error) { - logger.error(`Failed to add user-address relation: ${error.message}`); + logger.error( `Failed to add user-address relation: ${ error.message }` ); throw error; } }, - async remove_relation(address_id, user_id) { + /** + * Remove a user-address relation + * @param {number} address_id - Address ID + * @param {number} user_id - User ID + * @returns {Promise} Deleted relation instance or null + * @throws {Error} If deletion fails + */ + async remove_relation( address_id, user_id ) { try { - return await get_user_addresses_model().remove_relation(address_id, user_id); + return await get_user_addresses_model().remove_relation( address_id, user_id ); } catch (error) { - logger.error(`Failed to remove user-address relation: ${error.message}`); + logger.error( `Failed to remove user-address relation: ${ error.message }` ); throw error; } }, - async find_by_ids(user_id, address_id, excludes) { + /** + * Find user-address relation by IDs + * @param {number} user_id - User ID + * @param {number} address_id - Address ID + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} Relation instance or null + * @throws {Error} If the query fails + */ + async find_by_ids( user_id, address_id, excludes ) { try { - return await get_user_addresses_model().find_by_ids(user_id, address_id, excludes); + return await get_user_addresses_model().find_by_ids( user_id, address_id, excludes ); } catch (error) { - logger.error(`Failed to find user-address by IDs: ${error.message}`); + logger.error( `Failed to find user-address by IDs: ${ error.message }` ); throw error; } }, - async find_by_user_id(user_id, excludes, order_by, limit, offset) { + /** + * Find user-address relations by user ID + * @param {number} user_id - User ID + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of relations + * @throws {Error} If the query fails + */ + async find_by_user_id( user_id, excludes, order_by, limit, offset ) { try { - return await get_user_addresses_model().find_by_user_id(user_id, excludes, order_by, limit, offset); + return await get_user_addresses_model().find_by_user_id( user_id, excludes, order_by, limit, offset ); } catch (error) { - logger.error(`Failed to find user-addresses by user ID: ${error.message}`); + logger.error( `Failed to find user-addresses by user ID: ${ error.message }` ); throw error; } }, - instance: () => new (get_user_addresses_model())() + instance:() => new (get_user_addresses_model())() }, - authentication: { - async create(auth_data) { + authentication:{ + /** + * Create a new authentication record + * @param {Object} auth_data - Authentication data + * @returns {Promise} Created authentication instance + * @throws {Error} If creation fails + */ + async create( auth_data ) { try { - return await get_authentication_model().create(auth_data); + return await get_authentication_model().create( auth_data ); } catch (error) { - logger.error(`Failed to create authentication record: ${error.message}`); + logger.error( `Failed to create authentication record: ${ error.message }` ); throw error; } }, - async find_by_user_id(user_id, excludes) { + /** + * Find authentication record by user ID + * @param {number} user_id - User ID + * @param {string[]} [excludes=['password', 'password_salt']] - Fields to exclude + * @returns {Promise} Authentication instance or null + * @throws {Error} If the query fails + */ + async find_by_user_id( user_id, excludes ) { try { - return await get_authentication_model().find_by_user_id(user_id, excludes); + return await get_authentication_model().find_by_user_id( user_id, excludes ); } catch (error) { - logger.error(`Failed to find authentication by user ID: ${error.message}`); + logger.error( `Failed to find authentication by user ID: ${ error.message }` ); throw error; } }, - async find_by_reset_token(token, excludes) { + /** + * Find authentication record by reset token + * @param {string} token - Password reset token + * @param {string[]} [excludes=['password', 'password_salt']] - Fields to exclude + * @returns {Promise} Authentication instance or null + * @throws {Error} If the query fails + */ + async find_by_reset_token( token, excludes ) { try { - return await get_authentication_model().find_by_reset_token(token, excludes); + return await get_authentication_model().find_by_reset_token( token, excludes ); } catch (error) { - logger.error(`Failed to find authentication by reset token: ${error.message}`); + logger.error( `Failed to find authentication by reset token: ${ error.message }` ); throw error; } }, - async find_one(where, excludes) { + /** + * Find one authentication record + * @param {Object} where - Conditions for the query + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} Authentication instance or null + * @throws {Error} If the query fails + */ + async find_one( where, excludes ) { try { - return await new (get_authentication_model())().find_one(where, excludes); + return await new (get_authentication_model())().find_one( where, excludes ); } catch (error) { - logger.error(`Failed to find one authentication record: ${error.message}`); + logger.error( `Failed to find one authentication record: ${ error.message }` ); throw error; } }, - async find_many(where, excludes, order_by, limit, offset) { + /** + * Find many authentication records + * @param {Object} [where] - Conditions for the query + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of authentication instances + * @throws {Error} If the query fails + */ + async find_many( where, excludes, order_by, limit, offset ) { try { - return await new (get_authentication_model())().find_many(where, excludes, order_by, limit, offset); + return await new (get_authentication_model())().find_many( where, excludes, order_by, limit, offset ); } catch (error) { - logger.error(`Failed to find many authentication records: ${error.message}`); + logger.error( `Failed to find many authentication records: ${ error.message }` ); throw error; } }, - async lock_account(id) { + /** + * Lock an authentication account + * @param {number} id - Authentication ID + * @returns {Promise} Updated authentication instance + * @throws {NotFoundError} If record not found + * @throws {Error} If update fails + */ + async lock_account( id ) { try { - const auth = await new (get_authentication_model())().find_one({ id }, []); - if (!auth) throw new NotFoundError('Authentication record not found'); + const auth = await new (get_authentication_model())().find_one( { id }, [] ); + if (!auth) { // noinspection ExceptionCaughtLocallyJS + throw new NotFoundError( 'Authentication record not found' ); + } return await auth.lock_account(); } catch (error) { - logger.error(`Failed to lock account: ${error.message}`); + logger.error( `Failed to lock account: ${ error.message }` ); throw error; } }, - async unlock_account(id) { + /** + * Unlock an authentication account + * @param {number} id - Authentication ID + * @returns {Promise} Updated authentication instance + * @throws {NotFoundError} If record not found + * @throws {Error} If update fails + */ + async unlock_account( id ) { try { - const auth = await new (get_authentication_model())().find_one({ id }, []); - if (!auth) throw new NotFoundError('Authentication record not found'); + const auth = await new (get_authentication_model())().find_one( { id }, [] ); + if (!auth) { // noinspection ExceptionCaughtLocallyJS + throw new NotFoundError( 'Authentication record not found' ); + } return await auth.unlock_account(); } catch (error) { - logger.error(`Failed to unlock account: ${error.message}`); + logger.error( `Failed to unlock account: ${ error.message }` ); + throw error; + } + }, + /** + * Soft delete an authentication record + * @param {number} id - Authentication ID + * @param {number} deleted_by_id - ID of user performing deletion + * @returns {Promise} Updated authentication instance + * @throws {NotFoundError} If record not found + * @throws {Error} If update fails + */ + async soft_delete( id, deleted_by_id ) { + try { + const auth = await new (get_authentication_model())().find_one( { id }, [] ); + if (!auth) { // noinspection ExceptionCaughtLocallyJS + throw new NotFoundError( 'Authentication record not found' ); + } + return await auth.soft_delete( deleted_by_id ); + } catch (error) { + logger.error( `Failed to soft delete authentication: ${ error.message }` ); + throw error; + } + }, + instance:() => new (get_authentication_model())() + }, + role:{ + /** + * Create a new role + * @param {Object} role_data - Role data + * @returns {Promise} Created role instance + * @throws {Error} If creation fails + */ + async create( role_data ) { + try { + return await get_role_model().create( role_data ); + } catch (error) { + logger.error( `Failed to create role: ${ error.message }` ); + throw error; + } + }, + /** + * Find role by name + * @param {string} name - Role name + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} Role instance or null + * @throws {Error} If the query fails + */ + async find_by_name( name, excludes ) { + try { + return await get_role_model().find_by_name( name, excludes ); + } catch (error) { + logger.error( `Failed to find role by name: ${ error.message }` ); + throw error; + } + }, + /** + * Find one role + * @param {Object} where - Conditions for the query + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} Role instance or null + * @throws {Error} If the query fails + */ + async find_one( where, excludes ) { + try { + return await new (get_role_model())().find_one( where, excludes ); + } catch (error) { + logger.error( `Failed to find one role: ${ error.message }` ); throw error; } }, - async soft_delete(id, deleted_by_id) { + /** + * Find many roles + * @param {Object} [where] - Conditions for the query + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of role instances + * @throws {Error} If the query fails + */ + async find_many( where, excludes, order_by, limit, offset ) { try { - const auth = await new (get_authentication_model())().find_one({ id }, []); - if (!auth) throw new NotFoundError('Authentication record not found'); - return await auth.soft_delete(deleted_by_id); + return await new (get_role_model())().find_many( where, excludes, order_by, limit, offset ); } catch (error) { - logger.error(`Failed to soft delete authentication: ${error.message}`); + logger.error( `Failed to find many roles: ${ error.message }` ); throw error; } }, - instance: () => new (get_authentication_model())() + instance:() => new (get_role_model())() }, - role: { - async create(role_data) { + user_roles:{ + /** + * Add a user-role relation + * @param {number} user_id - User ID + * @param {number} role_id - Role ID + * @returns {Promise} Created relation instance + * @throws {Error} If creation fails + */ + async add_relation( user_id, role_id ) { try { - return await get_role_model().create(role_data); + return await get_user_roles_model().add_relation( user_id, role_id ); } catch (error) { - logger.error(`Failed to create role: ${error.message}`); + logger.error( `Failed to add user-role relation: ${ error.message }` ); throw error; } }, - async find_by_name(name, excludes) { + /** + * Remove a user-role relation + * @param {number} role_id - Role ID + * @param {number} user_id - User ID + * @returns {Promise} Deleted relation instance or null + * @throws {Error} If deletion fails + */ + async remove_relation( role_id, user_id ) { try { - return await get_role_model().find_by_name(name, excludes); + return await get_user_roles_model().remove_relation( role_id, user_id ); } catch (error) { - logger.error(`Failed to find role by name: ${error.message}`); + logger.error( `Failed to remove user-role relation: ${ error.message }` ); throw error; } }, - async find_one(where, excludes) { + /** + * Find user-role relation by IDs + * @param {number} user_id - User ID + * @param {number} role_id - Role ID + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} Relation instance or null + * @throws {Error} If the query fails + */ + async find_by_ids( user_id, role_id, excludes ) { try { - return await new (get_role_model())().find_one(where, excludes); + return await get_user_roles_model().find_by_ids( user_id, role_id, excludes ); } catch (error) { - logger.error(`Failed to find one role: ${error.message}`); + logger.error( `Failed to find user-role by IDs: ${ error.message }` ); throw error; } }, - async find_many(where, excludes, order_by, limit, offset) { + /** + * Find user-role relations by user ID + * @param {number} user_id - User ID + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of relations + * @throws {Error} If the query fails + */ + async find_by_user_id( user_id, excludes, order_by, limit, offset ) { try { - return await new (get_role_model())().find_many(where, excludes, order_by, limit, offset); + return await get_user_roles_model().find_by_user_id( user_id, excludes, order_by, limit, offset ); } catch (error) { - logger.error(`Failed to find many roles: ${error.message}`); + logger.error( `Failed to find user-roles by user ID: ${ error.message }` ); throw error; } }, - instance: () => new (get_role_model())() + instance:() => new (get_user_roles_model())() }, - user_roles: { - async add_relation(user_id, role_id) { + file:{ + /** + * Create a new file + * @param {Object} file_data - File data + * @returns {Promise} Created file instance + * @throws {Error} If creation fails + */ + async create( file_data ) { + try { + return await get_file_model().create( file_data ); + } catch (error) { + logger.error( `Failed to create file: ${ error.message }` ); + throw error; + } + }, + /** + * Find file by file path + * @param {string} file_path - File path to search for + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} File instance or null + * @throws {Error} If the query fails + */ + async find_by_file_path( file_path, excludes ) { + try { + return await get_file_model().find_by_file_path( file_path, excludes ); + } catch (error) { + logger.error( `Failed to find file by file path: ${ error.message }` ); + throw error; + } + }, + /** + * Find one file + * @param {Object} where - Conditions for the query + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} File instance or null + * @throws {Error} If the query fails + */ + async find_one( where, excludes ) { + try { + return await new (get_file_model())().find_one( where, excludes ); + } catch (error) { + logger.error( `Failed to find one file: ${ error.message }` ); + throw error; + } + }, + /** + * Find many files + * @param {Object} [where] - Conditions for the query + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of file instances + * @throws {Error} If the query fails + */ + async find_many( where, excludes, order_by, limit, offset ) { + try { + return await new (get_file_model())().find_many( where, excludes, order_by, limit, offset ); + } catch (error) { + logger.error( `Failed to find many files: ${ error.message }` ); + throw error; + } + }, + /** + * Soft delete a file + * @param {number} id - File ID + * @param {number} deleted_by_id - ID of user performing deletion + * @returns {Promise} Updated file instance + * @throws {NotFoundError} If the file is not found + * @throws {Error} If update fails + */ + async soft_delete( id, deleted_by_id ) { try { - return await get_user_roles_model().add_relation(user_id, role_id); + const file = await new (get_file_model())().find_one( { id }, [] ); + if (!file) { // noinspection ExceptionCaughtLocallyJS + throw new NotFoundError( 'File not found' ); + } + return await file.soft_delete( deleted_by_id ); } catch (error) { - logger.error(`Failed to add user-role relation: ${error.message}`); + logger.error( `Failed to soft delete file: ${ error.message }` ); throw error; } }, - async remove_relation(role_id, user_id) { + instance:() => new (get_file_model())() + }, + file_users:{ + /** + * Add a file-user relation + * @param {number} file_id - File ID + * @param {number} user_id - User ID + * @returns {Promise} Created relation instance + * @throws {Error} If creation fails + */ + async add_relation( file_id, user_id ) { try { - return await get_user_roles_model().remove_relation(role_id, user_id); + return await get_file_users_model().add_relation( file_id, user_id ); } catch (error) { - logger.error(`Failed to remove user-role relation: ${error.message}`); + logger.error( `Failed to add file-user relation: ${ error.message }` ); throw error; } }, - async find_by_ids(user_id, role_id, excludes) { + /** + * Remove a file-user relation + * @param {number} file_id - File ID + * @param {number} user_id - User ID + * @returns {Promise} Deleted relation instance or null + * @throws {Error} If deletion fails + */ + async remove_relation( file_id, user_id ) { try { - return await get_user_roles_model().find_by_ids(user_id, role_id, excludes); + return await get_file_users_model().remove_relation( file_id, user_id ); } catch (error) { - logger.error(`Failed to find user-role by IDs: ${error.message}`); + logger.error( `Failed to remove file-user relation: ${ error.message }` ); throw error; } }, - async find_by_user_id(user_id, excludes, order_by, limit, offset) { + /** + * Find file-user relations by file ID + * @param {number} file_id - File ID + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of relations + * @throws {Error} If the query fails + */ + async find_by_file_id( file_id, excludes, order_by, limit, offset ) { try { - return await get_user_roles_model().find_by_user_id(user_id, excludes, order_by, limit, offset); + return await get_file_users_model().find_by_file_id( file_id, excludes, order_by, limit, offset ); } catch (error) { - logger.error(`Failed to find user-roles by user ID: ${error.message}`); + logger.error( `Failed to find file-user relations by file ID: ${ error.message }` ); throw error; } }, - instance: () => new (get_user_roles_model())() + instance:() => new (get_file_users_model())() }, - media: { - async create(media_data) { + post:{ + /** + * Create a new post + * @param {Object} post_data - Post data + * @returns {Promise} Created post instance + * @throws {Error} If creation fails + */ + async create( post_data ) { try { - return await get_media_model().create(media_data); + return await get_post_model().create( post_data ); } catch (error) { - logger.error(`Failed to create media: ${error.message}`); + logger.error( `Failed to create post: ${ error.message }` ); throw error; } }, - async find_by_file_path(file_path, excludes) { + /** + * Find post by title + * @param {string} title - Title to search for + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} Post instance or null + * @throws {Error} If the query fails + */ + async find_by_title( title, excludes ) { try { - return await get_media_model().find_by_file_path(file_path, excludes); + return await get_post_model().find_by_title( title, excludes ); } catch (error) { - logger.error(`Failed to find media by file path: ${error.message}`); + logger.error( `Failed to find post by title: ${ error.message }` ); throw error; } }, - async find_one(where, excludes) { + /** + * Find one post + * @param {Object} where - Conditions for the query + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} Post instance or null + * @throws {Error} If the query fails + */ + async find_one( where, excludes ) { try { - return await new (get_media_model())().find_one(where, excludes); + return await new (get_post_model())().find_one( where, excludes ); } catch (error) { - logger.error(`Failed to find one media: ${error.message}`); + logger.error( `Failed to find one post: ${ error.message }` ); throw error; } }, - async find_many(where, excludes, order_by, limit, offset) { + /** + * Find many posts + * @param {Object} [where] - Conditions for the query + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of post instances + * @throws {Error} If the query fails + */ + async find_many( where, excludes, order_by, limit, offset ) { try { - return await new (get_media_model())().find_many(where, excludes, order_by, limit, offset); + return await new (get_post_model())().find_many( where, excludes, order_by, limit, offset ); } catch (error) { - logger.error(`Failed to find many media: ${error.message}`); + logger.error( `Failed to find many posts: ${ error.message }` ); throw error; } }, - instance: () => new (get_media_model())() + /** + * Soft delete a post + * @param {number} id - Post ID + * @param {number} deleted_by_id - ID of user performing deletion + * @returns {Promise} Updated post instance + * @throws {NotFoundError} If post not found + * @throws {Error} If update fails + */ + async soft_delete( id, deleted_by_id ) { + try { + const post = await new (get_post_model())().find_one( { id }, [] ); + if (!post) { // noinspection ExceptionCaughtLocallyJS + throw new NotFoundError( 'Post not found' ); + } + return await post.soft_delete( deleted_by_id ); + } catch (error) { + logger.error( `Failed to soft delete post: ${ error.message }` ); + throw error; + } + }, + instance:() => new (get_post_model())() }, - post: { - async create(post_data) { + post_files:{ + /** + * Add a post-file relation + * @param {number} post_id - Post ID + * @param {number} file_id - File ID + * @returns {Promise} Created relation instance + * @throws {Error} If creation fails + */ + async add_relation( post_id, file_id ) { try { - return await get_post_model().create(post_data); + return await get_post_files_model().add_relation( post_id, file_id ); } catch (error) { - logger.error(`Failed to create post: ${error.message}`); + logger.error( `Failed to add post-file relation: ${ error.message }` ); throw error; } }, - async find_by_title(title, excludes) { + /** + * Remove a post-file relation + * @param {number} post_id - Post ID + * @param {number} file_id - File ID + * @returns {Promise} Deleted relation instance or null + * @throws {Error} If deletion fails + */ + async remove_relation( post_id, file_id ) { try { - return await get_post_model().find_by_title(title, excludes); + return await get_post_files_model().remove_relation( post_id, file_id ); } catch (error) { - logger.error(`Failed to find post by title: ${error.message}`); + logger.error( `Failed to remove post-file relation: ${ error.message }` ); throw error; } }, - async find_one(where, excludes) { + /** + * Find post-file relations by post ID + * @param {number} post_id - Post ID + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of relations + * @throws {Error} If the query fails + */ + async find_by_post_id( post_id, excludes, order_by, limit, offset ) { try { - return await new (get_post_model())().find_one(where, excludes); + return await get_post_files_model().find_by_post_id( post_id, excludes, order_by, limit, offset ); } catch (error) { - logger.error(`Failed to find one post: ${error.message}`); + logger.error( `Failed to find post-file relations by post ID: ${ error.message }` ); throw error; } }, - async find_many(where, excludes, order_by, limit, offset) { + instance:() => new (get_post_files_model())() + }, + post_users:{ + /** + * Add a post-user relation + * @param {number} post_id - Post ID + * @param {number} user_id - User ID + * @returns {Promise} Created relation instance + * @throws {Error} If creation fails + */ + async add_relation( post_id, user_id ) { try { - return await new (get_post_model())().find_many(where, excludes, order_by, limit, offset); + return await get_post_users_model().add_relation( post_id, user_id ); } catch (error) { - logger.error(`Failed to find many posts: ${error.message}`); + logger.error( `Failed to add post-user relation: ${ error.message }` ); throw error; } }, - instance: () => new (get_post_model())() + /** + * Remove a post-user relation + * @param {number} post_id - Post ID + * @param {number} user_id - User ID + * @returns {Promise} Deleted relation instance or null + * @throws {Error} If deletion fails + */ + async remove_relation( post_id, user_id ) { + try { + return await get_post_users_model().remove_relation( post_id, user_id ); + } catch (error) { + logger.error( `Failed to remove post-user relation: ${ error.message }` ); + throw error; + } + }, + /** + * Find post-user relations by post ID + * @param {number} post_id - Post ID + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of relations + * @throws {Error} If the query fails + */ + async find_by_post_id( post_id, excludes, order_by, limit, offset ) { + try { + return await get_post_users_model().find_by_post_id( post_id, excludes, order_by, limit, offset ); + } catch (error) { + logger.error( `Failed to find post-user relations by post ID: ${ error.message }` ); + throw error; + } + }, + instance:() => new (get_post_users_model())() }, - message_group: { - async create(message_group_data) { + post_reactions:{ + /** + * Add a post reaction + * @param {number} post_id - Post ID + * @param {number} user_id - User ID + * @param {string} reaction - Reaction type + * @returns {Promise} Created or updated reaction instance + * @throws {Error} If creation fails + */ + async add( post_id, user_id, reaction ) { try { - return await get_message_group_model().create(message_group_data); + return await get_post_reactions_model().add( post_id, user_id, reaction ); } catch (error) { - logger.error(`Failed to create message group: ${error.message}`); + logger.error( `Failed to add post reaction: ${ error.message }` ); throw error; } }, - async find_one(where, excludes) { + /** + * Remove a post reaction + * @param {number} post_id - Post ID + * @param {number} user_id - User ID + * @returns {Promise} Deleted reaction instance or null + * @throws {Error} If deletion fails + */ + async remove( post_id, user_id ) { try { - return await new (get_message_group_model())().find_one(where, excludes); + return await get_post_reactions_model().remove( post_id, user_id ); } catch (error) { - logger.error(`Failed to find one message group: ${error.message}`); + logger.error( `Failed to remove post reaction: ${ error.message }` ); throw error; } }, - async find_many(where, excludes, order_by, limit, offset) { + /** + * Find post reactions by post ID + * @param {number} post_id - Post ID + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of reactions + * @throws {Error} If the query fails + */ + async find_by_post_id( post_id, excludes, order_by, limit, offset ) { try { - return await new (get_message_group_model())().find_many(where, excludes, order_by, limit, offset); + return await get_post_reactions_model().find_by_post_id( post_id, excludes, order_by, limit, offset ); } catch (error) { - logger.error(`Failed to find many message groups: ${error.message}`); + logger.error( `Failed to find post reactions by post ID: ${ error.message }` ); throw error; } }, - instance: () => new (get_message_group_model())() + instance:() => new (get_post_reactions_model())() }, - message_group_members: { - async add_relation(group_id, user_id) { + comments:{ + /** + * Create a new comment + * @param {Object} comment_data - Comment data + * @returns {Promise} Created comment instance + * @throws {Error} If creation fails + */ + async create( comment_data ) { try { - return await get_message_group_members_model().add_relation(group_id, user_id); + return await get_comments_model().create( comment_data ); } catch (error) { - logger.error(`Failed to add message group member relation: ${error.message}`); + logger.error( `Failed to create comment: ${ error.message }` ); throw error; } }, - async remove_relation(user_id, group_id) { + /** + * Find comments by post ID + * @param {number} post_id - Post ID + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of comments + * @throws {Error} If the query fails + */ + async find_by_post_id( post_id, excludes, order_by, limit, offset ) { try { - return await get_message_group_members_model().remove_relation(user_id, group_id); + return await get_comments_model().find_by_post_id( post_id, excludes, order_by, limit, offset ); } catch (error) { - logger.error(`Failed to remove message group member relation: ${error.message}`); + logger.error( `Failed to find comments by post ID: ${ error.message }` ); throw error; } }, - async find_by_ids(group_id, user_id, excludes) { + /** + * Find one comment + * @param {Object} where - Conditions for the query + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} Comment instance or null + * @throws {Error} If the query fails + */ + async find_one( where, excludes ) { try { - return await get_message_group_members_model().find_by_ids(group_id, user_id, excludes); + return await new (get_comments_model())().find_one( where, excludes ); } catch (error) { - logger.error(`Failed to find message group member by IDs: ${error.message}`); + logger.error( `Failed to find one comment: ${ error.message }` ); throw error; } }, - async find_by_group_id(group_id, excludes, order_by, limit, offset) { + /** + * Find many comments + * @param {Object} [where] - Conditions for the query + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of comment instances + * @throws {Error} If the query fails + */ + async find_many( where, excludes, order_by, limit, offset ) { try { - return await get_message_group_members_model().find_by_group_id(group_id, excludes, order_by, limit, offset); + return await new (get_comments_model())().find_many( where, excludes, order_by, limit, offset ); } catch (error) { - logger.error(`Failed to find message group members by group ID: ${error.message}`); + logger.error( `Failed to find many comments: ${ error.message }` ); throw error; } }, - instance: () => new (get_message_group_members_model())() + instance:() => new (get_comments_model())() }, - message: { - async create(message_data) { + comment_reactions:{ + /** + * Add a comment reaction + * @param {number} comment_id - Comment ID + * @param {number} user_id - User ID + * @param {string} reaction - Reaction type + * @returns {Promise} Created or updated reaction instance + * @throws {Error} If creation fails + */ + async add( comment_id, user_id, reaction ) { try { - return await get_message_model().create(message_data); + return await get_comment_reactions_model().add( comment_id, user_id, reaction ); } catch (error) { - logger.error(`Failed to create message: ${error.message}`); + logger.error( `Failed to add comment reaction: ${ error.message }` ); throw error; } }, - async find_by_group_id(group_id, excludes, order_by, limit, offset) { + /** + * Remove a comment reaction + * @param {number} comment_id - Comment ID + * @param {number} user_id - User ID + * @returns {Promise} Deleted reaction instance or null + * @throws {Error} If deletion fails + */ + async remove( comment_id, user_id ) { try { - return await get_message_model().find_by_group_id(group_id, excludes, order_by, limit, offset); + return await get_comment_reactions_model().remove( comment_id, user_id ); } catch (error) { - logger.error(`Failed to find messages by group ID: ${error.message}`); + logger.error( `Failed to remove comment reaction: ${ error.message }` ); throw error; } }, - async find_by_recipient_id(recipient_id, excludes, order_by, limit, offset) { + /** + * Find comment reactions by comment ID + * @param {number} comment_id - Comment ID + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of reactions + * @throws {Error} If the query fails + */ + async find_by_comment_id( comment_id, excludes, order_by, limit, offset ) { try { - return await get_message_model().find_by_recipient_id(recipient_id, excludes, order_by, limit, offset); + return await get_comment_reactions_model().find_by_comment_id( comment_id, excludes, order_by, limit, offset ); } catch (error) { - logger.error(`Failed to find messages by recipient ID: ${error.message}`); + logger.error( `Failed to find comment reactions by comment ID: ${ error.message }` ); throw error; } }, - async find_one(where, excludes) { + instance:() => new (get_comment_reactions_model())() + }, + message_group:{ + /** + * Create a new message group + * @param {Object} message_group_data - Message group data + * @returns {Promise} Created message group instance + * @throws {Error} If creation fails + */ + async create( message_group_data ) { try { - return await new (get_message_model())().find_one(where, excludes); + return await get_message_group_model().create( message_group_data ); } catch (error) { - logger.error(`Failed to find one message: ${error.message}`); + logger.error( `Failed to create message group: ${ error.message }` ); throw error; } }, - async find_many(where, excludes, order_by, limit, offset) { + /** + * Find one message group + * @param {Object} where - Conditions for the query + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} Message group instance or null + * @throws {Error} If the query fails + */ + async find_one( where, excludes ) { try { - return await new (get_message_model())().find_many(where, excludes, order_by, limit, offset); + return await new (get_message_group_model())().find_one( where, excludes ); } catch (error) { - logger.error(`Failed to find many messages: ${error.message}`); + logger.error( `Failed to find one message group: ${ error.message }` ); throw error; } }, - async mark_as_read(id) { + /** + * Find many message groups + * @param {Object} [where] - Conditions for the query + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of message group instances + * @throws {Error} If the query fails + */ + async find_many( where, excludes, order_by, limit, offset ) { try { - const message = await new (get_message_model())().find_one({ id }, []); - if (!message) throw new NotFoundError('Message not found'); + return await new (get_message_group_model())().find_many( where, excludes, order_by, limit, offset ); + } catch (error) { + logger.error( `Failed to find many message groups: ${ error.message }` ); + throw error; + } + }, + instance:() => new (get_message_group_model())() + }, + message_group_members:{ + /** + * Add a message group member relation + * @param {number} group_id - Message group ID + * @param {number} user_id - User ID + * @returns {Promise} Created relation instance + * @throws {Error} If creation fails + */ + async add_relation( group_id, user_id ) { + try { + return await get_message_group_members_model().add_relation( group_id, user_id ); + } catch (error) { + logger.error( `Failed to add message group member relation: ${ error.message }` ); + throw error; + } + }, + /** + * Remove a message group member relation + * @param {number} user_id - User ID + * @param {number} [group_id] - Message group ID + * @returns {Promise} Deleted relation instance or null + * @throws {Error} If deletion fails + */ + async remove_relation( user_id, group_id ) { + try { + return await get_message_group_members_model().remove_relation( user_id, group_id ); + } catch (error) { + logger.error( `Failed to remove message group member relation: ${ error.message }` ); + throw error; + } + }, + /** + * Find message group member relation by IDs + * @param {number} group_id - Message group ID + * @param {number} user_id - User ID + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} Relation instance or null + * @throws {Error} If the query fails + */ + async find_by_ids( group_id, user_id, excludes ) { + try { + return await get_message_group_members_model().find_by_ids( group_id, user_id, excludes ); + } catch (error) { + logger.error( `Failed to find message group member by IDs: ${ error.message }` ); + throw error; + } + }, + /** + * Find message group member relations by group ID + * @param {number} group_id - Message group ID + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of relations + * @throws {Error} If the query fails + */ + async find_by_group_id( group_id, excludes, order_by, limit, offset ) { + try { + return await get_message_group_members_model().find_by_group_id( group_id, excludes, order_by, limit, offset ); + } catch (error) { + logger.error( `Failed to find message group members by group ID: ${ error.message }` ); + throw error; + } + }, + instance:() => new (get_message_group_members_model())() + }, + message:{ + /** + * Create a new message + * @param {Object} message_data - Message data + * @returns {Promise} Created message instance + * @throws {Error} If creation fails + */ + async create( message_data ) { + try { + return await get_message_model().create( message_data ); + } catch (error) { + logger.error( `Failed to create message: ${ error.message }` ); + throw error; + } + }, + /** + * Find messages by group ID + * @param {number} group_id - Group ID + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of messages + * @throws {Error} If the query fails + */ + async find_by_group_id( group_id, excludes, order_by, limit, offset ) { + try { + return await get_message_model().find_by_group_id( group_id, excludes, order_by, limit, offset ); + } catch (error) { + logger.error( `Failed to find messages by group ID: ${ error.message }` ); + throw error; + } + }, + /** + * Find messages by recipient ID + * @param {number} recipient_id - Recipient ID + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of messages + * @throws {Error} If the query fails + */ + async find_by_recipient_id( recipient_id, excludes, order_by, limit, offset ) { + try { + return await get_message_model().find_by_recipient_id( recipient_id, excludes, order_by, limit, offset ); + } catch (error) { + logger.error( `Failed to find messages by recipient ID: ${ error.message }` ); + throw error; + } + }, + /** + * Find one message + * @param {Object} where - Conditions for the query + * @param {string[]} [excludes] - Fields to exclude + * @returns {Promise} Message instance or null + * @throws {Error} If the query fails + */ + async find_one( where, excludes ) { + try { + return await new (get_message_model())().find_one( where, excludes ); + } catch (error) { + logger.error( `Failed to find one message: ${ error.message }` ); + throw error; + } + }, + /** + * Find many messages + * @param {Object} [where] - Conditions for the query + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of message instances + * @throws {Error} If the query fails + */ + async find_many( where, excludes, order_by, limit, offset ) { + try { + return await new (get_message_model())().find_many( where, excludes, order_by, limit, offset ); + } catch (error) { + logger.error( `Failed to find many messages: ${ error.message }` ); + throw error; + } + }, + /** + * Mark a message as read + * @param {number} id - Message ID + * @returns {Promise} Updated message instance + * @throws {NotFoundError} If the message is not found + * @throws {Error} If update fails + */ + async mark_as_read( id ) { + try { + const message = await new (get_message_model())().find_one( { id }, [] ); + if (!message) { // noinspection ExceptionCaughtLocallyJS + throw new NotFoundError( 'Message not found' ); + } return await message.mark_as_read(); } catch (error) { - logger.error(`Failed to mark message as read: ${error.message}`); + logger.error( `Failed to mark message as read: ${ error.message }` ); + throw error; + } + }, + instance:() => new (get_message_model())() + }, + message_files:{ + /** + * Add a message-file relation + * @param {number} message_id - Message ID + * @param {number} file_id - File ID + * @returns {Promise} Created relation instance + * @throws {Error} If creation fails + */ + async add_relation( message_id, file_id ) { + try { + return await get_message_files_model().add_relation( message_id, file_id ); + } catch (error) { + logger.error( `Failed to add message-file relation: ${ error.message }` ); + throw error; + } + }, + /** + * Remove a message-file relation + * @param {number} message_id - Message ID + * @param {number} file_id - File ID + * @returns {Promise} Deleted relation instance or null + * @throws {Error} If deletion fails + */ + async remove_relation( message_id, file_id ) { + try { + return await get_message_files_model().remove_relation( message_id, file_id ); + } catch (error) { + logger.error( `Failed to remove message-file relation: ${ error.message }` ); + throw error; + } + }, + /** + * Find message-file relations by message ID + * @param {number} message_id - Message ID + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of relations + * @throws {Error} If the query fails + */ + async find_by_message_id( message_id, excludes, order_by, limit, offset ) { + try { + return await get_message_files_model().find_by_message_id( message_id, excludes, order_by, limit, offset ); + } catch (error) { + logger.error( `Failed to find message-file relations by message ID: ${ error.message }` ); + throw error; + } + }, + instance:() => new (get_message_files_model())() + }, + message_reactions:{ + /** + * Add a message reaction + * @param {number} message_id - Message ID + * @param {number} user_id - User ID + * @param {string} reaction - Reaction type + * @returns {Promise} Created or updated reaction instance + * @throws {Error} If creation fails + */ + async add( message_id, user_id, reaction ) { + try { + return await get_message_reactions_model().add( message_id, user_id, reaction ); + } catch (error) { + logger.error( `Failed to add message reaction: ${ error.message }` ); + throw error; + } + }, + /** + * Remove a message reaction + * @param {number} message_id - Message ID + * @param {number} user_id - User ID + * @returns {Promise} Deleted reaction instance or null + * @throws {Error} If deletion fails + */ + async remove( message_id, user_id ) { + try { + return await get_message_reactions_model().remove( message_id, user_id ); + } catch (error) { + logger.error( `Failed to remove message reaction: ${ error.message }` ); + throw error; + } + }, + /** + * Find message reactions by message ID + * @param {number} message_id - Message ID + * @param {string[]} [excludes] - Fields to exclude + * @param {Object|null} [order_by] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of reactions + * @throws {Error} If the query fails + */ + async find_by_message_id( message_id, excludes, order_by, limit, offset ) { + try { + return await get_message_reactions_model().find_by_message_id( message_id, excludes, order_by, limit, offset ); + } catch (error) { + logger.error( `Failed to find message reactions by message ID: ${ error.message }` ); throw error; } }, - instance: () => new (get_message_model())() + instance:() => new (get_message_reactions_model())() } }; diff --git a/src/models/media.model.js b/src/models/media.model.js deleted file mode 100644 index 586b110..0000000 --- a/src/models/media.model.js +++ /dev/null @@ -1,107 +0,0 @@ -/** - * @file Media model for phase.media table - */ - -const { Model, NotFoundError, ValidationError, FailedToCreateError } = require('./model'); - -/** - * @typedef {Object} Media - * @property {number} id - Media ID (primary key) - * @property {number} user_id - User ID (foreign key) - * @property {string} file_path - File path - * @property {string} file_type - File type (e.g., 'image', 'video') - * @property {string} visibility - Visibility (e.g., 'private', 'family', 'public') - * @property {number|null} created_by_id - ID of user who created this media - * @property {Date} created_at - Creation timestamp - * @property {boolean} is_deleted - Soft delete flag - * @property {number|null} deleted_by_id - ID of user who deleted this media - * @property {Date|null} deleted_at - Deletion timestamp - */ - -/** - * Media model class - * @extends Model - */ -class Media extends Model { - /** - * Create a Media instance - * @param {Partial} [props] - Media properties - */ - constructor(props) { - super(props); - this.table = 'phase.media'; - this.prepend = 'm'; - this.default_columns = [ - 'id', 'user_id', 'file_path', 'file_type', 'visibility', - 'created_by_id', 'created_at', 'is_deleted', 'deleted_by_id', 'deleted_at' - ]; - this.update_exclude_columns = ['id', 'created_at', 'is_deleted', 'deleted_at', 'deleted_by_id']; - this.base_query = ` - SELECT m.id, m.user_id, m.file_path, m.file_type, m.visibility, - m.created_by_id, m.created_at, m.is_deleted, m.deleted_by_id, m.deleted_at - FROM phase.media m - WHERE m.is_deleted = false - `; - this.base_list_query = ` - SELECT m.id, m.user_id, m.file_path, m.file_type, m.visibility, - m.created_by_id, m.created_at - FROM phase.media m - WHERE m.is_deleted = false - `; - this.default_order_by = 'ORDER BY m.created_at DESC'; - this.instance = _props => new Media(_props); - } - - /** - * Create a new media - * @param {Omit} media_data - Media data - * @returns {Promise} Created media instance - * @throws {ValidationError} If required fields are missing - * @throws {FailedToCreateError} If creation fails - */ - static async create(media_data) { - const { user_id, file_path, file_type, visibility = 'private', created_by_id = null } = media_data; - if (!user_id || !file_path || !file_type) { - throw new ValidationError('Missing required fields: user_id, file_path, file_type'); - } - const query_str = ` - INSERT INTO phase.media (user_id, file_path, file_type, visibility, created_by_id) - VALUES ($1, $2, $3, $4, $5) RETURNING *; - `; - const values = [user_id, file_path, file_type, visibility, created_by_id]; - const result = await phsdb.query(query_str, values, { plain: true }); - if (!result) throw new FailedToCreateError('Failed to create media'); - return new Media(result); - } - - /** - * Find media by file path - * @param {string} file_path - File path to search for - * @param {string[]} [excludes] - Fields to exclude from result - * @returns {Promise} Media instance or null - */ - static async find_by_file_path(file_path, excludes = []) { - return await new Media().find_one({ file_path }, excludes); - } - - /** - * Soft delete media - * @param {number|string} deleted_by_id - ID of user performing deletion - * @returns {Promise} Updated media instance - * @throws {ValidationError} If deleted_by_id is invalid - * @throws {NotFoundError} If record not found - */ - async soft_delete(deleted_by_id) { - const deleted_by_id_int = parseInt(deleted_by_id, 10); - if (isNaN(deleted_by_id_int)) { - throw new ValidationError('deleted_by_id must be a valid integer'); - } - return await this.update({ - is_deleted: true, - deleted_at: new Date().toISOString(), - deleted_by_id: deleted_by_id_int - }); - } -} - -module.exports = Media; \ No newline at end of file diff --git a/src/models/message.model.js b/src/models/message.model.js old mode 100644 new mode 100755 index 32ff965..04ca92f --- a/src/models/message.model.js +++ b/src/models/message.model.js @@ -1,8 +1,9 @@ /** * @file Message model for phase.messages table + * @module Message */ -const { Model, NotFoundError, ValidationError, FailedToCreateError } = require('./model'); +const { Model, NotFoundError, ValidationError, FailedToCreateError } = require( './model' ); /** * @typedef {Object} Message @@ -14,6 +15,8 @@ const { Model, NotFoundError, ValidationError, FailedToCreateError } = require(' * @property {boolean} read - Whether the message has been read * @property {Date|null} read_at - Timestamp when the message was read * @property {Date} created_at - Creation timestamp + * @property {Object[]} attached_files - Array of attached file IDs + * @property {Object[]} reactions - Array of message reactions */ /** @@ -25,71 +28,105 @@ class Message extends Model { * Create a Message instance * @param {Partial} [props] - Message properties */ - constructor(props) { - super(props); + constructor( props ) { + super( props ); + /** @type {string} Database table name */ this.table = 'phase.messages'; + /** @type {string} Table alias for queries */ this.prepend = 'm'; - this.default_columns = [ - 'id', 'sender_id', 'group_id', 'recipient_id', 'content', - 'read', 'read_at', 'created_at' - ]; + /** @type {string[]} Allowed columns for queries */ + this.default_columns = ['id', 'sender_id', 'group_id', 'recipient_id', 'content', 'read', 'read_at', 'created_at']; + /** @type {string[]} Columns excluded from updates */ this.update_exclude_columns = ['id', 'created_at']; + /** @type {string} Base query for single record retrieval */ this.base_query = ` - SELECT m.id, m.sender_id, m.group_id, m.recipient_id, m.content, - m.read, m.read_at, m.created_at - FROM phase.messages m + SELECT m.id, + m.sender_id, + m.group_id, + m.recipient_id, + m.content, + m.read, + m.read_at, + m.created_at, + concat(se.first_name, ' ', se.last_name) as sender_name, + concat(re.first_name, ' ', re.last_name) as recipient_name, + concat(mg.name) as group_name, + (SELECT json_agg(json_build_object('file_id', mf.file_id)) + FROM phase.message_files mf + WHERE mf.message_id = m.id) as attached_files, + (SELECT json_agg(json_build_object('user_id', mr.user_id, 'reaction', mr.reaction)) + FROM phase.message_reactions mr + WHERE mr.message_id = m.id) as reactions + FROM phase.messages m + INNER JOIN phase.users se ON se.id = m.sender_id + LEFT OUTER JOIN phase.users re ON re.id = m.recipient_id + LEFT OUTER JOIN phase.message_groups mg ON mg.id = m.group_id `; + /** @type {string} Base query for multiple record retrieval */ this.base_list_query = this.base_query; - this.default_order_by = 'ORDER BY m.created_at DESC'; - this.instance = _props => new Message(_props); + /** @type {string} Default ORDER BY clause */ + this.default_order_by = 'ORDER BY m.created_at ASC'; + /** @type {Function} Function to instantiate a model */ + this.instance = _props => new Message( _props ); } /** * Create a new message - * @param {Omit} message_data - Message data + * @param {Omit & {file_ids?: number[]}} message_data - Message data * @returns {Promise} Created message instance * @throws {ValidationError} If required fields are missing * @throws {FailedToCreateError} If creation fails */ - static async create(message_data) { - const { sender_id, group_id, recipient_id, content } = message_data; + static async create( message_data ) { + const { sender_id, group_id, recipient_id, content, file_ids = [] } = message_data; if (!sender_id || !content || (group_id == null && recipient_id == null)) { - throw new ValidationError('Missing required fields: sender_id, content, and either group_id or recipient_id'); + throw new ValidationError( 'Missing required fields: sender_id, content, and either group_id or recipient_id' ); } - const query_str = ` - INSERT INTO phase.messages (sender_id, group_id, recipient_id, content, read, read_at) - VALUES ($1, $2, $3, $4, FALSE, NULL) RETURNING *; - `; - const values = [sender_id, group_id, recipient_id, content]; - const result = await phsdb.query(query_str, values, { plain: true }); - if (!result) throw new FailedToCreateError('Failed to create message'); - return new Message(result); + return await this.prototype.with_transaction( async ( client ) => { + const query_str = ` + INSERT INTO phase.messages (sender_id, group_id, recipient_id, content, read, read_at) + VALUES ($1, $2, $3, $4, FALSE, NULL) + RETURNING *; + `; + const values = [sender_id, group_id, recipient_id, content]; + const result = await phsdb.client_query( client, query_str, values, { plain:true } ); + if (!result) throw new FailedToCreateError( 'Failed to create message' ); + const message = new Message( result ); + if (file_ids?.length) { + const file_query = `INSERT INTO phase.message_files (message_id, file_id) + VALUES ($1, $2);`; + for (const file_id of file_ids) { + await phsdb.client_query( client, file_query, [message.id, file_id] ); + } + } + return message; + } ); } /** * Find messages by group ID * @param {number|string} group_id - Group ID - * @param {string[]} [excludes] - Fields to exclude from result - * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration + * @param {string[]} [excludes=[]] - Fields to exclude from result + * @param {{name: string, direction?: 'asc'|'desc'}|null} [order_by=null] - Order by configuration * @param {number} [limit=100] - Maximum number of records * @param {number} [offset=0] - Number of records to skip * @returns {Promise} Array of messages */ - static async find_by_group_id(group_id, excludes = [], order_by = null, limit = 100, offset = 0) { - return await new Message().find_many({ group_id }, excludes, order_by, limit, offset); + static async find_by_group_id( group_id, excludes = [], order_by = null, limit = 100, offset = 0 ) { + return await new Message().find_many( { group_id }, excludes, order_by, limit, offset ); } /** * Find messages by recipient ID * @param {number|string} recipient_id - Recipient ID - * @param {string[]} [excludes] - Fields to exclude from result - * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration + * @param {string[]} [excludes=[]] - Fields to exclude from result + * @param {{name: string, direction?: 'asc'|'desc'}|null} [order_by=null] - Order by configuration * @param {number} [limit=100] - Maximum number of records * @param {number} [offset=0] - Number of records to skip * @returns {Promise} Array of messages */ - static async find_by_recipient_id(recipient_id, excludes = [], order_by = null, limit = 100, offset = 0) { - return await new Message().find_many({ recipient_id }, excludes, order_by, limit, offset); + static async find_by_recipient_id( recipient_id, excludes = [], order_by = null, limit = 100, offset = 0 ) { + return await new Message().find_many( { recipient_id }, excludes, order_by, limit, offset ); } /** @@ -98,10 +135,7 @@ class Message extends Model { * @throws {NotFoundError} If record not found */ async mark_as_read() { - return await this.update({ - read: true, - read_at: new Date().toISOString() - }); + return await this.update( { read:true, read_at:new Date().toISOString() } ); } } diff --git a/src/models/message_files.model.js b/src/models/message_files.model.js new file mode 100755 index 0000000..5ed9955 --- /dev/null +++ b/src/models/message_files.model.js @@ -0,0 +1,104 @@ +/** + * @file Message files model for phase.message_files table + * @module MessageFile + */ + +const { Model, ValidationError, FailedToCreateError } = require( './model' ); + +/** + * @typedef {Object} MessageFile + * @property {number} message_id - Message ID (foreign key to messages) + * @property {number} file_id - File ID (foreign key to files) + */ + +/** + * Message files model class + * @extends Model + */ +class MessageFile extends Model { + /** + * Create a MessageFile instance + * @param {Partial} [props] - Message file properties + */ + constructor( props ) { + super( props ); + /** @type {string} Database table name */ + this.table = 'phase.message_files'; + /** @type {string} Table alias for queries */ + this.prepend = 'mf'; + /** @type {string[]} Allowed columns for queries */ + this.default_columns = [ 'message_id', 'file_id' ]; + /** @type {string[]} Columns excluded from updates */ + this.update_exclude_columns = [ 'message_id', 'file_id' ]; + /** @type {string} Base query for single record retrieval */ + this.base_query = `SELECT mf.message_id, mf.file_id + FROM phase.message_files mf`; + /** @type {string} Base query for multiple record retrieval */ + this.base_list_query = this.base_query; + /** @type {string} Default ORDER BY clause */ + this.default_order_by = 'ORDER BY mf.message_id ASC'; + /** @type {Function} Function to instantiate a model */ + this.instance = _props => new MessageFile( _props ); + } + + /** + * Add a message-file relation + * @param {number|string} message_id - Message ID + * @param {number|string} file_id - File ID + * @returns {Promise} Created relation instance + * @throws {ValidationError} If IDs are invalid + * @throws {FailedToCreateError} If creation fails + */ + static async add_relation( message_id, file_id ) { + const message_id_int = parseInt( message_id, 10 ); + const file_id_int = parseInt( file_id, 10 ); + if ( isNaN( message_id_int ) || isNaN( file_id_int ) ) { + throw new ValidationError( 'message_id and file_id must be valid integers' ); + } + const query_str = `INSERT INTO phase.message_files (message_id, file_id) + VALUES ($1, $2) + RETURNING *;`; + const values = [ message_id_int, file_id_int ]; + const result = await phsdb.query( query_str, values, { plain: true } ); + if ( !result ) throw new FailedToCreateError( 'Failed to add message-file relation' ); + return new MessageFile( result ); + } + + /** + * Remove a message-file relation + * @param {number|string} message_id - Message ID + * @param {number|string} file_id - File ID + * @returns {Promise} Deleted relation instance or null + * @throws {ValidationError} If IDs are invalid + */ + static async remove_relation( message_id, file_id ) { + const message_id_int = parseInt( message_id, 10 ); + const file_id_int = parseInt( file_id, 10 ); + if ( isNaN( message_id_int ) || isNaN( file_id_int ) ) { + throw new ValidationError( 'message_id and file_id must be valid integers' ); + } + const query_str = `DELETE + FROM phase.message_files + WHERE message_id = $1 + AND file_id = $2 + RETURNING *;`; + const values = [ message_id_int, file_id_int ]; + const result = await phsdb.query( query_str, values, { plain: true } ); + return result ? new MessageFile( result ) : null; + } + + /** + * Find message-file relations by message ID + * @param {number|string} message_id - Message ID + * @param {string[]} [excludes=[]] - Fields to exclude from result + * @param {{name: string, direction?: 'asc'|'desc'}|null} [order_by=null] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of relations + */ + static async find_by_message_id( message_id, excludes = [], order_by = null, limit = 100, offset = 0 ) { + return await new MessageFile().find_many( { message_id }, excludes, order_by, limit, offset ); + } +} + +module.exports = MessageFile; \ No newline at end of file diff --git a/src/models/message_group.model.js b/src/models/message_group.model.js old mode 100644 new mode 100755 diff --git a/src/models/message_group_members.model.js b/src/models/message_group_members.model.js old mode 100644 new mode 100755 diff --git a/src/models/message_reactions.model.js b/src/models/message_reactions.model.js new file mode 100755 index 0000000..ee3aad3 --- /dev/null +++ b/src/models/message_reactions.model.js @@ -0,0 +1,109 @@ +/** + * @file Message reactions model for phase.message_reactions table + * @module MessageReaction + */ + +const { Model, ValidationError, FailedToCreateError } = require( './model' ); + +/** + * @typedef {Object} MessageReaction + * @property {number} message_id - Message ID (foreign key to messages) + * @property {number} user_id - User ID (foreign key to users) + * @property {string} reaction - Reaction type (e.g., 'like', 'heart', 'laugh') + */ + +/** + * Message reactions model class + * @extends Model + */ +class MessageReaction extends Model { + /** + * Create a MessageReaction instance + * @param {Partial} [props] - Message reaction properties + */ + constructor( props ) { + super( props ); + /** @type {string} Database table name */ + this.table = 'phase.message_reactions'; + /** @type {string} Table alias for queries */ + this.prepend = 'mr'; + /** @type {string[]} Allowed columns for queries */ + this.default_columns = [ 'message_id', 'user_id', 'reaction' ]; + /** @type {string[]} Columns excluded from updates */ + this.update_exclude_columns = [ 'message_id', 'user_id' ]; + /** @type {string} Base query for single record retrieval */ + this.base_query = `SELECT mr.message_id, mr.user_id, mr.reaction + FROM phase.message_reactions mr`; + /** @type {string} Base query for multiple record retrieval */ + this.base_list_query = this.base_query; + /** @type {string} Default ORDER BY clause */ + this.default_order_by = 'ORDER BY mr.message_id ASC'; + /** @type {Function} Function to instantiate a model */ + this.instance = _props => new MessageReaction( _props ); + } + + /** + * Add a message reaction + * @param {number|string} message_id - Message ID + * @param {number|string} user_id - User ID + * @param {string} reaction - Reaction type + * @returns {Promise} Created or updated reaction instance + * @throws {ValidationError} If required fields are missing + * @throws {FailedToCreateError} If creation fails + */ + static async add( message_id, user_id, reaction ) { + const message_id_int = parseInt( message_id, 10 ); + const user_id_int = parseInt( user_id, 10 ); + if ( isNaN( message_id_int ) || isNaN( user_id_int ) || !reaction ) { + throw new ValidationError( 'message_id, user_id, and reaction are required' ); + } + const query_str = ` + INSERT INTO phase.message_reactions (message_id, user_id, reaction) + VALUES ($1, $2, $3) + ON CONFLICT (message_id, user_id) DO UPDATE SET reaction = EXCLUDED.reaction + RETURNING *; + `; + const values = [ message_id_int, user_id_int, reaction ]; + const result = await phsdb.query( query_str, values, { plain: true } ); + if ( !result ) throw new FailedToCreateError( 'Failed to add message reaction' ); + return new MessageReaction( result ); + } + + /** + * Remove a message reaction + * @param {number|string} message_id - Message ID + * @param {number|string} user_id - User ID + * @returns {Promise} Deleted reaction instance or null + * @throws {ValidationError} If IDs are invalid + */ + static async remove( message_id, user_id ) { + const message_id_int = parseInt( message_id, 10 ); + const user_id_int = parseInt( user_id, 10 ); + if ( isNaN( message_id_int ) || isNaN( user_id_int ) ) { + throw new ValidationError( 'message_id and user_id must be valid integers' ); + } + const query_str = `DELETE + FROM phase.message_reactions + WHERE message_id = $1 + AND user_id = $2 + RETURNING *;`; + const values = [ message_id_int, user_id_int ]; + const result = await phsdb.query( query_str, values, { plain: true } ); + return result ? new MessageReaction( result ) : null; + } + + /** + * Find message reactions by message ID + * @param {number|string} message_id - Message ID + * @param {string[]} [excludes=[]] - Fields to exclude from result + * @param {{name: string, direction?: 'asc'|'desc'}|null} [order_by=null] - Order by configuration + * @param {number} [limit=100] - Maximum number of records + * @param {number} [offset=0] - Number of records to skip + * @returns {Promise} Array of reactions + */ + static async find_by_message_id( message_id, excludes = [], order_by = null, limit = 100, offset = 0 ) { + return await new MessageReaction().find_many( { message_id }, excludes, order_by, limit, offset ); + } +} + +module.exports = MessageReaction; \ No newline at end of file diff --git a/src/models/model.js b/src/models/model.js index 2c6d48d..776e093 100755 --- a/src/models/model.js +++ b/src/models/model.js @@ -48,12 +48,28 @@ class ValidationError extends HttpError { /** * Base model class for database operations + * @class + * @method where_clause - Build WHERE clause + * @method build_where - Filter WHERE object to valid columns + * @method get_base_list_query - Get base list query + * @method create_update_fields - Create update fields string and values + * @method instance - Function to instantiate a model + * @method find_one - Find one record + * @method find_many - Find many records + * @method update - Update the record + * @method toJSON - Serialize to JSON + * @method with_transaction - Execute transaction + * @property {string} table - Database table name + * @property {string[]} default_columns - Allowed columns for queries + * @property {string[]} update_exclude_columns - Columns excluded from updates + * @property {string} prepend - Table alias for queries + * @property {string} base_query - Base query for single record retrieval + * @property {string} base_list_query - Base query for multiple record retrieval + * @property {string|undefined} default_order_by - Default ORDER BY clause + * @property {Function} instance - Function to instantiate a model + * @property {string|undefined} group_by - GROUP BY clause */ class Model { - /** - * Create a model instance - * @param {Object} [props] - Properties to initialize the model - */ constructor( props ) { props && Object.keys( props ).forEach( c => { this[c] = props[c]; @@ -72,7 +88,7 @@ class Model { this.base_list_query = ''; /** @type {string|undefined} Default ORDER BY clause */ this.default_order_by = undefined; - /** @type {Function} Function to instantiate a model */ + /** @type {Function} Function to instantiate a model*/ this.instance = _props => new Model( _props ); /** @type {string|undefined} GROUP BY clause */ this.group_by = undefined; @@ -113,7 +129,7 @@ class Model { * Find one record * @param {Object} where - Conditions for the WHERE clause * @param {string[]} [excludes] - Fields to exclude from result - * @returns {Promise} Found record or null + * @returns {Promise} Found record or null */ async find_one( where, excludes = [] ) { const { keys, values } = this.build_where( where, this.default_columns ); @@ -128,24 +144,6 @@ class Model { return found; }; - /** - * Find one record (raw data) - * @param {Object} where - Conditions for the WHERE clause - * @param {string[]} [excludes] - Fields to exclude from result - * @returns {Promise} Found record or null - */ - async find_one_simple( where, excludes = [] ) { - const { keys, values } = this.build_where( where, this.default_columns ); - const result = await phsdb.query( - `${ this.base_query } ${ this.where_clause( keys, this.prepend ) } ${ this.group_by ? this.group_by : '' }`, - values, - { plain:true } - ); - if (!result) return null; - excludes?.forEach( e => delete result[e] ); - return result; - }; - /** * Get base list query * @returns {string} Base list query @@ -185,43 +183,11 @@ class Model { return res; }; - /** - * Find many records (raw data) - * @param {Object} [where] - Conditions for the WHERE clause - * @param {string[]} [excludes] - Fields to exclude from result - * @param {{name: string, direction?: 'asc'|'desc'}||null} [order_by] - Order by configuration - * @param {number} [limit=100] - Maximum number of records - * @param {number} [offset=0] - Number of records to skip - * @returns {Promise} Array of records - */ - async find_many_lite( where, excludes = [], order_by = null, limit = 100, offset = 0 ) { - const { keys, values } = this.build_where( where, this.default_columns ); - values.push( limit, offset ); - const results = await phsdb.query( - ` - ${ this.get_base_list_query() } - ${ this.where_clause( keys, this.prepend ) } - ${ this.group_by ? this.group_by : '' } - ${ order_by ? `ORDER BY ${ order_by.name } ${ order_by.direction ?? 'asc' }` : this.default_order_by ?? '' } - LIMIT $${ values.length - 1 } OFFSET $${ values.length } - `, - values - ); - const res = []; - for (const result of results) { - const data_values = {}; - Object.keys( result ).forEach( k => data_values[k] = result[k] ); - excludes?.forEach( e => delete data_values[e] ); - res.push( data_values ); - } - return res; - }; - /** * Update the record * @param {Object} params - Fields to update * @param {string} [identifier='id'] - Identifier field for update - * @returns {Promise} Updated record + * @returns {Promise} Updated record * @throws {ValidationError} If no valid fields are provided or ID is missing * @throws {NotFoundError} If record is not found */ diff --git a/src/models/phone_number.model.js b/src/models/phone_number.model.js old mode 100644 new mode 100755 diff --git a/src/models/post.model.js b/src/models/post.model.js old mode 100644 new mode 100755 diff --git a/src/models/post_files.model.js b/src/models/post_files.model.js old mode 100644 new mode 100755 diff --git a/src/models/post_reactions.model.js b/src/models/post_reactions.model.js old mode 100644 new mode 100755 diff --git a/src/models/post_users.model.js b/src/models/post_users.model.js old mode 100644 new mode 100755 diff --git a/src/models/role.model.js b/src/models/role.model.js old mode 100644 new mode 100755 diff --git a/src/models/user.model.js b/src/models/user.model.js old mode 100644 new mode 100755 index 116af0f..1de91c5 --- a/src/models/user.model.js +++ b/src/models/user.model.js @@ -1,13 +1,15 @@ +// noinspection JSUnusedGlobalSymbols + /** * @file User model for phase.users table */ -const db = require('../models'); -const bcrypt = require('bcrypt'); -const jwt = require('jsonwebtoken'); -const config = require('../config/default.json'); -const { Model, ValidationError } = require('./model'); -const createHttpError = require('http-errors'); +const db = require( '../models' ); +const bcrypt = require( 'bcrypt' ); +const jwt = require( 'jsonwebtoken' ); +const config = require( '../config/default.json' ); +const { Model, ValidationError } = require( './model' ); +const createHttpError = require( 'http-errors' ); /** * @typedef {Object} User @@ -33,8 +35,8 @@ const createHttpError = require('http-errors'); * @extends Model */ class User extends Model { - constructor(props) { - super(props); + constructor( props ) { + super( props ); this.table = 'phase.users'; this.prepend = 'u'; this.default_columns = [ @@ -44,36 +46,71 @@ class User extends Model { ]; this.update_exclude_columns = ['id', 'created_at', 'is_deleted', 'deleted_at', 'deleted_by_id']; this.base_query = ` - SELECT u.id, u.email, u.first_name, u.middle_name, u.last_name, u.initials, u.nickname, - u.created_by_id, u.created_at, u.is_deleted, u.deleted_by_id, u.deleted_at, - u.is_active, u.deactivated_by_id, u.deactivated_at, a.password, a.password_salt, - a.is_locked, a.locked_date, - (select json_agg(r) from phase.user_roles ur inner join phase.roles r on ur.role_id = r.id ) as roles + SELECT u.id, + u.email, + u.first_name, + u.middle_name, + u.last_name, + u.initials, + u.nickname, + u.created_by_id, + u.created_at, + u.is_deleted, + u.deleted_by_id, + u.deleted_at, + u.is_active, + u.deactivated_by_id, + u.deactivated_at, + a.password, + a.password_salt, + a.is_locked, + a.locked_date, + (select json_agg(r) + from phase.roles r + inner join phase.user_roles ur on r.id = ur.role_id + where ur.user_id = u.id) as roles FROM phase.users u - inner join phase.authentication a on u.id = a.user_id + inner join phase.authentication a on u.id = a.user_id `; this.base_list_query = ` - SELECT u.id, u.email, u.first_name, u.middle_name, u.last_name, u.initials, u.nickname, - u.created_by_id, u.created_at, u.is_active, u.deactivated_by_id, u.deactivated_at, a.password, a.password_salt, - a.is_locked, a.locked_date + SELECT u.id, + u.email, + u.first_name, + u.middle_name, + u.last_name, + u.initials, + u.nickname, + u.created_by_id, + u.created_at, + u.is_active, + u.deactivated_by_id, + u.deactivated_at, + a.password, + a.password_salt, + a.is_locked, + a.locked_date, + (select json_agg(r) + from phase.roles r + inner join phase.user_roles ur on r.id = ur.role_id + where ur.user_id = u.id) as roles FROM phase.users u inner join phase.authentication a on u.id = a.user_id `; this.default_order_by = 'ORDER BY u.email ASC'; - this.instance = _props => new User(_props); + this.instance = _props => new User( _props ); this.toJSON = () => { const { password, password_salt, ...safe_data } = super.toJSON(); return safe_data; }; }; - static async create(user_data) { + static async create( user_data ) { const { email, first_name, middle_name = '', last_name, initials = null, nickname = null, created_by_id = null, is_active = true, deactivated_by_id = null, deactivated_at = null } = user_data; if (!email || !first_name || !last_name) { - throw new ValidationError('Missing required fields: email, first_name, last_name'); + throw new ValidationError( 'Missing required fields: email, first_name, last_name' ); } const query_str = ` INSERT INTO phase.users (email, first_name, middle_name, last_name, initials, nickname, created_by_id, @@ -85,141 +122,142 @@ class User extends Model { email, first_name, middle_name, last_name, initials, nickname, created_by_id, is_active, deactivated_by_id, deactivated_at ]; - const result = await phsdb.query(query_str, values, { plain: true }); - if (!result) throw new ValidationError('Failed to create user'); - return new User(result); - } - - async get_user_roles() { - const query_str = ` - SELECT r.name - FROM phase.user_roles ur - INNER JOIN phase.roles r ON r.id = ur.role_id - WHERE ur.user_id = $1 AND r.is_deleted = false - `; - const result = await phsdb.query(query_str, [this.id]); - return result.map(r => r.name); + const result = await phsdb.query( query_str, values, { plain:true } ); + if (!result) throw new ValidationError( 'Failed to create user' ); + return new User( result ); } - static async find_by_email(email, excludes = []) { - return await new User().find_one({ email }, excludes); + static async find_by_email( email, excludes = [] ) { + return await new User().find_one( { email }, excludes ); } - static async find_by_nickname(nickname, excludes = []) { - return await new User().find_one({ nickname }, excludes); + static async find_by_nickname( nickname, excludes = [] ) { + return await new User().find_one( { nickname }, excludes ); } - static async find_active(excludes = [], order_by = null, limit = 100, offset = 0) { - return await new User().find_many({ is_active: true }, excludes, order_by, limit, offset); + static async find_active( excludes = [], order_by = null, limit = 100, offset = 0 ) { + return await new User().find_many( { is_active:true }, excludes, order_by, limit, offset ); } - static async find_deleted(excludes = [], order_by = null, limit = 100, offset = 0) { - const query_str = this.prototype.base_list_query.replace('WHERE u.is_deleted = false', 'WHERE u.is_deleted = true'); + static async find_deleted( excludes = [], order_by = null, limit = 100, offset = 0 ) { + const query_str = this.prototype.base_list_query.replace( 'WHERE u.is_deleted = false', 'WHERE u.is_deleted = true' ); const instance = this.prototype.instance; - const { keys, values } = this.prototype.build_where({}, this.prototype.default_columns); - values.push(limit, offset); + const { keys, values } = this.prototype.build_where( {}, this.prototype.default_columns ); + values.push( limit, offset ); const results = await phsdb.query( ` - ${query_str} - ${this.prototype.where_clause(keys, this.prototype.prepend)} - ${order_by ? `ORDER BY ${order_by.name} ${order_by.direction ?? 'asc'}` : this.prototype.default_order_by ?? ''} - LIMIT $${values.length - 1} OFFSET $${values.length} + ${ query_str } + ${ this.prototype.where_clause( keys, this.prototype.prepend ) } + ${ order_by ? `ORDER BY ${ order_by.name } ${ order_by.direction ?? 'asc' }` : this.prototype.default_order_by ?? '' } + LIMIT $${ values.length - 1 } OFFSET $${ values.length } `, values ); - return results.map(result => { - const found = instance(result); - excludes?.forEach(e => delete found[e]); + return results.map( result => { + const found = instance( result ); + excludes?.forEach( e => delete found[e] ); return found; - }); + } ); + } + + async get_user_roles() { + const query_str = ` + SELECT r.name + FROM phase.user_roles ur + INNER JOIN phase.roles r ON r.id = ur.role_id + WHERE ur.user_id = $1 + AND r.is_deleted = false + `; + const result = await phsdb.query( query_str, [this.id] ); + return result.map( r => r.name ); } - async deactivate(deactivated_by_id) { - const deactivated_by_id_int = parseInt(deactivated_by_id, 10); - if (isNaN(deactivated_by_id_int)) { - throw new ValidationError('deactivated_by_id must be a valid integer'); + async deactivate( deactivated_by_id ) { + const deactivated_by_id_int = parseInt( deactivated_by_id, 10 ); + if (isNaN( deactivated_by_id_int )) { + throw new ValidationError( 'deactivated_by_id must be a valid integer' ); } - return await this.update({ - is_active: false, - deactivated_at: new Date().toISOString(), - deactivated_by_id: deactivated_by_id_int - }); + return await this.update( { + is_active:false, + deactivated_at:new Date().toISOString(), + deactivated_by_id:deactivated_by_id_int + } ); } async reactivate() { - return await this.update({ - is_active: true, - deactivated_at: null, - deactivated_by_id: null - }); + return await this.update( { + is_active:true, + deactivated_at:null, + deactivated_by_id:null + } ); } - async comparePassword(password) { - return password ? bcrypt.compareSync(password, this.password) : false; + async comparePassword( password ) { + return password ? bcrypt.compareSync( password, this.password ) : false; } async createToken() { - const auth = await db.authentication.find_by_user_id(this.id); + const auth = await db.authentication.find_by_user_id( this.id ); const roles = await this.get_user_roles(); - const tokenPayload = { id: this.id, email: this.email, roles }; - const token = jwt.sign(tokenPayload, config.keys.secret, { expiresIn: '24h' }); - const { exp } = jwt.decode(token); - const token_expiry = new Date(exp * 1000).toISOString(); - await auth.update({ - password_failures_since_last_success: 0, - password_verification_token: token, - password_verification_token_expiry: token_expiry - }); + const tokenPayload = { id:this.id, email:this.email, roles }; + const token = jwt.sign( tokenPayload, config.keys.secret, { expiresIn:'24h' } ); + const { exp } = jwt.decode( token ); + const token_expiry = new Date( exp * 1000 ).toISOString(); + await auth.update( { + password_failures_since_last_success:0, + password_verification_token:token, + password_verification_token_expiry:token_expiry + } ); return token; } async failLogin() { - const auth = await db.authentication.find_by_user_id(this.id); - await auth.update({ - password_failures_since_last_success: (auth.password_failures_since_last_success || 0) + 1, - password_verification_token: null, - password_verification_token_expiry: null, - last_password_failure: new Date().toISOString() - }); + const auth = await db.authentication.find_by_user_id( this.id ); + await auth.update( { + password_failures_since_last_success:(auth.password_failures_since_last_success || 0) + 1, + password_verification_token:null, + password_verification_token_expiry:null, + last_password_failure:new Date().toISOString() + } ); return true; } async lockAccount() { - const auth = await db.authentication.find_by_user_id(this.id); - await auth.update({ - is_locked: true, - locked_date: new Date().toISOString() - }); + const auth = await db.authentication.find_by_user_id( this.id ); + await auth.update( { + is_locked:true, + locked_date:new Date().toISOString() + } ); return true; } - async hashPassword(password) { - const auth = await db.authentication.find_by_user_id(this.id); - if (!auth) throw createHttpError(400, 'No authentication record found for this user.'); - const salt = await bcrypt.genSalt(10); - const hash = await bcrypt.hash(password, salt); - await auth.update({ - password_failures_since_last_success: 0, - password_changed_date: new Date().toISOString(), - password: hash, - password_salt: salt, - ms_password: false, - password_reset_token: null, - password_reset_expire_date: null - }); + async hashPassword( password ) { + const auth = await db.authentication.find_by_user_id( this.id ); + if (!auth) throw createHttpError( 400, 'No authentication record found for this user.' ); + const salt = await bcrypt.genSalt( 10 ); + const hash = await bcrypt.hash( password, salt ); + await auth.update( { + password_failures_since_last_success:0, + password_changed_date:new Date().toISOString(), + password:hash, + password_salt:salt, + ms_password:false, + password_reset_token:null, + password_reset_expire_date:null + } ); return { salt, hash }; } - async soft_delete(deleted_by_id) { - const deleted_by_id_int = parseInt(deleted_by_id, 10); - if (isNaN(deleted_by_id_int)) { - throw new ValidationError('deleted_by_id must be a valid integer'); + async soft_delete( deleted_by_id ) { + const deleted_by_id_int = parseInt( deleted_by_id, 10 ); + if (isNaN( deleted_by_id_int )) { + throw new ValidationError( 'deleted_by_id must be a valid integer' ); } - return await this.update({ - is_deleted: true, - deleted_at: new Date().toISOString(), - deleted_by_id: deleted_by_id_int - }); + return await this.update( { + is_deleted:true, + deleted_at:new Date().toISOString(), + deleted_by_id:deleted_by_id_int + } ); } is_active() { diff --git a/src/models/user_addresses.model.js b/src/models/user_addresses.model.js old mode 100644 new mode 100755 diff --git a/src/models/user_phone_numbers.model.js b/src/models/user_phone_numbers.model.js old mode 100644 new mode 100755 diff --git a/src/models/user_roles.model.js b/src/models/user_roles.model.js old mode 100644 new mode 100755 diff --git a/src/phsdb.js b/src/phsdb.js index a4f8fb9..1f689bf 100755 --- a/src/phsdb.js +++ b/src/phsdb.js @@ -23,7 +23,7 @@ const json_slimify = (key, value) => { // noinspection JSUnusedLocalSymbols const log_query = (text, values, options={}) => { try{ - // logger.debug(`log_query: ${text} -- With values: ${JSON.stringify(values,json_slimify)}`); // Incase you need to see the values and properties. + // logger.debug(`log_query: ${text} -- With values: ${JSON.stringify(values,json_slimify)}`); // In case you need to see the values and properties. const formattedQuery = pgp.as.format(text, values); logger.debug( `[Executing Query] ${formattedQuery} -- With values: ${JSON.stringify( diff --git a/src/routes/address.routes.js b/src/routes/address.routes.js old mode 100644 new mode 100755 diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js old mode 100644 new mode 100755 index 54078d4..2c19106 --- a/src/routes/auth.routes.js +++ b/src/routes/auth.routes.js @@ -1,8 +1,9 @@ -const express = require('express'); +const express = require( 'express' ); const router = express.Router(); -const authController = require('../controllers/auth.controller'); +const authController = require( '../controllers/auth.controller' ); module.exports = () => { - router.post('/authenticate', authController.authenticate); - return router; + router.post( '/authenticate', authController.authenticate ); + router.get( '/logout', authController.logout ); + return router; }; \ No newline at end of file diff --git a/src/routes/authentication.routes.js b/src/routes/authentication.routes.js old mode 100644 new mode 100755 diff --git a/src/routes/docker.routes.js b/src/routes/docker.routes.js old mode 100644 new mode 100755 diff --git a/src/routes/file_users.routes.js b/src/routes/file_users.routes.js new file mode 100755 index 0000000..745efa2 --- /dev/null +++ b/src/routes/file_users.routes.js @@ -0,0 +1,21 @@ +/** + * @file File users routes configuration + * @module FileUsersRoutes + */ + +const express = require( 'express' ); +const router = express.Router(); +const { validate_auth, restrictToRoles } = require( '../middleware/routeHelpers' ); +const file_users_controller = require( '../controllers/file_users.controller' ); + +/** + * Configure file users routes + * @param {Object} passport - Passport instance for authentication + * @returns {Object} Express router with file users routes + */ +module.exports = ( passport ) => { + router.post( '/add', validate_auth( passport ), restrictToRoles( [ 'admin', 'family' ] ), file_users_controller.add_relation ); + router.delete( '/:file_id/:user_id', validate_auth( passport ), restrictToRoles( [ 'admin', 'family' ] ), file_users_controller.remove_relation ); + router.get( '/file/:file_id', validate_auth( passport ), file_users_controller.find_by_file_id ); + return router; +}; \ No newline at end of file diff --git a/src/routes/files.routes.js b/src/routes/files.routes.js new file mode 100755 index 0000000..57cfc90 --- /dev/null +++ b/src/routes/files.routes.js @@ -0,0 +1,15 @@ +const express = require( 'express' ); +const router = express.Router(); +const { validate_auth, restrictToRoles } = require( '../middleware/routeHelpers' ); +const files_controller = require( '../controllers/files.controller' ); + +module.exports = ( passport ) => { + router.post( '/create', files_controller.uploadFile, validate_auth( passport ), files_controller.create ); + router.get( '/file_path/:file_path', validate_auth( passport ), files_controller.find_by_file_path ); + router.get( '/:id', validate_auth( passport ), files_controller.find_one ); + router.get( '/', validate_auth( passport ), files_controller.find_many ); + router.put( '/:id', validate_auth( passport ), restrictToRoles( ['admin', 'family'] ), files_controller.update ); + router.put( '/:id/soft_delete', validate_auth( passport ), restrictToRoles( ['admin', 'family'] ), files_controller.soft_delete ); + router.get( '/:id/view', validate_auth( passport ), files_controller.get_file ); + return router; +}; \ No newline at end of file diff --git a/src/routes/index.js b/src/routes/index.js index 0734afe..9df7086 100755 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,8 +1,17 @@ +/** + * @file Main routes configuration + * @module Routes + */ + const express = require( 'express' ); const router = express.Router(); +/** + * Configure API routes + * @param {Object} passport - Passport instance for authentication + * @returns {Object} Express router with API routes + */ module.exports.APIRoutes = ( passport ) => { - router.use( '/git', require( './git.routes' )( passport ) ); router.use( '/docker', require( './docker.routes' )( passport ) ); router.use( '/vpn', require( './vpn.routes' )( passport ) ); @@ -14,11 +23,15 @@ module.exports.APIRoutes = ( passport ) => { router.use( '/authentication', require( './authentication.routes' )( passport ) ); router.use( '/role', require( './role.routes' )( passport ) ); router.use( '/user_roles', require( './user_roles.routes' )( passport ) ); - router.use('/media', require('./media.routes')(passport)); - router.use('/post', require('./post.routes')(passport)); - router.use('/message_group', require('./message_group.routes')(passport)); - router.use('/message_group_members', require('./message_group_members.routes')(passport)); - router.use('/message', require('./message.routes')(passport)); - router.use('/auth', require('./auth.routes')()); + router.use( '/file', require( './files.routes' )( passport ) ); + router.use( '/file_users', require( './file_users.routes' )( passport ) ); + router.use( '/post', require( './post.routes' )( passport ) ); + router.use( '/post_files', require( './post_files.routes' )( passport ) ); + router.use( '/post_users', require( './post_users.routes' )( passport ) ); + router.use( '/message_group', require( './message_group.routes' )( passport ) ); + router.use( '/message_group_members', require( './message_group_members.routes' )( passport ) ); + router.use( '/message', require( './message.routes' )( passport ) ); + router.use( '/message_files', require( './message_files.routes' )( passport ) ); + router.use( '/auth', require( './auth.routes' )() ); return router; -}; +}; \ No newline at end of file diff --git a/src/routes/media.routes.js b/src/routes/media.routes.js deleted file mode 100644 index 01a1e8d..0000000 --- a/src/routes/media.routes.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @file Media routes configuration - */ - -const express = require('express'); -const router = express.Router(); -const { validate_auth } = require('../middleware/routeHelpers'); -const media_controller = require('../controllers/media.controller'); - -/** - * Configure media routes - * @param {Object} passport - Passport instance for authentication - * @returns {Object} Express router with media routes - */ -module.exports = (passport) => { - router.post('/create', validate_auth(passport), media_controller.create); - router.get('/file_path/:file_path', validate_auth(passport), media_controller.find_by_file_path); - router.get('/:id', validate_auth(passport), media_controller.find_one); - router.get('/', validate_auth(passport), media_controller.find_many); - router.put('/:id', validate_auth(passport), media_controller.update); - router.put('/:id/soft_delete', validate_auth(passport), media_controller.soft_delete); - return router; -}; \ No newline at end of file diff --git a/src/routes/message.routes.js b/src/routes/message.routes.js old mode 100644 new mode 100755 index 52d4fbb..0c874bd --- a/src/routes/message.routes.js +++ b/src/routes/message.routes.js @@ -2,22 +2,23 @@ * @file Message routes configuration */ -const express = require('express'); +const express = require( 'express' ); const router = express.Router(); -const { validate_auth } = require('../middleware/routeHelpers'); -const message_controller = require('../controllers/message.controller'); +const { validate_auth, restrictToRoles } = require( '../middleware/routeHelpers' ); +const message_controller = require( '../controllers/message.controller' ); /** * Configure message routes * @param {Object} passport - Passport instance for authentication * @returns {Object} Express router with message routes */ -module.exports = (passport) => { - router.post('/create', validate_auth(passport), message_controller.create); - router.get('/group/:group_id', validate_auth(passport), message_controller.find_by_group_id); - router.get('/recipient/:recipient_id', validate_auth(passport), message_controller.find_by_recipient_id); - router.get('/:id', validate_auth(passport), message_controller.find_one); - router.get('/', validate_auth(passport), message_controller.find_many); - router.put('/:id/mark_as_read', validate_auth(passport), message_controller.mark_as_read); +module.exports = ( passport ) => { + router.post( '/create', validate_auth( passport ), message_controller.create ); + router.get( '/group/:group_id', validate_auth( passport ), message_controller.find_by_group_id ); + router.get( '/recipient/:recipient_id', validate_auth( passport ), message_controller.find_by_recipient_id ); + router.get( '/:id', validate_auth( passport ), message_controller.find_one ); + router.get( '/', validate_auth( passport ), message_controller.find_many ); + router.put( '/:id/mark_as_read', validate_auth( passport ), restrictToRoles( ['admin', 'family'] ), message_controller.mark_as_read ); + router.post( '/:messageId/reactions', validate_auth( passport ), message_controller.add_reaction ); return router; }; \ No newline at end of file diff --git a/src/routes/message_files.routes.js b/src/routes/message_files.routes.js new file mode 100755 index 0000000..952e9cd --- /dev/null +++ b/src/routes/message_files.routes.js @@ -0,0 +1,21 @@ +/** + * @file Message files routes configuration + * @module MessageFilesRoutes + */ + +const express = require( 'express' ); +const router = express.Router(); +const { validate_auth, restrictToRoles } = require( '../middleware/routeHelpers' ); +const message_files_controller = require( '../controllers/message_files.controller' ); + +/** + * Configure message files routes + * @param {Object} passport - Passport instance for authentication + * @returns {Object} Express router with message files routes + */ +module.exports = ( passport ) => { + router.post( '/add', validate_auth( passport ), restrictToRoles( [ 'admin', 'family' ] ), message_files_controller.add_relation ); + router.delete( '/:message_id/:file_id', validate_auth( passport ), restrictToRoles( [ 'admin', 'family' ] ), message_files_controller.remove_relation ); + router.get( '/message/:message_id', validate_auth( passport ), message_files_controller.find_by_message_id ); + return router; +}; \ No newline at end of file diff --git a/src/routes/message_group.routes.js b/src/routes/message_group.routes.js old mode 100644 new mode 100755 diff --git a/src/routes/message_group_members.routes.js b/src/routes/message_group_members.routes.js old mode 100644 new mode 100755 diff --git a/src/routes/phone_number.routes.js b/src/routes/phone_number.routes.js old mode 100644 new mode 100755 diff --git a/src/routes/post.routes.js b/src/routes/post.routes.js old mode 100644 new mode 100755 diff --git a/src/routes/post_files.routes.js b/src/routes/post_files.routes.js old mode 100644 new mode 100755 diff --git a/src/routes/post_users.routes.js b/src/routes/post_users.routes.js old mode 100644 new mode 100755 diff --git a/src/routes/role.routes.js b/src/routes/role.routes.js old mode 100644 new mode 100755 diff --git a/src/routes/user.routes.js b/src/routes/user.routes.js old mode 100644 new mode 100755 diff --git a/src/routes/user_addresses.routes.js b/src/routes/user_addresses.routes.js old mode 100644 new mode 100755 diff --git a/src/routes/user_phone_numbers.routes.js b/src/routes/user_phone_numbers.routes.js old mode 100644 new mode 100755 diff --git a/src/routes/user_roles.routes.js b/src/routes/user_roles.routes.js old mode 100644 new mode 100755 diff --git a/src/routes/users.js b/src/routes/users.js deleted file mode 100755 index 623e430..0000000 --- a/src/routes/users.js +++ /dev/null @@ -1,9 +0,0 @@ -var express = require('express'); -var router = express.Router(); - -/* GET users listing. */ -router.get('/', function(req, res, next) { - res.send('respond with a resource'); -}); - -module.exports = router; diff --git a/src/routes/vpn.routes.js b/src/routes/vpn.routes.js old mode 100644 new mode 100755