Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import { ItemTypeKind } from '@roostorg/types';
2import _ from 'lodash';
3import sequelize, {
4 type BelongsToGetAssociationMixin,
5 type HasManyAddAssociationsMixin,
6 type HasManyGetAssociationsMixin,
7 type HasManyGetAssociationsMixinOptions,
8 type HasManySetAssociationsMixin,
9 type InferAttributes,
10 type InferCreationAttributes,
11 type Sequelize,
12} from 'sequelize';
13
14import { type ItemSchema } from '../../services/moderationConfigService/index.js';
15import { type DataTypes } from '../index.js';
16import { type Org } from '../OrgModel.js';
17import { type SequelizeAction } from './ActionModel.js';
18import { type Rule, type RuleWithLatestVersion } from './RuleModel.js';
19
20const { Model } = sequelize;
21
22export type ItemType = InstanceType<ReturnType<typeof makeItemTypeModel>>;
23
24const makeItemTypeModel = (sequelize: Sequelize, DataTypes: DataTypes) => {
25 class ItemType extends Model<
26 InferAttributes<ItemType>,
27 InferCreationAttributes<ItemType>
28 > {
29 public declare id: string;
30 public declare name: string;
31 public declare description?: string | null;
32 public declare fields: ItemSchema;
33 public declare getRules: HasManyGetAssociationsMixin<Rule>;
34
35 public declare orgId: string;
36 public declare getOrg: BelongsToGetAssociationMixin<Org>;
37 public declare kind: ItemTypeKind;
38
39 public declare addActions: HasManyAddAssociationsMixin<
40 SequelizeAction,
41 string
42 >;
43 public declare setActions: HasManySetAssociationsMixin<
44 SequelizeAction,
45 string
46 >;
47 private declare getActionsSequelizeImpl: HasManyGetAssociationsMixin<SequelizeAction>;
48
49 // eslint-disable-next-line @typescript-eslint/no-explicit-any
50 static associate(models: { [key: string]: any }) {
51 ItemType.belongsTo(models.Org, { as: 'Org' });
52 ItemType.belongsToMany(models.Rule, {
53 through: 'rules_and_item_types',
54 foreignKey: 'item_type_id',
55 as: 'Rules',
56 });
57
58 const actionAssoc = ItemType.belongsToMany(models.Action, {
59 through: 'actions_and_item_types',
60 foreignKey: 'item_type_id',
61 as: 'Actions',
62 });
63 Object.defineProperty(
64 models.ItemType.prototype,
65 'getActionsSequelizeImpl',
66 {
67 enumerable: false,
68 value(...params: unknown[]) {
69 // eslint-disable-next-line @typescript-eslint/no-explicit-any
70 return (actionAssoc as any)['get'](this, ...params);
71 },
72 },
73 );
74 }
75
76 /**
77 * This function returns the list of rules that are "enabled", meaning that
78 * we'd run them against a new piece of content of this content type, if the
79 * content were submitted right now. "Running the rule" just means checking
80 * if its conditions pass on the content; whether we'd run the actions of
81 * each passing rule is a different question.
82 *
83 * This function is _highly_ impure. Its results will change as rules
84 * expire, or as daily limits on rules are reached, among other things. As
85 * we see more use cases, we might wanna refactor where this lives.
86 */
87 async getEnabledRules() {
88 return this.getRules({
89 scope: 'enabled',
90 include: ['latestVersion'],
91 }) as Promise<RuleWithLatestVersion[]>;
92 }
93
94 async getActions(
95 options?: HasManyGetAssociationsMixinOptions,
96 ): Promise<SequelizeAction[]> {
97 return sequelize.transaction(async () => {
98 const [explicitlyAssociatedActions, allActions] = await Promise.all([
99 this.getActionsSequelizeImpl(options),
100 this.sequelize.model('action').findAll({
101 ...options,
102 where: {
103 ...options?.where,
104 orgId: this.orgId,
105 },
106 }),
107 ]);
108
109 return _.uniqBy(
110 [
111 // Do this filter outside of the sequelize query because Sequelize
112 // assumes the name of the enum and doesn't allow you to set it
113 ...(allActions as SequelizeAction[]).filter((it) =>
114 it.appliesToAllItemsOfKind.includes(this.kind),
115 ),
116 ...explicitlyAssociatedActions,
117 ],
118 (it) => it.id,
119 );
120 });
121 }
122 }
123
124 /* Fields */
125 ItemType.init(
126 {
127 id: {
128 type: DataTypes.STRING,
129 primaryKey: true,
130 },
131 orgId: {
132 type: DataTypes.STRING,
133 allowNull: false,
134 },
135 // Name of the item type, which must unique within each Org
136 name: {
137 type: DataTypes.STRING,
138 allowNull: false,
139 validate: { notEmpty: true },
140 },
141 description: {
142 type: DataTypes.STRING,
143 allowNull: true,
144 },
145 fields: {
146 type: DataTypes.ARRAY(DataTypes.JSONB),
147 allowNull: false,
148 validate: {
149 notEmpty: true,
150 },
151 },
152 kind: {
153 type: DataTypes.ENUM(...Object.values(ItemTypeKind)),
154 allowNull: false,
155 defaultValue: ItemTypeKind.CONTENT,
156 },
157 },
158 {
159 sequelize,
160 // legacy name; left as-is in case changing it will break auto-generated
161 // methods added by sequelize and calls to sequelize.model('content_type')
162 // and possibly many other things on which don't have great typescript
163 // support to check us.
164 modelName: 'content_type',
165 underscored: true,
166 tableName: 'item_types',
167 updatedAt: false,
168 },
169 );
170
171 return ItemType;
172};
173
174export default makeItemTypeModel;