<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
+ <w>appspecific</w>
<w>certfile</w>
+ <w>dport</w>
<w>easyrsa</w>
<w>genkey</w>
<w>ifconfig</w>
<w>nopass</w>
<w>ovpn</w>
<w>passout</w>
+ <w>pgmigrations</w>
<w>phasecustomsoft</w>
<w>phsdb</w>
<w>pkitool</w>
-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;
} ) );
app.use( bodyParser.urlencoded( {
extended:true,
- limit: '50mb'
+ limit:'50mb'
} ) );
-app.use(cookieParser());
+app.use( cookieParser() );
//Setting up log4js
const logger = log4js.default;
// 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() );
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 ) {
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 ) );
}
*/
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 );
}
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();
};
"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"
},
"@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",
"@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",
}
]
},
+ "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",
"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",
"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",
"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",
"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"
},
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
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);
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 = {
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<void>}
+ */
+ 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
--- /dev/null
+/**
+ * @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<void>}
+ */
+ 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<void>}
+ */
+ 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<void>}
+ */
+ 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
--- /dev/null
+/**
+ * @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<void>}
+ */
+ 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<void>}
+ */
+ 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<void>}
+ */
+ 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<void>}
+ */
+ 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<void>}
+ */
+ 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<void>}
+ */
+ 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<void>}
+ */
+ 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
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) );
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();
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) );
}) );
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();
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) );
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();
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) );
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
+++ /dev/null
-/**
- * @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<void>}
- */
- 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<void>}
- */
- 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<void>}
- */
- 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<void>}
- */
- 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<void>}
- */
- 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<void>}
- */
- 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
/**
* @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
* @param {Function} next - Express next middleware function
* @returns {Promise<void>}
*/
- 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 ) );
}
},
* @param {Function} next - Express next middleware function
* @returns {Promise<void>}
*/
- 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 ) );
}
},
* @param {Function} next - Express next middleware function
* @returns {Promise<void>}
*/
- 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 ) );
}
},
* @param {Function} next - Express next middleware function
* @returns {Promise<void>}
*/
- 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 ) );
}
},
* @param {Function} next - Express next middleware function
* @returns {Promise<void>}
*/
- 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 ) );
}
},
* @param {Function} next - Express next middleware function
* @returns {Promise<void>}
*/
- 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<void>}
+ */
+ 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
--- /dev/null
+/**
+ * @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<void>}
+ */
+ 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<void>}
+ */
+ 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<void>}
+ */
+ 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
* @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
* @param {Function} next - Express next middleware function
* @returns {Promise<void>}
*/
- 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 ) );
}
},
* @param {Function} next - Express next middleware function
* @returns {Promise<void>}
*/
- 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 ) );
}
},
* @param {Function} next - Express next middleware function
* @returns {Promise<void>}
*/
- 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 ) );
}
},
* @param {Function} next - Express next middleware function
* @returns {Promise<void>}
*/
- 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
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 ) );
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) {
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
/**
* 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<Authentication>} [props] - Authentication properties
+ * @param {Partial<Authentication_props>} [props] - Authentication properties
+ * @returns {Model<Authentication>||Object} Authentication instance
+ * @throws {ValidationError} If required fields are missing
+ * @throws {FailedToCreateError} If creation fails
*/
constructor( props ) {
super( props );
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;
};
};
* 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|null>} Authentication instance or null
+ * @returns {Promise<Model<Authentication>|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 );
/**
* Soft delete authentication record
* @param {number|string} deleted_by_id - ID of user performing deletion
- * @returns {Promise<Authentication>} Updated authentication instance
+ * @returns {Promise<Model<Authentication>>} Updated authentication instance
* @throws {ValidationError} If deleted_by_id is invalid
* @throws {NotFoundError} If record not found
*/
/**
* Lock user account
- * @returns {Promise<Authentication>} Updated authentication instance
+ * @returns {Promise<Model<Authentication>>} Updated authentication instance
* @throws {NotFoundError} If record not found
*/
async lock_account() {
/**
* Unlock user account
- * @returns {Promise<Authentication>} Updated authentication instance
+ * @returns {Promise<Model<Authentication>>} Updated authentication instance
* @throws {NotFoundError} If record not found
*/
async unlock_account() {
--- /dev/null
+/**
+ * @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<CommentReaction>} [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<CommentReaction>} 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<CommentReaction|null>} 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<CommentReaction[]>} 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
--- /dev/null
+/**
+ * @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<Comment>} [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, 'id'|'created_at'>} comment_data - Comment data
+ * @returns {Promise<Comment>} 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<Comment[]>} 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
--- /dev/null
+/**
+ * @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<File>} [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<File, 'id'|'created_at'|'is_deleted'|'deleted_by_id'|'deleted_at'> & {specific_users?: number[]}} file_data - File data
+ * @returns {Promise<File>} 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|null>} 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<File>} 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
--- /dev/null
+/**
+ * @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<FileUser>} [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<FileUser>} 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<FileUser|null>} 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<FileUser[]>} 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
-// noinspection ExceptionCaughtLocallyJS
+// noinspection JSUnusedGlobalSymbols
/**
* @file Unified database access interface
+ * @module Database
*/
const model_cache = new Map();
* @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<Object>} 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<Object|null>} 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<Object|null>} 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<Object[]>} 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<Object[]>} 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<Object|null>} 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<Object[]>} 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<Object>} 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<Object>} 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<Object>} 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<Object>} 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<Object|null>} 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<Object|null>} 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<Object[]>} 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<Object>} 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<Object|null>} 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<Object|null>} 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<Object[]>} 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<Object>} 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<Object|null>} 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<Object|null>} 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<Object[]>} 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<Object>} 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<Object|null>} 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<Object|null>} 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<Object[]>} 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<Object>} 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|null>} 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<Object|null>} 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<Object|null>} 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<Object[]>} 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<Object>} 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<Object>} 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<Object>} 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<Object>} 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<Object|null>} 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<Object|null>} 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<Object[]>} 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<Object>} 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<Object|null>} 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<Object|null>} 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<Object[]>} 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<Object>} 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<Object|null>} 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<Object|null>} 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<Object[]>} 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<Object>} 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<Object>} 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<Object|null>} 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<Object[]>} 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<Object>} 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<Object|null>} 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<Object|null>} 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<Object[]>} 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<Object>} 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<Object>} 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<Object|null>} 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<Object[]>} 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<Object>} 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<Object|null>} 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<Object[]>} 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<Object>} 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<Object|null>} 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<Object[]>} 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<Object>} 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<Object[]>} 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<Object|null>} 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<Object[]>} 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<Object>} 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<Object|null>} 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<Object[]>} 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<Object>} 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<Object|null>} 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<Object[]>} 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<Object>} 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<Object|null>} 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<Object|null>} 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<Object[]>} 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<Object>} 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<Object[]>} 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<Object[]>} 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|null>} 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<Message[]>} 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<Object>} 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<Object>} 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<Object|null>} 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<Object[]>} 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<Object>} 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<Object|null>} 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<Object[]>} 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())()
}
};
+++ /dev/null
-/**
- * @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<Media>} [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, 'id'|'created_at'|'is_deleted'|'deleted_by_id'|'deleted_at'>} media_data - Media data
- * @returns {Promise<Media>} 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|null>} 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<Media>} 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
/**
* @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
* @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
*/
/**
* Create a Message instance
* @param {Partial<Message>} [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, 'id'|'created_at'|'read'|'read_at'>} message_data - Message data
+ * @param {Omit<Message, 'id'|'created_at'|'read'|'read_at'> & {file_ids?: number[]}} message_data - Message data
* @returns {Promise<Message>} 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<Message[]>} 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<Message[]>} 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 );
}
/**
* @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() } );
}
}
--- /dev/null
+/**
+ * @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<MessageFile>} [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<MessageFile>} 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<MessageFile|null>} 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<MessageFile[]>} 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
--- /dev/null
+/**
+ * @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<MessageReaction>} [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<MessageReaction>} 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<MessageReaction|null>} 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<MessageReaction[]>} 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
/**
* 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];
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;
* Find one record
* @param {Object} where - Conditions for the WHERE clause
* @param {string[]} [excludes] - Fields to exclude from result
- * @returns {Promise<Object|null>} Found record or null
+ * @returns {Promise<Model|null>} Found record or null
*/
async find_one( where, excludes = [] ) {
const { keys, values } = this.build_where( where, this.default_columns );
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<Object|null>} 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
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<Object[]>} 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<Object>} Updated record
+ * @returns {Promise<Model|Object>} Updated record
* @throws {ValidationError} If no valid fields are provided or ID is missing
* @throws {NotFoundError} If record is not found
*/
+// 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
* @extends Model
*/
class User extends Model {
- constructor(props) {
- super(props);
+ constructor( props ) {
+ super( props );
this.table = 'phase.users';
this.prepend = 'u';
this.default_columns = [
];
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,
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() {
// 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(
-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
--- /dev/null
+/**
+ * @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
--- /dev/null
+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
+/**
+ * @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 ) );
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
+++ /dev/null
-/**
- * @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
* @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
--- /dev/null
+/**
+ * @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
+++ /dev/null
-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;