Source: models/file.js

'use strict';

const { getPostgresDAL } = require('../db-postgres');
const type = require('../dal').type;
const mlString = require('../dal').mlString;
const debug = require('../util/debug');
const { initializeModel } = require('../dal/lib/model-initializer');

const validLicenses = ['cc-0', 'cc-by', 'cc-by-sa', 'fair-use'];

let File = null;

/**
 * Initialize the PostgreSQL File model
 * @param {DataAccessLayer} dal - Optional DAL instance for testing
 */
async function initializeFileModel(dal = null) {
  const activeDAL = dal || await getPostgresDAL();

  if (!activeDAL) {
    debug.db('PostgreSQL DAL not available, skipping File model initialization');
    return null;
  }

  try {
    // Create the schema with revision fields
    const fileSchema = {
      id: type.string().uuid(4),
      name: type.string().max(512),
      description: mlString.getSchema(),
      
      // CamelCase fields that map to snake_case database columns
      uploadedBy: type.string().uuid(4),
      uploadedOn: type.date(),
      mimeType: type.string(),
      license: type.string().enum(validLicenses),
      
      // Provided by uploader: if not the author, who is?
      creator: mlString.getSchema(),
      // Provided by uploader: where does this file come from?
      source: mlString.getSchema(),
      // Uploaded files with incomplete metadata are stored in a separate directory
      completed: type.boolean().default(false),
      
      // Virtual permission fields
      userCanDelete: type.virtual().default(false),
      userIsCreator: type.virtual().default(false)
    };

    const { model, isNew } = initializeModel({
      dal: activeDAL,
      baseTable: 'files',
      schema: fileSchema,
      camelToSnake: {
        uploadedBy: 'uploaded_by',
        uploadedOn: 'uploaded_on',
        mimeType: 'mime_type'
      },
      withRevision: {
        static: [
          'createFirstRevision',
          'getNotStaleOrDeleted',
          'filterNotStaleOrDeleted',
          'getMultipleNotStaleOrDeleted'
        ],
        instance: ['deleteAllRevisions']
      },
      staticMethods: {
        getStashedUpload,
        getValidLicenses,
        getFileFeed
      },
      instanceMethods: {
        populateUserInfo
      },
      relations: [
        {
          name: 'uploader',
          targetTable: 'users',
          sourceKey: 'uploaded_by',
          targetKey: 'id',
          hasRevisions: false,
          cardinality: 'one'
        },
        {
          name: 'things',
          targetTable: 'things',
          sourceKey: 'id',
          targetKey: 'id',
          hasRevisions: true,
          through: {
            table: 'thing_files',
            sourceForeignKey: 'file_id',
            targetForeignKey: 'thing_id'
          },
          cardinality: 'many'
        }
      ]
    });
    File = model;

    if (!isNew) {
      return File;
    }

    debug.db('PostgreSQL File model initialized with all methods');
    return File;
  } catch (error) {
    debug.error('Failed to initialize PostgreSQL File model:', error);
    return null;
  }
}

// NOTE: STATIC METHODS --------------------------------------------------------

/**
 * Get a stashed (incomplete) upload by user ID and name
 *
 * @param {String} userID - User ID who uploaded the file
 * @param {String} name - File name
 * @returns {Promise<File|undefined>} File instance or undefined if not found
 * @async
 */
async function getStashedUpload(userID, name) {
  const query = `
    SELECT * FROM ${File.tableName} 
    WHERE name = $1 
      AND uploaded_by = $2 
      AND completed = false 
      AND (_rev_deleted IS NULL OR _rev_deleted = false)
      AND (_old_rev_of IS NULL)
    LIMIT 1
  `;
  
  const result = await File.dal.query(query, [name, userID]);
  return result.rows.length > 0 ? File._createInstance(result.rows[0]) : undefined;
}

/**
 * Get array of valid license values
 *
 * @returns {String[]} Array of valid license strings
 */
function getValidLicenses() {
  return validLicenses.slice();
}

/**
 * Get a feed of completed files with pagination
 *
 * @param {Object} options - Feed options
 * @param {Date} options.offsetDate - Date for pagination offset
 * @param {Number} options.limit - Maximum number of items to return (default: 10)
 * @returns {Promise<Object>} Feed object with items and optional offsetDate
 * @async
 */
async function getFileFeed({ offsetDate, limit = 10 } = {}) {
  let query = File.filter({ completed: true })
    .filterNotStaleOrDeleted()
    .getJoin({ uploader: true })
    .orderBy('uploadedOn', 'DESC');

  if (offsetDate && offsetDate.valueOf) {
    query = query.filter(row => row('uploadedOn').lt(offsetDate));
  }

  // Get one extra to check for more results
  const items = await query.limit(limit + 1).run();

  const feed = {
    items: items.slice(0, limit)
  };

  // At least one additional document available, set offset for pagination
  if (items.length === limit + 1) {
    feed.offsetDate = feed.items[limit - 1].uploadedOn;
  }

  return feed;
}

// NOTE: INSTANCE METHODS ------------------------------------------------------

/**
 * Populate virtual permission fields in a File object with the rights of a
 * given user.
 *
 * @param {User} user - the user whose permissions to check
 * @memberof File
 * @instance
 */
function populateUserInfo(user) {
  if (!user) {
    return; // Permissions will be at their default value (false)
  }

  this.userIsCreator = user.id === this.uploadedBy;
  this.userCanDelete = this.userIsCreator || user.isSuperUser || user.isSiteModerator || false;
}

/**
 * Get the PostgreSQL File model (initialize if needed)
 * @param {DataAccessLayer} dal - Optional DAL instance for testing
*/
async function getPostgresFileModel(dal = null) {
  if (!File || dal) {
    File = await initializeFileModel(dal);
  }
  return File;
}

// Synchronous handle for production use - proxies to the registered model
// Create synchronous handle using the model handle factory
const { createAutoModelHandle } = require('../dal/lib/model-handle');

const FileHandle = createAutoModelHandle('files', initializeFileModel);

module.exports = FileHandle;

// Export factory function for fixtures and tests
module.exports.initializeModel = initializeFileModel;
module.exports.initializeFileModel = initializeFileModel; // Backward compatibility
module.exports.getPostgresFileModel = getPostgresFileModel;