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

rogerpadilla / uql / 20685289522

04 Jan 2026 12:49AM UTC coverage: 95.658% (-0.1%) from 95.778%
20685289522

push

github

rogerpadilla
chore(release): publish

 - @uql/core@3.6.0

1303 of 1434 branches covered (90.86%)

Branch coverage included in aggregate %.

3434 of 3518 relevant lines covered (97.61%)

234.32 hits per line

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

94.2
/packages/core/src/querier/abstractQuerier.ts
1
import { getMeta } from '../entity/decorator/index.js';
2
import { GenericRepository } from '../repository/index.js';
3
import type {
4
  ExtraOptions,
5
  IdValue,
6
  Key,
7
  Querier,
8
  Query,
9
  QueryConflictPaths,
10
  QueryOne,
11
  QueryOptions,
12
  QuerySearch,
13
  QuerySelect,
14
  QueryUpdateResult,
15
  RelationKey,
16
  RelationValue,
17
  Repository,
18
  Type,
19
} from '../type/index.js';
20
import {
21
  augmentWhere,
22
  clone,
23
  filterPersistableRelationKeys,
24
  filterRelationKeys,
25
  getKeys,
26
  LoggerWrapper,
27
} from '../util/index.js';
28
import { Serialized } from './decorator/index.js';
29

30
/**
31
 * Base class for all database queriers.
32
 * It provides a standardized way to execute tasks serially to prevent race conditions on database connections.
33
 */
34
export abstract class AbstractQuerier implements Querier {
35
  /**
36
   * Internal promise used to queue database operations.
37
   * This ensures that each operation is executed serially, preventing race conditions
38
   * and ensuring that the database connection is used safely across concurrent calls.
39
   */
40
  private taskQueue: Promise<unknown> = Promise.resolve();
155✔
41
  protected readonly logger: LoggerWrapper;
42

43
  constructor(readonly extra?: ExtraOptions) {
155✔
44
    this.logger = new LoggerWrapper(extra?.logger, extra?.slowQueryThreshold);
155✔
45
  }
46

47
  findOneById<E>(entity: Type<E>, id: IdValue<E>, q: QueryOne<E> = {}) {
52✔
48
    const meta = getMeta(entity);
52✔
49
    q.$where = augmentWhere(meta, q.$where, id);
52✔
50
    return this.findOne(entity, q);
52✔
51
  }
52

53
  async findOne<E>(entity: Type<E>, q: QueryOne<E>) {
54
    const rows = await this.findMany(entity, { ...q, $limit: 1 });
90✔
55
    return rows[0];
90✔
56
  }
57

58
  abstract findMany<E>(entity: Type<E>, q: Query<E>): Promise<E[]>;
59

60
  findManyAndCount<E>(entity: Type<E>, q: Query<E>) {
61
    const qCount = {
1✔
62
      ...q,
63
    } satisfies QuerySearch<E>;
64
    delete qCount.$sort;
1✔
65
    delete qCount.$limit;
1✔
66
    delete qCount.$skip;
1✔
67
    return Promise.all([this.findMany(entity, q), this.count(entity, qCount)]);
1✔
68
  }
69

70
  abstract count<E>(entity: Type<E>, q: QuerySearch<E>): Promise<number>;
71

72
  async insertOne<E>(entity: Type<E>, payload: E) {
73
    const [id] = await this.insertMany(entity, [payload]);
190✔
74
    return id;
190✔
75
  }
76

77
  abstract insertMany<E>(entity: Type<E>, payload: E[]): Promise<IdValue<E>[]>;
78

79
  updateOneById<E>(entity: Type<E>, id: IdValue<E>, payload: E) {
80
    return this.updateMany(entity, { $where: id }, payload);
32✔
81
  }
82

83
  abstract updateMany<E>(entity: Type<E>, q: QuerySearch<E>, payload: E): Promise<number>;
84

85
  abstract upsertOne<E>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E): Promise<QueryUpdateResult>;
86

87
  deleteOneById<E>(entity: Type<E>, id: IdValue<E>, opts?: QueryOptions) {
88
    return this.deleteMany(entity, { $where: id }, opts);
3✔
89
  }
90

91
  abstract deleteMany<E>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
92

93
  async saveOne<E>(entity: Type<E>, payload: E) {
94
    const [id] = await this.saveMany(entity, [payload]);
8✔
95
    return id;
8✔
96
  }
97

98
  async saveMany<E>(entity: Type<E>, payload: E[]) {
99
    const meta = getMeta(entity);
57✔
100
    const ids: IdValue<E>[] = [];
57✔
101
    const updates: E[] = [];
57✔
102
    const inserts: E[] = [];
57✔
103

104
    for (const it of payload) {
57✔
105
      if (it[meta.id]) {
106✔
106
        if (getKeys(it).length === 1) {
3✔
107
          ids.push(it[meta.id]);
2✔
108
        } else {
109
          updates.push(it);
1✔
110
        }
111
      } else {
112
        inserts.push(it);
103✔
113
      }
114
    }
115

116
    return Promise.all([
57✔
117
      ...ids,
118
      ...(inserts.length ? await this.insertMany(entity, inserts) : []),
57✔
119
      ...updates.map(async (it) => {
120
        const { [meta.id]: id, ...data } = it;
1✔
121
        await this.updateOneById(entity, id, data as E);
1✔
122
        return id;
1✔
123
      }),
124
    ]);
125
  }
