Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import sequelize, {
2 type HasManyAddAssociationsMixin,
3 type HasManyGetAssociationsMixin,
4 type HasManyGetAssociationsMixinOptions,
5 type HasManySetAssociationsMixin,
6 type InferAttributes,
7 type InferCreationAttributes,
8 type Sequelize,
9} from 'sequelize';
10import { type JsonObject } from 'type-fest';
11
12import {
13 ActionType,
14 ItemTypeKind,
15 UserPenaltySeverity,
16} from '../../services/moderationConfigService/index.js';
17import { validateUrlOrNull } from '../../utils/url.js';
18import { type DataTypes } from '../index.js';
19import { type ItemType as TContentType } from './ItemTypeModel.js';
20
21const { Model } = sequelize;
22
23// The default type an Action sequelize model instance.
24// This type is vague, w/ more optional fields than we'll have at runtime, and
25// not accounting for the rules that we've set up in pg for how different action
26// type values constrain the values in other columns.
27export type CollapsedSequelizeAction = InstanceType<
28 ReturnType<typeof makeActionModel>
29>;
30
31// These types handle the different constraints per action type, mirroring pg.
32export type EnqueueToMrtAction = CollapsedSequelizeAction & {
33 actionType: (typeof ActionType)['ENQUEUE_TO_MRT'];
34 callbackUrl: null;
35};
36
37export type EnqueueToNcmecAction = CollapsedSequelizeAction & {
38 actionType: (typeof ActionType)['ENQUEUE_TO_NCMEC'];
39 callbackUrl: null;
40};
41
42export type CustomAction = CollapsedSequelizeAction & {
43 actionType: (typeof ActionType)['CUSTOM_ACTION'];
44 callbackUrl: string;
45};
46
47export type EnqueueAuthorToMrtAction = CollapsedSequelizeAction & {
48 actionType: (typeof ActionType)['ENQUEUE_AUTHOR_TO_MRT'];
49 callbackUrl: string;
50};
51
52// And this is the more precise replacement for UntypedAction, which we
53// use outside this file.
54export type SequelizeAction =
55 | EnqueueToMrtAction
56 | EnqueueToNcmecAction
57 | EnqueueAuthorToMrtAction
58 | CustomAction;
59
60/**
61 * Data Model for Actions. Actions are components
62 * of Rules that get executed if all Conditions are met.
63 * Examples of Actions are Delete, Enqueue, Log, etc.
64 */
65const makeActionModel = (sequelize: Sequelize, DataTypes: DataTypes) => {
66 class Action extends Model<
67 InferAttributes<Action>,
68 InferCreationAttributes<Action>
69 > {
70 public declare id: string;
71 public declare name: string;
72 public declare orgId: string;
73 public declare description: string | null;
74 public declare callbackUrl: string | null;
75 public declare callbackUrlHeaders: JsonObject | null;
76 public declare callbackUrlBody: JsonObject | null;
77 public declare customMrtApiParams: JsonObject | null;
78
79 public declare penalty: UserPenaltySeverity;
80 public declare actionType: ActionType;
81 public declare appliesToAllItemsOfKind: ItemTypeKind[];
82 public declare applyUserStrikes: boolean;
83
84 public declare addContentTypes: HasManyAddAssociationsMixin<
85 unknown,
86 string
87 >;
88 public declare setContentTypes: HasManySetAssociationsMixin<
89 unknown,
90 string
91 >;
92 private declare getContentTypesSequelizeImpl: HasManyGetAssociationsMixin<unknown>;
93
94 // eslint-disable-next-line @typescript-eslint/no-explicit-any
95 static associate(models: { [key: string]: any }) {
96 Action.belongsTo(models.Org, { as: 'org' });
97 Action.belongsToMany(models.Rule, {
98 through: 'rules_and_actions',
99 as: 'rules',
100 });
101
102 // Assign the default sequelize getContentTypes function to another
103 // name so that we can use it in the actual implemented function.
104 //
105 const contentTypeAssoc = Action.belongsToMany(models.ItemType, {
106 through: 'actions_and_item_types',
107 as: 'ContentTypes',
108 otherKey: 'item_type_id',
109 });
110 Object.defineProperty(
111 models.Action.prototype,
112 'getContentTypesSequelizeImpl',
113 {
114 enumerable: false,
115 value(...params: unknown[]) {
116 // eslint-disable-next-line @typescript-eslint/no-explicit-any
117 return (contentTypeAssoc as any)['get'](this, ...params);
118 },
119 },
120 );
121 }
122
123 async getContentTypes(
124 options?: HasManyGetAssociationsMixinOptions,
125 ): Promise<TContentType[]> {
126 const contentTypes =
127 this.appliesToAllItemsOfKind.length > 0
128 ? await this.sequelize.model('content_type').findAll({
129 ...options,
130 where: {
131 ...options?.where,
132 orgId: this.orgId,
133 kind: this.appliesToAllItemsOfKind,
134 },
135 })
136 : await this.getContentTypesSequelizeImpl(options);
137 return contentTypes as TContentType[];
138 }
139 }
140
141 /* Fields */
142 Action.init(
143 {
144 id: {
145 type: DataTypes.STRING,
146 primaryKey: true,
147 },
148 // Name of the action -- this must be unique for each Org (i.e. an Org can't
149 // have two actions with the same name)
150 name: {
151 type: DataTypes.STRING,
152 allowNull: false,
153 validate: { notEmpty: true },
154 },
155 orgId: {
156 type: DataTypes.STRING,
157 allowNull: false,
158 },
159 description: {
160 type: DataTypes.STRING,
161 allowNull: true,
162 },
163 callbackUrl: {
164 type: DataTypes.STRING,
165 allowNull: true,
166 validate: {
167 isValidUrl: validateUrlOrNull,
168 },
169 },
170 callbackUrlHeaders: {
171 type: DataTypes.JSONB,
172 allowNull: true,
173 },
174 callbackUrlBody: {
175 type: DataTypes.JSONB,
176 allowNull: true,
177 },
178 customMrtApiParams: {
179 type: DataTypes.ARRAY(DataTypes.JSONB),
180 allowNull: true,
181 },
182 penalty: {
183 type: DataTypes.STRING,
184 defaultValue: UserPenaltySeverity.NONE,
185 allowNull: false,
186 validate: {
187 isIn: [Object.values(UserPenaltySeverity)],
188 },
189 },
190 actionType: {
191 type: DataTypes.STRING,
192 defaultValue: ActionType.CUSTOM_ACTION,
193 allowNull: false,
194 validate: {
195 notNull: true,
196 isIn: [Object.values(ActionType)],
197 },
198 },
199 appliesToAllItemsOfKind: {
200 field: 'applies_to_all_items_of_kind',
201 type: DataTypes.ARRAY(DataTypes.ENUM(...Object.values(ItemTypeKind))),
202 defaultValue: [],
203 },
204 applyUserStrikes: {
205 type: DataTypes.BOOLEAN,
206 defaultValue: false,
207 allowNull: false,
208 },
209 },
210 {
211 sequelize,
212 modelName: 'action',
213 underscored: true,
214 },
215 );
216
217 return Action;
218};
219
220export default makeActionModel;