@replyke/express
Version:
Replyke: Build interactive apps with social features like comments, votes, feeds, user lists, notifications, and more.
278 lines (277 loc) • 10.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const sequelize_1 = require("sequelize");
const short_uuid_1 = __importDefault(require("short-uuid"));
const Comment_1 = __importDefault(require("./Comment"));
const User_1 = __importDefault(require("./User"));
const Report_1 = __importDefault(require("./Report"));
class Entity extends sequelize_1.Model {
static initModel(sequelize) {
Entity.init({
id: {
type: sequelize_1.DataTypes.UUID,
defaultValue: sequelize_1.DataTypes.UUIDV4,
primaryKey: true,
allowNull: false,
},
projectId: {
type: sequelize_1.DataTypes.UUID,
allowNull: false,
},
userId: {
type: sequelize_1.DataTypes.UUID,
allowNull: true,
},
shortId: {
type: sequelize_1.DataTypes.STRING,
allowNull: false,
},
referenceId: {
type: sequelize_1.DataTypes.STRING,
allowNull: true,
},
foreignId: {
type: sequelize_1.DataTypes.STRING,
allowNull: true,
},
sourceId: {
type: sequelize_1.DataTypes.STRING,
allowNull: true,
},
resourceId: {
type: sequelize_1.DataTypes.STRING,
allowNull: true,
},
title: {
type: sequelize_1.DataTypes.TEXT,
allowNull: true,
validate: { len: [0, 300] },
set(value) {
const newValue = value && value.trim() !== "" ? value : null;
this.setDataValue("title", newValue);
},
},
content: {
type: sequelize_1.DataTypes.TEXT,
allowNull: true,
validate: { len: [0, 100000] },
set(value) {
const newValue = value && value.trim() !== "" ? value : null;
this.setDataValue("content", newValue);
},
},
media: {
type: sequelize_1.DataTypes.ARRAY(sequelize_1.DataTypes.JSONB),
allowNull: false,
defaultValue: [],
set(value) {
const sanitized = Array.isArray(value)
? value.filter((item) => item && typeof item === "object" && !Array.isArray(item))
: [];
this.setDataValue("media", sanitized);
},
validate: {
isArrayOfObjects(value) {
if (!Array.isArray(value)) {
throw new Error("Value must be an array.");
}
for (const item of value) {
if (!item || typeof item !== "object" || Array.isArray(item)) {
throw new Error("Each item must be a non-null object.");
}
}
},
},
},
attachments: {
type: sequelize_1.DataTypes.ARRAY(sequelize_1.DataTypes.JSONB),
allowNull: false,
defaultValue: [],
set(value) {
const sanitized = Array.isArray(value)
? value.filter((item) => item && typeof item === "object" && !Array.isArray(item))
: [];
this.setDataValue("attachments", sanitized);
},
validate: {
isArrayOfObjects(value) {
if (!Array.isArray(value)) {
throw new Error("Value must be an array.");
}
for (const item of value) {
if (!item || typeof item !== "object" || Array.isArray(item)) {
throw new Error("Each item must be a non-null object.");
}
}
},
},
},
mentions: {
type: sequelize_1.DataTypes.ARRAY(sequelize_1.DataTypes.JSONB),
allowNull: false,
defaultValue: [],
},
upvotes: {
type: sequelize_1.DataTypes.ARRAY(sequelize_1.DataTypes.UUID),
allowNull: false,
defaultValue: [],
},
downvotes: {
type: sequelize_1.DataTypes.ARRAY(sequelize_1.DataTypes.UUID),
allowNull: false,
defaultValue: [],
},
sharesCount: {
type: sequelize_1.DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
views: {
type: sequelize_1.DataTypes.INTEGER,
allowNull: false,
defaultValue: 1,
},
location: {
type: sequelize_1.DataTypes.GEOGRAPHY("POINT"),
allowNull: true,
},
keywords: {
type: sequelize_1.DataTypes.ARRAY(sequelize_1.DataTypes.TEXT),
allowNull: false,
defaultValue: [],
set(value) {
const filtered = value.filter((k) => k.trim() !== "");
this.setDataValue("keywords", filtered);
},
},
score: {
type: sequelize_1.DataTypes.FLOAT,
allowNull: false,
defaultValue: 2,
},
scoreUpdatedAt: {
type: sequelize_1.DataTypes.DATE,
allowNull: false,
defaultValue: sequelize_1.DataTypes.NOW,
},
metadata: {
type: sequelize_1.DataTypes.JSONB,
allowNull: false,
defaultValue: {},
validate: {
notTooLarge(value) {
const MAX_SIZE = 1024 * 1024; // 1 MB
if (value !== null) {
const size = Buffer.byteLength(JSON.stringify(value), "utf8");
if (size > MAX_SIZE) {
throw new Error(`Metadata exceeds the size limit of ${MAX_SIZE} bytes.`);
}
}
},
},
},
createdAt: {
type: sequelize_1.DataTypes.DATE,
allowNull: false,
defaultValue: sequelize_1.DataTypes.NOW,
},
updatedAt: {
type: sequelize_1.DataTypes.DATE,
allowNull: false,
defaultValue: sequelize_1.DataTypes.NOW,
},
deletedAt: {
type: sequelize_1.DataTypes.DATE,
allowNull: true,
},
}, {
sequelize,
modelName: "Entity",
tableName: "Entities",
timestamps: false,
paranoid: true,
indexes: [
{ unique: true, fields: ["projectId", "referenceId"] },
{
name: "idx_entities_by_foreignId",
fields: ["projectId", "foreignId"],
where: {
deletedAt: null,
},
},
{ unique: true, fields: ["projectId", "shortId"] },
{ fields: ["location"], using: "gist" },
{ fields: ["score"] },
{
fields: ["title"],
using: "gin",
operator: "gin_trgm_ops",
},
{
fields: ["content"],
using: "gin",
operator: "gin_trgm_ops",
},
{
fields: ["keywords"],
using: "gin",
},
{
name: "idx_entities_feed",
fields: [
{ name: "projectId" },
{ name: "sourceId" },
{ name: "createdAt", order: "DESC" },
],
where: {
deletedAt: null,
},
},
{
name: "idx_entities_by_shortid",
fields: [{ name: "projectId" }, { name: "shortId" }],
where: {
deletedAt: null,
},
},
],
});
Entity.beforeValidate((instance) => {
// only generate if it's not already set (e.g. on updates)
if (!instance.shortId) {
// id will have its defaultValue (UUIDV4) applied already
instance.shortId = (0, short_uuid_1.default)().fromUUID(instance.id);
}
});
// On update: only overwrite updatedAt *if* the user didn’t explicitly set it.
Entity.beforeUpdate((inst) => {
if (!inst.changed("updatedAt")) {
inst.setDataValue("updatedAt", new Date());
}
});
}
/**
* Define associations to other models
*/
static associate() {
Entity.hasMany(Comment_1.default, {
foreignKey: "entityId",
onDelete: "CASCADE", // If an Entity is deleted, all its Comments are deleted
});
// We don't cascade delete comments when users are remove to keep conversation integrity. We will just not show the user if there is no one
Entity.belongsTo(User_1.default, {
foreignKey: "userId",
as: "user", // Define the alias here
});
Entity.hasMany(Report_1.default, {
foreignKey: "targetId",
constraints: false, // Disable FK constraints for polymorphic relation
onDelete: "CASCADE",
scope: { targetType: "Entity" }, // Ensures this association only applies to Reports for Entities
});
}
}
exports.default = Entity;