126

127
  protected async fillToManyRelations<E>(entity: Type<E>, payload: E[], select: QuerySelect<E>) {
128
    if (!payload.length) {
177✔
129
      return;
31✔
130
    }
131

132
    const meta = getMeta(entity);
146✔
133
    const relKeys = filterRelationKeys(meta, select);
146✔
134

135
    for (const relKey of relKeys) {
146✔
136
      const relOpts = meta.relations[relKey];
71✔
137
      const relEntity = relOpts.entity();
71✔
138
      type RelEntity = typeof relEntity;
139
      const relSelect = clone(select[relKey as string]);
71✔
140
      const relQuery: Query<RelEntity> =
141
        relSelect === true || relSelect === undefined
71✔
142
          ? {}
143
          : Array.isArray(relSelect)
40✔
144
            ? { $select: relSelect }
145
            : relSelect;
146
      const ids = payload.map((it) => it[meta.id]);
94✔
147

148
      if (relOpts.through) {
71✔
149
        const localField = relOpts.references[0].local;
17✔
150
        const throughEntity = relOpts.through();
17✔
151
        const throughMeta = getMeta(throughEntity);
17✔
152
        const targetRelKey = getKeys(throughMeta.relations).find((key) =>
17✔
153
          throughMeta.relations[key].references.some(({ local }) => local === relOpts.references[1].local),
29✔
154
        );
155
        const throughFounds = await this.findMany(throughEntity, {
17✔
156
          ...relQuery,
157
          $select: {
158
            [localField]: true,
159
            [targetRelKey]: {
160
              ...relQuery,
161
              $required: true,
162
            },
163
          },
164
          $where: {
165
            ...relQuery.$where,
166
            [localField]: ids,
167
          },
168
        });
169
        const founds = throughFounds.map((it) => ({ ...it[targetRelKey], [localField]: it[localField] }));
30✔
170
        this.putChildrenInParents(payload, founds, meta.id, localField, relKey);
17✔
171
      } else if (relOpts.cardinality === '1m') {
54✔
172
        const foreignField = relOpts.references[0].foreign;
24✔
173
        if (relQuery.$select) {
24✔
174
          if (Array.isArray(relQuery.$select)) {
12!
175
            if (!relQuery.$select.includes(foreignField as Key<RelEntity>)) {
12!
176
              relQuery.$select.push(foreignField as Key<RelEntity>);
12✔
177
            }
178
          } else if (!relQuery.$select[foreignField]) {
×
179
            relQuery.$select[foreignField] = true;
×
180
          }
181
        }
182
        relQuery.$where = { ...relQuery.$where, [foreignField]: ids };
24✔
183
        const founds = await this.findMany(relEntity, relQuery);
24✔
184
        this.putChildrenInParents(payload, founds, meta.id, foreignField, relKey);
24✔
185
      }
186
    }
187
  }
188

189
  protected putChildrenInParents<E>(
190
    parents: E[],
191
    children: E[],
192
    parentIdKey: string,
193
    referenceKey: string,
194
    relKey: string,
195
  ): void {
196
    const childrenByParentMap = children.reduce((acc, child) => {
41✔
197
      const parenId = child[referenceKey];
72✔
198
      if (!acc[parenId]) {
72✔
199
        acc[parenId] = [];
42✔
200
      }
201
      acc[parenId].push(child);
72✔
202
      return acc;
72✔
203
    }, {});
204

205
    for (const parent of parents) {
41✔
206
      const parentId = parent[parentIdKey];
49✔
207
      parent[relKey] = childrenByParentMap[parentId];
49✔
208
    }
209
  }
210

211
  protected async insertRelations<E>(entity: Type<E>, payload: E[]) {
212
    const meta = getMeta(entity);
309✔
213
    await Promise.all(
309✔
214
      payload.map((it) => {
215
        const relKeys = filterPersistableRelationKeys(meta, it, 'persist');
434✔
216
        if (!relKeys.length) {
434✔
217
          return Promise.resolve();
385✔
218
        }
219
        return Promise.all(relKeys.map((relKey) => this.saveRelation(entity, it, relKey)));
49✔
220
      }),
221
    );
222
  }
223

224
  protected async updateRelations<E>(entity: Type<E>, q: QuerySearch<E>, payload: E) {
225
    const meta = getMeta(entity);
54✔
226
    const relKeys = filterPersistableRelationKeys(meta, payload, 'persist');
54✔
227

228
    if (!relKeys.length) {
54✔
229
      return;
27✔
230
    }
231

232
    const founds = await this.findMany(entity, { ...q, $select: [meta.id] });
27✔
233
    const ids = founds.map((found) => found[meta.id]);
27✔
234

235
    await Promise.all(
27✔
236
      ids.map((id) =>
237
        Promise.all(relKeys.map((relKey) => this.saveRelation(entity, { ...payload, [meta.id]: id }, relKey, true))),
27✔
238
      ),
239
    );
240
  }
