• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hicommonwealth / commonwealth / 16321940014

16 Jul 2025 02:13PM UTC coverage: 39.771% (+0.06%) from 39.709%
16321940014

push

github

web-flow
Merge pull request #11992 from hicommonwealth/tim/truncate-edit-thread-body

Store truncated bodies in DB

1872 of 5062 branches covered (36.98%)

Branch coverage included in aggregate %.

6 of 6 new or added lines in 5 files covered. (100.0%)

3 existing lines in 1 file now uncovered.

3268 of 7862 relevant lines covered (41.57%)

36.19 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.09
/libs/model/src/models/utils.ts
1
import {
2
  decamelize,
3
  MAX_TRUNCATED_CONTENT_LENGTH,
4
  safeTruncateBody,
5
} from '@hicommonwealth/shared';
6
import { Model, type ModelStatic, type SyncOptions } from 'sequelize';
7
import type {
8
  Associable,
9
  FkMap,
10
  ManyToManyOptions,
11
  OneToManyOptions,
12
  OneToOneOptions,
13
  RuleOptions,
14
  State,
15
} from './types';
16

17
/**
18
 * Enforces fk naming convention
19
 */
20
function getDefaultFK<Source extends State, Target extends State>(
21
  source: ModelStatic<Model<Source>>,
22
  target: ModelStatic<Model<Target>>,
23
) {
24
  const fk = decamelize(`${source.name}_${source.primaryKeyAttribute}`);
5,883✔
25
  if (!target.getAttributes()[fk])
5,883!
26
    throw Error(
×
27
      `Table "${target.tableName}" missing foreign key field "${fk}" to "${source.tableName}"`,
28
    );
29
  return fk as keyof Target & string;
5,883✔
30
}
31

32
/**
33
 * Builds on-to-one association between two models
34
 * @param this source model with FK to target
35
 * @param target target model with FK to source
36
 * @param options one-to-one options
37
 */
38
export function oneToOne<Source extends State, Target extends State>(
39
  this: ModelStatic<Model<Source>> & Associable<Source>,
40
  target: ModelStatic<Model<Target>> & Associable<Target>,
41
  options?: OneToOneOptions<Source, Target>,
42
): ModelStatic<Model<Source>> & Associable<Source> {
43
  const targetKey = options?.targetKey ?? this.primaryKeyAttribute;
999✔
44
  const foreignKey = options?.foreignKey ?? getDefaultFK(this, target);
999✔
45

46
  // sequelize is not creating fk when fk = pk or fk in composite pk
47
  if (
999✔
48
    foreignKey === target.primaryKeyAttribute ||
1,554✔
49
    target.primaryKeyAttributes.includes(foreignKey)
50
  ) {
51
    mapFk(
555✔
52
      target,
53
      this,
54
      { primaryKey: [this.primaryKeyAttribute], foreignKey: [foreignKey] },
55
      {
56
        onUpdate: options?.onUpdate ?? 'NO ACTION',
888✔
57
        onDelete: options?.onDelete ?? 'NO ACTION',
666✔
58
      },
59
    );
60
  }
61

62
  target.belongsTo(this, {
999✔
63
    targetKey,
64
    foreignKey,
65
    as: options?.as,
66
    onUpdate: options?.onUpdate ?? 'NO ACTION',
1,665✔
67
    onDelete: options?.onDelete ?? 'NO ACTION',
1,332✔
68
  });
69

70
  this.hasOne(target, {
999✔
71
    foreignKey,
72
    onUpdate: options?.onUpdate ?? 'NO ACTION',
1,665✔
73
    onDelete: options?.onDelete ?? 'NO ACTION',
1,332✔
74
  });
75

76
  // don't forget to return this (fluent)
77
  return this;
999✔
78
}
79

80
/**
81
 * Builds on-to-many association between parent/child models
82
 * @param this parent model with PK
83
 * @param child child model with FK
84
 * @param options one-to-many options
85
 */
