/**
* @file Post controller for handling post-related API requests
+ * @module PostController
*/
-const db = require('../models');
-const createError = require('http-errors');
-const logger = global.logger;
+const db = require( '../models' );
+const createError = require( 'http-errors' );
/**
* Post controller
* @param {Function} next - Express next middleware function
* @returns {Promise<void>}
*/
- async create(req, res, next) {
+ async create( req, res, next ) {
try {
- const post_data = req.body;
- const post = await db.post.create(post_data);
- res.json(post);
- } catch (error) {
- logger.error(`Create post error: ${error.message}`);
- next(createError(error.status || 409, error.message));
+ const { title, content, post_type, visibility, specific_users = [] } = req.body;
+ const files = req.files || [];
+ const post_data = {
+ author_id: req.user.id,
+ title,
+ content,
+ post_type,
+ visibility,
+ specific_users,
+ created_by_id: req.user.id,
+ file_ids: []
+ };
+ 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,
+ specific_users,
+ created_by_id: req.user.id
+ };
+ const uploaded_file = await db.file.create( file_data );
+ post_data.file_ids.push( uploaded_file.id );
+ }
+ const post = await db.post.create( post_data );
+ res.json( post );
+ } catch ( error ) {
+ logger.error( `Create post error: ${ error.message }` );
+ next( createError( error.status || 409, error.message ) );
}
},
* @param {Function} next - Express next middleware function
* @returns {Promise<void>}
*/
- async find_by_title(req, res, next) {
+ async find_by_title( req, res, next ) {
try {
const { title } = req.params;
- const post = await db.post.find_by_title(title);
- if (!post) return next(createError(404, 'Post not found'));
- res.json(post);
- } catch (error) {
- logger.error(`Find post by title error: ${error.message}`);
- next(createError(error.status || 500, error.message));
+ const post = await db.post.find_by_title( title );
+ if ( !post ) return next( createError( 404, 'Post not found' ) );
+ res.json( post );
+ } catch ( error ) {
+ logger.error( `Find post by title 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 post = await db.post.find_one({ id: parseInt(id) });
- if (!post) return next(createError(404, 'Post not found'));
- res.json(post);
- } catch (error) {
- logger.error(`Find post error: ${error.message}`);
- next(createError(error.status || 500, error.message));
+ const post = await db.post.find_one( { id: parseInt( id ) } );
+ if ( !post ) return next( createError( 404, 'Post not found' ) );
+ res.json( post );
+ } catch ( error ) {
+ logger.error( `Find post 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 posts = await db.post.find_many(where, [], null, parseInt(limit), parseInt(offset));
- res.json(posts);
- } catch (error) {
- logger.error(`Find many posts error: ${error.message}`);
- next(createError(error.status || 500, error.message));
+ const { limit = '100', offset = '0', ...where } = req.query;
+ where.is_deleted = false;
+ if ( req.user ) {
+ where[ '$or' ] = [
+ { visibility: 'family' },
+ { author_id: req.user.id },
+ { specific_users: { $contains: [ req.user.id ] } }
+ ];
+ }
+ const posts = await db.post.find_many( where, [], null, parseInt( limit ), parseInt( offset ) );
+ res.json( posts );
+ } catch ( error ) {
+ logger.error( `Find many posts 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 post_data = req.body;
- const post = await db.post.instance().find_one({ id: parseInt(id) });
- if (!post) return next(createError(404, 'Post not found'));
- const updated_post = await post.update(post_data);
- res.json(updated_post);
- } catch (error) {
- logger.error(`Update post error: ${error.message}`);
- next(createError(error.status || 400, error.message));
+ const post = await db.post.instance().find_one( { id: parseInt( id ) } );
+ if ( !post ) return next( createError( 404, 'Post not found' ) );
+ if ( post.author_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) {
+ return next( createError( 403, 'Unauthorized to update this post' ) );
+ }
+ const updated_post = await post.update( post_data );
+ res.json( updated_post );
+ } catch ( error ) {
+ logger.error( `Update post error: ${ error.message }` );
+ next( createError( error.status || 400, error.message ) );
}
},
* @param {Function} next - Express next middleware function
* @returns {Promise<void>}
*/
- async soft_delete(req, res, next) {
+ async soft_delete( req, res, next ) {
try {
const { id } = req.params;
const { deleted_by_id } = req.body;
- const post = await db.post.instance().find_one({ id: parseInt(id) });
- if (!post) return next(createError(404, 'Post not found'));
- const deleted_post = await post.soft_delete(deleted_by_id);
- res.json(deleted_post);
- } catch (error) {
- logger.error(`Soft delete post error: ${error.message}`);
- next(createError(error.status || 400, error.message));
+ const post = await db.post.instance().find_one( { id: parseInt( id ) } );
+ if ( !post ) return next( createError( 404, 'Post not found' ) );
+ if ( post.author_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) {
+ return next( createError( 403, 'Unauthorized to delete this post' ) );
+ }
+ const deleted_post = await post.soft_delete( deleted_by_id || req.user.id );
+ res.json( deleted_post );
+ } catch ( error ) {
+ logger.error( `Soft delete post error: ${ error.message }` );
+ next( createError( error.status || 400, error.message ) );
+ }
+ },
+
+ /**
+ * Add a comment to a post
+ * @param {Object} req - Express request object
+ * @param {Object} res - Express response object
+ * @param {Function} next - Express next middleware function
+ * @returns {Promise<void>}
+ */
+ async add_comment( req, res, next ) {
+ try {
+ const { id } = req.params;
+ const { content, parent_id } = req.body;
+ const comment_data = {
+ post_id: parseInt( id ),
+ user_id: req.user.id,
+ parent_id: parent_id ? parseInt( parent_id ) : null,
+ content
+ };
+ const comment = await db.comments.create( comment_data );
+ res.json( comment );
+ } catch ( error ) {
+ logger.error( `Add comment error: ${ error.message }` );
+ next( createError( error.status || 400, error.message ) );
+ }
+ },
+
+ /**
+ * Add a reaction to a post
+ * @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 { id } = req.params;
+ const { reaction } = req.body;
+ const reaction_data = await db.post_reactions.add( id, req.user.id, reaction );
+ res.json( reaction_data );
+ } catch ( error ) {
+ logger.error( `Add post reaction error: ${ error.message }` );
+ next( createError( error.status || 400, error.message ) );
+ }
+ },
+
+ /**
+ * Add a reaction to a comment
+ * @param {Object} req - Express request object
+ * @param {Object} res - Express response object
+ * @param {Function} next - Express next middleware function
+ * @returns {Promise<void>}
+ */
+ async add_comment_reaction( req, res, next ) {
+ try {
+ const { commentId } = req.params;
+ const { reaction } = req.body;
+ const reaction_data = await db.comment_reactions.add( commentId, req.user.id, reaction );
+ res.json( reaction_data );
+ } catch ( error ) {
+ logger.error( `Add comment reaction error: ${ error.message }` );
+ next( createError( error.status || 400, error.message ) );
}
}
};
\ No newline at end of file
--- /dev/null
+/**
+ * @file Post files controller for handling post-files relation API requests
+ * @module PostFilesController
+ */
+
+const db = require( '../models' );
+const createError = require( 'http-errors' );
+
+/**
+ * Post files controller
+ * @type {Object}
+ */
+module.exports = {
+ /**
+ * Add a post-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 { post_id, file_id } = req.body;
+ const post = await db.post.find_one( { id: parseInt( post_id ) } );
+ if ( !post ) return next( createError( 404, 'Post not found' ) );
+ if ( post.author_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) {
+ return next( createError( 403, 'Unauthorized to add relation to this post' ) );
+ }
+ const file = await db.file.find_one( { id: parseInt( file_id ) } );
+ if ( !file ) return next( createError( 404, 'File not found' ) );
+ const relation = await db.post_files.add_relation( post_id, file_id );
+ res.json( relation );
+ } catch ( error ) {
+ logger.error( `Add post-file relation error: ${ error.message }` );
+ next( createError( error.status || 400, error.message ) );
+ }
+ },
+
+ /**
+ * Remove a post-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 { post_id, file_id } = req.params;
+ const post = await db.post.find_one( { id: parseInt( post_id ) } );
+ if ( !post ) return next( createError( 404, 'Post not found' ) );
+ if ( post.author_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) {
+ return next( createError( 403, 'Unauthorized to remove relation from this post' ) );
+ }
+ const file = await db.file.find_one( { id: parseInt( file_id ) } );
+ if ( !file ) return next( createError( 404, 'File not found' ) );
+ const relation = await db.post_files.remove_relation( post_id, file_id );
+ res.json( relation || { message: 'Relation removed' } );
+ } catch ( error ) {
+ logger.error( `Remove post-file relation error: ${ error.message }` );
+ next( createError( error.status || 400, error.message ) );
+ }
+ },
+
+ /**
+ * Find post-file relations by post 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_post_id( req, res, next ) {
+ try {
+ const { post_id } = req.params;
+ const { limit = '100', offset = '0' } = req.query;
+ const relations = await db.post_files.find_by_post_id( parseInt( post_id ), [], null, parseInt( limit ), parseInt( offset ) );
+ res.json( relations );
+ } catch ( error ) {
+ logger.error( `Find post-file relations by post ID error: ${ error.message }` );
+ next( createError( error.status || 500, error.message ) );
+ }
+ }
+};
\ No newline at end of file
--- /dev/null
+/**
+ * @file Post users controller for handling post-users relation API requests
+ * @module PostUsersController
+ */
+
+const db = require( '../models' );
+const createError = require( 'http-errors' );
+
+/**
+ * Post users controller
+ * @type {Object}
+ */
+module.exports = {
+ /**
+ * Add a post-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 { post_id, user_id } = req.body;
+ const post = await db.post.find_one( { id: parseInt( post_id ) } );
+ if ( !post ) return next( createError( 404, 'Post not found' ) );
+ if ( post.author_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) {
+ return next( createError( 403, 'Unauthorized to add relation to this post' ) );
+ }
+ const user = await db.user.find_one( { id: parseInt( user_id ) } );
+ if ( !user ) return next( createError( 404, 'User not found' ) );
+ const relation = await db.post_users.add_relation( post_id, user_id );
+ res.json( relation );
+ } catch ( error ) {
+ logger.error( `Add post-user relation error: ${ error.message }` );
+ next( createError( error.status || 400, error.message ) );
+ }
+ },
+
+ /**
+ * Remove a post-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 { post_id, user_id } = req.params;
+ const post = await db.post.find_one( { id: parseInt( post_id ) } );
+ if ( !post ) return next( createError( 404, 'Post not found' ) );
+ if ( post.author_id !== req.user.id && !req.user.roles.includes( 'admin' ) ) {
+ return next( createError( 403, 'Unauthorized to remove relation from this post' ) );
+ }
+ const user = await db.user.find_one( { id: parseInt( user_id ) } );
+ if ( !user ) return next( createError( 404, 'User not found' ) );
+ const relation = await db.post_users.remove_relation( post_id, user_id );
+ res.json( relation || { message: 'Relation removed' } );
+ } catch ( error ) {
+ logger.error( `Remove post-user relation error: ${ error.message }` );
+ next( createError( error.status || 400, error.message ) );
+ }
+ },
+
+ /**
+ * Find post-user relations by post 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_post_id( req, res, next ) {
+ try {
+ const { post_id } = req.params;
+ const { limit = '100', offset = '0' } = req.query;
+ const relations = await db.post_users.find_by_post_id( parseInt( post_id ), [], null, parseInt( limit ), parseInt( offset ) );
+ res.json( relations );
+ } catch ( error ) {
+ logger.error( `Find post-user relations by post ID error: ${ error.message }` );
+ next( createError( error.status || 500, error.message ) );
+ }
+ }
+};
\ No newline at end of file
/**
* @file Post model for phase.posts table
+ * @module Post
*/
-const { Model, NotFoundError, ValidationError, FailedToCreateError } = require('./model');
+const { Model, NotFoundError, ValidationError, FailedToCreateError } = require( './model' );
/**
* @typedef {Object} Post
* @property {number} id - Post ID (primary key)
- * @property {number} user_id - User ID (foreign key)
+ * @property {number} author_id - User ID (foreign key)
* @property {string} title - Post title
* @property {string} content - Post content
* @property {string} post_type - Post type (e.g., 'blog', 'vlog')
- * @property {string} visibility - Visibility (e.g., 'private', 'family', 'public')
+ * @property {string} visibility - Visibility (e.g., 'private', 'family', 'specific')
* @property {number|null} created_by_id - ID of user who created this post
* @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 post
* @property {Date|null} deleted_at - Deletion timestamp
+ * @property {Object[]} attached_files - Array of attached file IDs
+ * @property {Object[]} specific_users - Array of user IDs for specific visibility
+ * @property {Object[]} reactions - Array of post reactions
+ * @property {Object[]} comments - Array of comments
*/
/**
* Create a Post instance
* @param {Partial<Post>} [props] - Post properties
*/
- constructor(props) {
- super(props);
+ constructor( props ) {
+ super( props );
+ /** @type {string} Database table name */
this.table = 'phase.posts';
+ /** @type {string} Table alias for queries */
this.prepend = 'p';
- this.default_columns = [
- 'id', 'user_id', 'title', 'content', 'post_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'];
+ /** @type {string[]} Allowed columns for queries */
+ this.default_columns = [ 'id', 'author_id', 'title', 'content', 'post_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 p.id, p.user_id, p.title, p.content, p.post_type, p.visibility,
- p.created_by_id, p.created_at, p.is_deleted, p.deleted_by_id, p.deleted_at
- FROM phase.posts p
- WHERE p.is_deleted = false
- `;
- this.base_list_query = `
- SELECT p.id, p.user_id, p.title, p.content, p.post_type, p.visibility,
- p.created_by_id, p.created_at
- FROM phase.posts p
- WHERE p.is_deleted = false
+ SELECT p.id,
+ p.author_id,
+ p.title,
+ p.content,
+ p.post_type,
+ p.visibility,
+ p.created_by_id,
+ p.created_at,
+ p.is_deleted,
+ p.deleted_by_id,
+ p.deleted_at,
+ concat(u.first_name, ' ', u.last_name) as user_name,
+ concat(u2.first_name, ' ', u2.last_name) as created_by_name,
+ (SELECT json_agg(json_build_object('file_id', pf.file_id))
+ FROM phase.post_files pf
+ WHERE pf.post_id = p.id) as attached_files,
+ (SELECT json_agg(json_build_object('user_id', pu.user_id))
+ FROM phase.post_users pu
+ WHERE pu.post_id = p.id) as specific_users,
+ (SELECT json_agg(json_build_object('user_id', pr.user_id, 'reaction', pr.reaction))
+ FROM phase.post_reactions pr
+ WHERE pr.post_id = p.id) as reactions,
+ (SELECT json_agg(json_build_object(
+ 'id', c.id, 'post_id', c.post_id, 'user_id', c.user_id, 'user_name',
+ concat(cu.first_name, ' ', cu.last_name),
+ 'content', c.content, 'created_at', c.created_at, 'parent_id', c.parent_id,
+ 'reactions', (SELECT json_agg(json_build_object('user_id', cr.user_id, 'reaction', cr.reaction))
+ FROM phase.comment_reactions cr
+ WHERE cr.comment_id = c.id)
+ ))
+ FROM phase.comments c
+ JOIN phase.users cu ON c.user_id = cu.id
+ WHERE c.post_id = p.id) as comments
+ FROM phase.posts p
+ INNER JOIN phase.users u ON p.author_id = u.id
+ INNER JOIN phase.users u2 ON u2.id = p.created_by_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 p.created_at DESC';
- this.instance = _props => new Post(_props);
+ /** @type {Function} Function to instantiate a model */
+ this.instance = _props => new Post( _props );
}
/**
* Create a new post
- * @param {Omit<Post, 'id'|'created_at'|'is_deleted'|'deleted_by_id'|'deleted_at'>} post_data - Post data
+ * @param {Omit<Post, 'id'|'created_at'|'is_deleted'|'deleted_by_id'|'deleted_at'> & {specific_users?: number[], file_ids?: number[]}} post_data - Post data
* @returns {Promise<Post>} Created post instance
* @throws {ValidationError} If required fields are missing
* @throws {FailedToCreateError} If creation fails
*/
- static async create(post_data) {
- const { user_id, title, content, post_type, visibility = 'private', created_by_id = null } = post_data;
- if (!user_id || !title || !content || !post_type) {
- throw new ValidationError('Missing required fields: user_id, title, content, post_type');
+ static async create( post_data ) {
+ const {
+ author_id,
+ title,
+ content,
+ post_type,
+ visibility = 'family',
+ created_by_id = null,
+ specific_users = [],
+ file_ids = []
+ } = post_data;
+ if ( !author_id || !title || !content || !post_type ) {
+ throw new ValidationError( 'Missing required fields: author_id, title, content, post_type' );
}
- const query_str = `
- INSERT INTO phase.posts (user_id, title, content, post_type, visibility, created_by_id)
- VALUES ($1, $2, $3, $4, $5, $6) RETURNING *;
- `;
- const values = [user_id, title, content, post_type, visibility, created_by_id];
- const result = await phsdb.query(query_str, values, { plain: true });
- if (!result) throw new FailedToCreateError('Failed to create post');
- return new Post(result);
+ return await this.prototype.with_transaction( async ( client ) => {
+ const query_str = `
+ INSERT INTO phase.posts (author_id, title, content, post_type, visibility, created_by_id)
+ VALUES ($1, $2, $3, $4, $5, $6)
+ RETURNING *;
+ `;
+ const values = [ author_id, title, content, post_type, visibility, created_by_id || author_id ];
+ const result = await phsdb.client_query( client, query_str, values, { plain: true } );
+ if ( !result ) throw new FailedToCreateError( 'Failed to create post' );
+ const post = new Post( result );
+ if ( visibility === 'specific' && specific_users?.length ) {
+ const user_query = `INSERT INTO phase.post_users (post_id, user_id)
+ VALUES ($1, $2);`;
+ for ( const user_id of specific_users ) {
+ await phsdb.client_query( client, user_query, [ post.id, user_id ] );
+ }
+ }
+ if ( file_ids?.length ) {
+ const file_query = `INSERT INTO phase.post_files (post_id, file_id)
+ VALUES ($1, $2);`;
+ for ( const file_id of file_ids ) {
+ await phsdb.client_query( client, file_query, [ post.id, file_id ] );
+ }
+ }
+ return post;
+ } );
}
/**
* Find post by title
* @param {string} title - Title to search for
- * @param {string[]} [excludes] - Fields to exclude from result
+ * @param {string[]} [excludes=[]] - Fields to exclude from result
* @returns {Promise<Post|null>} Post instance or null
*/
- static async find_by_title(title, excludes = []) {
- return await new Post().find_one({ title }, excludes);
+ static async find_by_title( title, excludes = [] ) {
+ return await new Post().find_one( { title }, excludes );
}
/**
* @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');
+ 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({
+ return await this.update( {
is_deleted: true,
deleted_at: new Date().toISOString(),
deleted_by_id: deleted_by_id_int
- });
+ } );
}
}
--- /dev/null
+/**
+ * @file Post files model for phase.post_files table
+ * @module PostFile
+ */
+
+const { Model, ValidationError, FailedToCreateError } = require( './model' );
+
+/**
+ * @typedef {Object} PostFile
+ * @property {number} post_id - Post ID (foreign key to posts)
+ * @property {number} file_id - File ID (foreign key to files)
+ */
+
+/**
+ * Post files model class
+ * @extends Model
+ */
+class PostFile extends Model {
+ /**
+ * Create a PostFile instance
+ * @param {Partial<PostFile>} [props] - Post file properties
+ */
+ constructor( props ) {
+ super( props );
+ /** @type {string} Database table name */
+ this.table = 'phase.post_files';
+ /** @type {string} Table alias for queries */
+ this.prepend = 'pf';
+ /** @type {string[]} Allowed columns for queries */
+ this.default_columns = [ 'post_id', 'file_id' ];
+ /** @type {string[]} Columns excluded from updates */
+ this.update_exclude_columns = [ 'post_id', 'file_id' ];
+ /** @type {string} Base query for single record retrieval */
+ this.base_query = `SELECT pf.post_id, pf.file_id
+ FROM phase.post_files pf`;
+ /** @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 pf.post_id ASC';
+ /** @type {Function} Function to instantiate a model */
+ this.instance = _props => new PostFile( _props );
+ }
+
+ /**
+ * Add a post-file relation
+ * @param {number|string} post_id - Post ID
+ * @param {number|string} file_id - File ID
+ * @returns {Promise<PostFile>} Created relation instance
+ * @throws {ValidationError} If IDs are invalid
+ * @throws {FailedToCreateError} If creation fails
+ */
+ static async add_relation( post_id, file_id ) {
+ const post_id_int = parseInt( post_id, 10 );
+ const file_id_int = parseInt( file_id, 10 );
+ if ( isNaN( post_id_int ) || isNaN( file_id_int ) ) {
+ throw new ValidationError( 'post_id and file_id must be valid integers' );
+ }
+ const query_str = `INSERT INTO phase.post_files (post_id, file_id)
+ VALUES ($1, $2)
+ RETURNING *;`;
+ const values = [ post_id_int, file_id_int ];
+ const result = await phsdb.query( query_str, values, { plain: true } );
+ if ( !result ) throw new FailedToCreateError( 'Failed to add post-file relation' );
+ return new PostFile( result );
+ }
+
+ /**
+ * Remove a post-file relation
+ * @param {number|string} post_id - Post ID
+ * @param {number|string} file_id - File ID
+ * @returns {Promise<PostFile|null>} Deleted relation instance or null
+ * @throws {ValidationError} If IDs are invalid
+ */
+ static async remove_relation( post_id, file_id ) {
+ const post_id_int = parseInt( post_id, 10 );
+ const file_id_int = parseInt( file_id, 10 );
+ if ( isNaN( post_id_int ) || isNaN( file_id_int ) ) {
+ throw new ValidationError( 'post_id and file_id must be valid integers' );
+ }
+ const query_str = `DELETE
+ FROM phase.post_files
+ WHERE post_id = $1
+ AND file_id = $2
+ RETURNING *;`;
+ const values = [ post_id_int, file_id_int ];
+ const result = await phsdb.query( query_str, values, { plain: true } );
+ return result ? new PostFile( result ) : null;
+ }
+
+ /**
+ * Find post-file relations 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<PostFile[]>} Array of relations
+ */
+ static async find_by_post_id( post_id, excludes = [], order_by = null, limit = 100, offset = 0 ) {
+ return await new PostFile().find_many( { post_id }, excludes, order_by, limit, offset );
+ }
+}
+
+module.exports = PostFile;
\ No newline at end of file
--- /dev/null
+/**
+ * @file Post reactions model for phase.post_reactions table
+ * @module PostReaction
+ */
+
+const { Model, ValidationError, FailedToCreateError } = require( './model' );
+
+/**
+ * @typedef {Object} PostReaction
+ * @property {number} post_id - Post ID (foreign key to posts)
+ * @property {number} user_id - User ID (foreign key to users)
+ * @property {string} reaction - Reaction type (e.g., 'like', 'heart', 'laugh')
+ */
+
+/**
+ * Post reactions model class
+ * @extends Model
+ */
+class PostReaction extends Model {
+ /**
+ * Create a PostReaction instance
+ * @param {Partial<PostReaction>} [props] - Post reaction properties
+ */
+ constructor( props ) {
+ super( props );
+ /** @type {string} Database table name */
+ this.table = 'phase.post_reactions';
+ /** @type {string} Table alias for queries */
+ this.prepend = 'pr';
+ /** @type {string[]} Allowed columns for queries */
+ this.default_columns = [ 'post_id', 'user_id', 'reaction' ];
+ /** @type {string[]} Columns excluded from updates */
+ this.update_exclude_columns = [ 'post_id', 'user_id' ];
+ /** @type {string} Base query for single record retrieval */
+ this.base_query = `SELECT pr.post_id, pr.user_id, pr.reaction
+ FROM phase.post_reactions pr`;
+ /** @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 pr.post_id ASC';
+ /** @type {Function} Function to instantiate a model */
+ this.instance = _props => new PostReaction( _props );
+ }
+
+ /**
+ * Add a post reaction
+ * @param {number|string} post_id - Post ID
+ * @param {number|string} user_id - User ID
+ * @param {string} reaction - Reaction type
+ * @returns {Promise<PostReaction>} Created or updated reaction instance
+ * @throws {ValidationError} If required fields are missing
+ * @throws {FailedToCreateError} If creation fails
+ */
+ static async add( post_id, user_id, reaction ) {
+ const post_id_int = parseInt( post_id, 10 );
+ const user_id_int = parseInt( user_id, 10 );
+ if ( isNaN( post_id_int ) || isNaN( user_id_int ) || !reaction ) {
+ throw new ValidationError( 'post_id, user_id, and reaction are required' );
+ }
+ const query_str = `
+ INSERT INTO phase.post_reactions (post_id, user_id, reaction)
+ VALUES ($1, $2, $3)
+ ON CONFLICT (post_id, user_id) DO UPDATE SET reaction = EXCLUDED.reaction
+ RETURNING *;
+ `;
+ const values = [ post_id_int, user_id_int, reaction ];
+ const result = await phsdb.query( query_str, values, { plain: true } );
+ if ( !result ) throw new FailedToCreateError( 'Failed to add post reaction' );
+ return new PostReaction( result );
+ }
+
+ /**
+ * Remove a post reaction
+ * @param {number|string} post_id - Post ID
+ * @param {number|string} user_id - User ID
+ * @returns {Promise<PostReaction|null>} Deleted reaction instance or null
+ * @throws {ValidationError} If IDs are invalid
+ */
+ static async remove( post_id, user_id ) {
+ const post_id_int = parseInt( post_id, 10 );
+ const user_id_int = parseInt( user_id, 10 );
+ if ( isNaN( post_id_int ) || isNaN( user_id_int ) ) {
+ throw new ValidationError( 'post_id and user_id must be valid integers' );
+ }
+ const query_str = `DELETE
+ FROM phase.post_reactions
+ WHERE post_id = $1
+ AND user_id = $2
+ RETURNING *;`;
+ const values = [ post_id_int, user_id_int ];
+ const result = await phsdb.query( query_str, values, { plain: true } );
+ return result ? new PostReaction( result ) : null;
+ }
+
+ /**
+ * Find post reactions 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<PostReaction[]>} Array of reactions
+ */
+ static async find_by_post_id( post_id, excludes = [], order_by = null, limit = 100, offset = 0 ) {
+ return await new PostReaction().find_many( { post_id }, excludes, order_by, limit, offset );
+ }
+}
+
+module.exports = PostReaction;
\ No newline at end of file
--- /dev/null
+/**
+ * @file Post users model for phase.post_users table
+ * @module PostUser
+ */
+
+const { Model, ValidationError, FailedToCreateError } = require( './model' );
+
+/**
+ * @typedef {Object} PostUser
+ * @property {number} post_id - Post ID (foreign key to posts)
+ * @property {number} user_id - User ID (foreign key to users)
+ */
+
+/**
+ * Post users model class
+ * @extends Model
+ */
+class PostUser extends Model {
+ /**
+ * Create a PostUser instance
+ * @param {Partial<PostUser>} [props] - Post user properties
+ */
+ constructor( props ) {
+ super( props );
+ /** @type {string} Database table name */
+ this.table = 'phase.post_users';
+ /** @type {string} Table alias for queries */
+ this.prepend = 'pu';
+ /** @type {string[]} Allowed columns for queries */
+ this.default_columns = [ 'post_id', 'user_id' ];
+ /** @type {string[]} Columns excluded from updates */
+ this.update_exclude_columns = [ 'post_id', 'user_id' ];
+ /** @type {string} Base query for single record retrieval */
+ this.base_query = `SELECT pu.post_id, pu.user_id
+ FROM phase.post_users pu`;
+ /** @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 pu.post_id ASC';
+ /** @type {Function} Function to instantiate a model */
+ this.instance = _props => new PostUser( _props );
+ }
+
+ /**
+ * Add a post-user relation
+ * @param {number|string} post_id - Post ID
+ * @param {number|string} user_id - User ID
+ * @returns {Promise<PostUser>} Created relation instance
+ * @throws {ValidationError} If IDs are invalid
+ * @throws {FailedToCreateError} If creation fails
+ */
+ static async add_relation( post_id, user_id ) {
+ const post_id_int = parseInt( post_id, 10 );
+ const user_id_int = parseInt( user_id, 10 );
+ if ( isNaN( post_id_int ) || isNaN( user_id_int ) ) {
+ throw new ValidationError( 'post_id and user_id must be valid integers' );
+ }
+ const query_str = `INSERT INTO phase.post_users (post_id, user_id)
+ VALUES ($1, $2)
+ RETURNING *;`;
+ const values = [ post_id_int, user_id_int ];
+ const result = await phsdb.query( query_str, values, { plain: true } );
+ if ( !result ) throw new FailedToCreateError( 'Failed to add post-user relation' );
+ return new PostUser( result );
+ }
+
+ /**
+ * Remove a post-user relation
+ * @param {number|string} post_id - Post ID
+ * @param {number|string} user_id - User ID
+ * @returns {Promise<PostUser|null>} Deleted relation instance or null
+ * @throws {ValidationError} If IDs are invalid
+ */
+ static async remove_relation( post_id, user_id ) {
+ const post_id_int = parseInt( post_id, 10 );
+ const user_id_int = parseInt( user_id, 10 );
+ if ( isNaN( post_id_int ) || isNaN( user_id_int ) ) {
+ throw new ValidationError( 'post_id and user_id must be valid integers' );
+ }
+ const query_str = `DELETE
+ FROM phase.post_users
+ WHERE post_id = $1
+ AND user_id = $2
+ RETURNING *;`;
+ const values = [ post_id_int, user_id_int ];
+ const result = await phsdb.query( query_str, values, { plain: true } );
+ return result ? new PostUser( result ) : null;
+ }
+
+ /**
+ * Find post-user relations 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<PostUser[]>} Array of relations
+ */
+ static async find_by_post_id( post_id, excludes = [], order_by = null, limit = 100, offset = 0 ) {
+ return await new PostUser().find_many( { post_id }, excludes, order_by, limit, offset );
+ }
+}
+
+module.exports = PostUser;
\ No newline at end of file
/**
* @file Post routes configuration
+ * @module PostRoutes
*/
-const express = require('express');
+const express = require( 'express' );
const router = express.Router();
-const { validate_auth } = require('../middleware/routeHelpers');
-const post_controller = require('../controllers/post.controller');
+const { validate_auth, restrictToRoles } = require( '../middleware/routeHelpers' );
+const post_controller = require( '../controllers/post.controller' );
/**
* Configure post routes
* @param {Object} passport - Passport instance for authentication
* @returns {Object} Express router with post routes
*/
-module.exports = (passport) => {
- router.post('/create', validate_auth(passport), post_controller.create);
- router.get('/title/:title', validate_auth(passport), post_controller.find_by_title);
- router.get('/:id', validate_auth(passport), post_controller.find_one);
- router.get('/', post_controller.find_many);
- router.put('/:id', validate_auth(passport), post_controller.update);
- router.put('/:id/soft_delete', validate_auth(passport), post_controller.soft_delete);
+module.exports = ( passport ) => {
+ router.post( '/create', validate_auth( passport ), post_controller.create );
+ router.get( '/title/:title', validate_auth( passport ), post_controller.find_by_title );
+ router.get( '/:id', validate_auth( passport ), post_controller.find_one );
+ router.get( '/', post_controller.find_many );
+ router.put( '/:id', validate_auth( passport ), restrictToRoles( [ 'admin', 'family' ] ), post_controller.update );
+ router.put( '/:id/soft_delete', validate_auth( passport ), restrictToRoles( [ 'admin', 'family' ] ), post_controller.soft_delete );
+ router.post( '/:id/comments', validate_auth( passport ), post_controller.add_comment );
+ router.post( '/:id/reactions', validate_auth( passport ), post_controller.add_reaction );
+ router.post( '/comments/:commentId/reactions', validate_auth( passport ), post_controller.add_comment_reaction );
return router;
};
\ No newline at end of file
--- /dev/null
+/**
+ * @file Post files routes configuration
+ * @module PostFilesRoutes
+ */
+
+const express = require( 'express' );
+const router = express.Router();
+const { validate_auth, restrictToRoles } = require( '../middleware/routeHelpers' );
+const post_files_controller = require( '../controllers/post_files.controller' );
+
+/**
+ * Configure post files routes
+ * @param {Object} passport - Passport instance for authentication
+ * @returns {Object} Express router with post files routes
+ */
+module.exports = ( passport ) => {
+ router.post( '/add', validate_auth( passport ), restrictToRoles( [ 'admin', 'family' ] ), post_files_controller.add_relation );
+ router.delete( '/:post_id/:file_id', validate_auth( passport ), restrictToRoles( [ 'admin', 'family' ] ), post_files_controller.remove_relation );
+ router.get( '/post/:post_id', validate_auth( passport ), post_files_controller.find_by_post_id );
+ return router;
+};
\ No newline at end of file
--- /dev/null
+/**
+ * @file Post users routes configuration
+ * @module PostUsersRoutes
+ */
+
+const express = require( 'express' );
+const router = express.Router();
+const { validate_auth, restrictToRoles } = require( '../middleware/routeHelpers' );
+const post_users_controller = require( '../controllers/post_users.controller' );
+
+/**
+ * Configure post users routes
+ * @param {Object} passport - Passport instance for authentication
+ * @returns {Object} Express router with post users routes
+ */
+module.exports = ( passport ) => {
+ router.post( '/add', validate_auth( passport ), restrictToRoles( [ 'admin', 'family' ] ), post_users_controller.add_relation );
+ router.delete( '/:post_id/:user_id', validate_auth( passport ), restrictToRoles( [ 'admin', 'family' ] ), post_users_controller.remove_relation );
+ router.get( '/post/:post_id', validate_auth( passport ), post_users_controller.find_by_post_id );
+ return router;
+};
\ No newline at end of file