241

242
  protected async deleteRelations<E>(entity: Type<E>, ids: IdValue<E>[], opts?: QueryOptions) {
243
    const meta = getMeta(entity);
241✔
244
    const relKeys = filterPersistableRelationKeys(meta, meta.relations as E, 'delete');
241✔
245

246
    for (const relKey of relKeys) {
241✔
247
      const relOpts = meta.relations[relKey];
98✔
248
      const relEntity = relOpts.entity();
98✔
249
      const localField = relOpts.references[0].local;
98✔
250
      if (relOpts.through) {
98✔
251
        const throughEntity = relOpts.through();
16✔
252
        await this.deleteMany(throughEntity, { $where: { [localField]: ids } }, opts);
16✔
253
        return;
16✔
254
      }
255
      await this.deleteMany(relEntity, { [localField]: ids }, opts);
82✔
256
    }
257
  }
258

259
  protected async saveRelation<E>(entity: Type<E>, payload: E, relKey: RelationKey<E>, isUpdate?: boolean) {
260
    const meta = getMeta(entity);
76✔
261
    const id = payload[meta.id];
76✔
262
    const { entity: entityGetter, cardinality, references, through } = meta.relations[relKey];
76✔
263
    const relEntity = entityGetter();
76✔
264
    const relPayload = payload[relKey] as unknown as RelationValue<E>[];
76✔
265

266
    if (cardinality === '1m' || cardinality === 'mm') {
76✔
267
      if (through) {
61✔
268
        const localField = references[0].local;
15✔
269

270
        const throughEntity = through();
15✔
271
        if (isUpdate) {
15✔
272
          await this.deleteMany(throughEntity, { $where: { [localField]: id } });
7✔
273
        }
274
        if (relPayload) {
15!
275
          const savedIds = await this.saveMany(relEntity, relPayload);
15✔
276
          const throughBodies = savedIds.map((relId) => ({
30✔
277
            [references[0].local]: id,
278
            [references[1].local]: relId,
279
          }));
280
          await this.insertMany(throughEntity, throughBodies);
15✔
281
        }
282
        return;
15✔
283
      }
284
      const foreignField = references[0].foreign;
46✔
285
      if (isUpdate) {
46✔
286
        await this.deleteMany(relEntity, { $where: { [foreignField]: id } });
18✔
287
      }
288
      if (relPayload) {
46✔
289
        for (const it of relPayload) {
34✔
290
          it[foreignField] = id;
68✔
291
        }
292
        await this.saveMany(relEntity, relPayload);
34✔
293
      }
294
      return;
46✔
295
    }
296

297
    if (cardinality === '11') {
15✔
298
      const foreignField = references[0].foreign;
9✔
299
      if (relPayload === null) {
9✔
300
        await this.deleteMany(relEntity, { $where: { [foreignField]: id } });
1✔
301
        return;
1✔
302
      }
303
      await this.saveOne(relEntity, { ...relPayload, [foreignField]: id });
8✔
304
      return;
8✔
305
    }
306

307
    if (cardinality === 'm1' && relPayload) {
6!
308
      const localField = references[0].local;
6✔
309
      const referenceId = await this.insertOne(relEntity, relPayload);
6✔
310
      await this.updateOneById(entity, id, { [localField]: referenceId });
6✔
311
      return;
6✔
312
    }
313
  }
314

315
  getRepository<E>(entity: Type<E>): Repository<E> {
316
    return new GenericRepository(entity, this);
5✔
317
  }
318

319
  abstract readonly hasOpenTransaction: boolean;
320

321
  async transaction<T>(callback: () => Promise<T>) {
322
    try {
7✔
323
      await this.beginTransaction();
7✔
324
      const res = await callback();
7✔
325
      await this.commitTransaction();
6✔
326
      return res;
6✔
327
    } catch (err) {
328
      await this.rollbackTransaction();
1✔
329
      throw err;
1✔
330
    } finally {
331
      await this.release();
7✔
332
    }
333
  }
334

335
  async releaseIfFree() {
336
    if (!this.hasOpenTransaction) {
×
337
      await this.internalRelease();
×
338
    }
339
  }
340

341
  /**
342
   * Schedules a task to be executed serially in the querier instance.
343
   * This is used by the @Serialized decorator to protect database-level operations.
344
   *
345
   * @param task - The async task to execute.
346
   * @returns A promise that resolves with the task's result.
347
   */
348
  protected async serialize<T>(task: () => Promise<T>): Promise<T> {
349
    const res = this.taskQueue.then(task);
4,709✔
350
    this.taskQueue = res.catch(() => {});
4,709✔
351
    return res;
4,709✔
352
  }
353

354
  abstract beginTransaction(): Promise<void>;
355

356
  abstract commitTransaction(): Promise<void>;
357

358
  abstract rollbackTransaction(): Promise<void>;
359

360
  protected abstract internalRelease(): Promise<void>;
361

362
  @Serialized()
363
  async release(): Promise<void> {
364
    return this.internalRelease();
236✔
365
  }
366
}
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