86
export function oneToMany<Parent extends State, Child extends State>(
87
  this: ModelStatic<Model<Parent>> & Associable<Parent>,
88
  child: ModelStatic<Model<Child>> & Associable<Child>,
89
  options?: OneToManyOptions<Parent, Child>,
90
): ModelStatic<Model<Parent>> & Associable<Parent> {
91
  const foreignKey = options?.foreignKey ?? getDefaultFK(this, child);
5,217✔
92

93
  const fk = Array.isArray(foreignKey) ? foreignKey[0] : foreignKey;
5,217✔
94
  this.hasMany(child, {
5,217✔
95
    foreignKey: { name: fk, allowNull: options?.optional },
96
    as: options?.asMany,
97
    onUpdate: options?.onUpdate ?? 'NO ACTION',
8,769✔
98
    onDelete: options?.onDelete ?? 'NO ACTION',
7,548✔
99
  });
100
  child.belongsTo(this, { foreignKey: fk, as: options?.asOne });
5,217✔
101

102
  // map fk when parent has composite pk
103
  if (Array.isArray(foreignKey))
5,217✔
104
    mapFk(
222✔
105
      child,
106
      this,
107
      {
108
        primaryKey: this.primaryKeyAttributes as Array<keyof Parent & string>,
109
        foreignKey,
110
      },
111
      {
112
        onUpdate: options?.onUpdate ?? 'NO ACTION',
333✔
113
        onDelete: options?.onDelete ?? 'NO ACTION',
222!
114
      },
115
    );
4,995✔
116
  // map fk when child has a composite pk that includes the fk,
117
  // or when fk = pk (sequelize is not creating fk when fk = pk)
118
  else if (
119
    (child.primaryKeyAttributes.length > 1 &&
10,212✔
120
      child.primaryKeyAttributes.includes(foreignKey)) ||
121
    foreignKey === child.primaryKeyAttribute
122
  )
123
    mapFk(
1,332✔
124
      child,
125
      this,
126
      { primaryKey: [this.primaryKeyAttribute], foreignKey: [foreignKey] },
127
      {
128
        onUpdate: options?.onUpdate ?? 'NO ACTION',
2,331✔
129
        onDelete: options?.onDelete ?? 'NO ACTION',
1,554✔
130
      },
131
    );
132

133
  // don't forget to return this (fluent)
134
  return this;
5,217✔
135
}
136

137
/**
138
 * Builds many-to-many association between three models (A->X<-B)
139
 * @param this cross-reference model with FKs to A and B
140
 * @param a [A model with PK, X->A fk field, aliases, fk rules]
141
 * @param b [B model with PK, X->B fk field, aliases, fk rules]
142
 */
143
export function manyToMany<X extends State, A extends State, B extends State>(
144
  this: ModelStatic<Model<X>> & Associable<X>,
145
  a: ManyToManyOptions<X, A> & Associable<A>,
146
  b: ManyToManyOptions<X, B> & Associable<B>,
147
): ModelStatic<Model<X>> & Associable<X> {
148
  const foreignKeyA = a.foreignKey ?? getDefaultFK(a.model, this);
888✔
149
  const foreignKeyB = b.foreignKey ?? getDefaultFK(b.model, this);
888✔
150

151
  this.belongsTo(a.model, {
888✔
152
    foreignKey: { name: foreignKeyA, allowNull: false },
153
    as: a.asOne,
154
    onUpdate: a.onUpdate ?? 'NO ACTION',
1,554✔
155
    onDelete: a.onDelete ?? 'NO ACTION',
1,221✔
156
  });
157
  this.belongsTo(b.model, {
888✔
158
    foreignKey: { name: foreignKeyB, allowNull: false },
159
    as: b.asOne,
160
    onUpdate: b.onUpdate ?? 'NO ACTION',
1,554✔
161
    onDelete: b.onDelete ?? 'NO ACTION',
1,221✔
162
  });
163
  a.model.hasMany(this, {
888✔
164
    foreignKey: { name: foreignKeyA, allowNull: false },
165
    hooks: a.hooks,
166
    as: a.as,
167
  });
168
  b.model.hasMany(this, {
888✔
169
    foreignKey: { name: foreignKeyB, allowNull: false },
170
    hooks: b.hooks,
171
    as: b.as,
172
  });
173
  a.model.belongsToMany(b.model, {
888✔
174
    through: this,
175
    foreignKey: foreignKeyA,
176
    as: a.asMany,
177
  });
178
  b.model.belongsToMany(a.model, {
888✔
179
    through: this,
180
    foreignKey: foreignKeyB,
181
    as: b.asMany,
182
  });
183

184
  // map fk when x-ref has composite pk
185
  if (this.primaryKeyAttributes.length > 1) {
888!
186
    mapFk(
888✔
187
      this,
188
      a.model,
189
      { primaryKey: [a.model.primaryKeyAttribute], foreignKey: [foreignKeyA] },
190
      {
191
        onUpdate: a.onUpdate ?? 'NO ACTION',
1,554✔
192
        onDelete: a.onDelete ?? 'NO ACTION',
1,221✔
193
      },
194
    );
195
    mapFk(
888✔
196
      this,
197
      b.model,
198
      { primaryKey: [b.model.primaryKeyAttribute], foreignKey: [foreignKeyB] },
199
      {
200
        onUpdate: b.onUpdate ?? 'NO ACTION',
1,554✔
201
        onDelete: b.onDelete ?? 'NO ACTION',
1,221✔
202
      },
203
    );
204
  }
205

206
  // don't forget to return this (fluent)
207
  return this;
888✔
208
}
209

210
/**
211
 * Maps composite FK constraints not supported by sequelize, with type safety
212
 * @param source model with FK
213
 * @param target model with PK
214
 * @param rules optional fk rules
215
 */
216
export function mapFk<Source extends State, Target extends State>(
217
  source: ModelStatic<Model<Source>> & Associable<Source>,
218
  target: ModelStatic<Model<Target>>,
219
  {
220
    primaryKey,
221
    foreignKey,
222
  }: {
223
    primaryKey: Array<keyof Target & string>;
224
    foreignKey: Array<keyof Source & string>;
225
  },
226
  rules?: RuleOptions,
227
) {
228
  const pk = primaryKey.map((k) => target.getAttributes()[k].field!);
4,107✔
229
  const fk = foreignKey.map((k) => source.getAttributes()[k].field!);
4,107✔
230
  const name = `${source.tableName}_${target.tableName.toLowerCase()}_${fk}_fkey`;
3,885✔
231
  source._fks.push({
3,885✔
232
    name,
233
    source: source.tableName,
234
    fk,
235
    target: target.tableName,
236
    pk,
237
    rules,
238
  });
239
}
240

241
/**
242
 * Creates composite FK constraints (not supported by sequelize)
243
 */
244
export const createFk = ({ name, source, fk, target, pk, rules }: FkMap) => `
1,505✔
245
  ALTER TABLE IF EXISTS "${source}"
246
    ADD CONSTRAINT "${name}"
247
      FOREIGN KEY (${fk.join(',')}) REFERENCES "${target}" (${pk.join(',')})
248
    ON
249
  UPDATE ${rules?.onUpdate ?? 'NO ACTION'}
1,505!
250
  ON
251
  DELETE ${rules?.onDelete ?? 'NO ACTION'};
1,505!
252
`;
253

254
/**
255
 * Drops composite FK constraints (not supported by sequelize)
256
 */
257
export const dropFk = ({ source, name }: FkMap) =>
45✔
258
  `ALTER TABLE IF EXISTS "${source}"
1,505✔
259
    DROP CONSTRAINT IF EXISTS "${name}";`;
260

261
/**
262
 * Model sync hooks that can be used to inspect sequelize generated scripts
263
 */
264
export const syncHooks = {
45✔
265
  beforeSync(options: SyncOptions) {
266
    options.logging = (sql) => {
×
267
      const s = sql.replace('Executing (default): ', '');
×
268
      if (!s.startsWith('SELECT')) {
×
269
        console.info('--', this);
×
270
        s.split(';').forEach((l) => console.info(l));
×
271
      }
272
    };
273
  },
274
  afterSync(options: SyncOptions) {
275
    options.logging = false;
×
276
  },
277
};
278

279
export const beforeValidateBodyHook = (instance: {
45✔
280
  body: string;
281
  content_url?: string | null | undefined;
282
}) => {
283
  if (!instance.body || instance.body.length <= MAX_TRUNCATED_CONTENT_LENGTH)
232!
284
    return;
232✔
285

UNCOV
286
  if (!instance.content_url) {
×
287
    throw new Error(
×
288
      'content_url must be defined if body ' +
289
        `length is greater than ${MAX_TRUNCATED_CONTENT_LENGTH}`,
290
    );
291
  } else
UNCOV
292
    instance.body = safeTruncateBody(
×
293
      instance.body,
294
      MAX_TRUNCATED_CONTENT_LENGTH,
295
    );
UNCOV
296
  return instance;
×
297
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc