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

doug-martin / nestjs-query / 6447619094

08 Oct 2023 12:30PM UTC coverage: 96.667%. Remained the same
6447619094

push

github

renovate[bot]
chore(deps): update dependency @types/supertest to v2.0.14

1796 of 1940 branches covered (0.0%)

Branch coverage included in aggregate %.

4962 of 5051 relevant lines covered (98.24%)

806.77 hits per line

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

79.31
/packages/query-typegoose/src/services/typegoose-query-service.ts
1
import {
2
  Query,
3
  Filter,
4
  AggregateQuery,
5
  AggregateResponse,
6
  FindByIdOptions,
7
  GetByIdOptions,
8
  DeepPartial,
9
  DeleteManyResponse,
10
  DeleteOneOptions,
11
  UpdateManyResponse,
12
  UpdateOneOptions,
13
  QueryService,
14
} from '@nestjs-query/core';
15
import { Base } from '@typegoose/typegoose/lib/defaultClasses';
16
import { ReturnModelType, DocumentType, mongoose } from '@typegoose/typegoose';
28✔
17
import { NotFoundException } from '@nestjs/common';
28✔
18
import { ReferenceQueryService } from './reference-query.service';
28✔
19
import { AggregateBuilder, FilterQueryBuilder } from '../query';
28✔
20
import { UpdateArrayQuery } from '../typegoose-types.helper';
21

22
export interface TypegooseQueryServiceOpts {
23
  toObjectOptions?: mongoose.ToObjectOptions;
24
}
25

26
export class TypegooseQueryService<Entity extends Base>
28✔
27
  extends ReferenceQueryService<Entity>
28
  implements QueryService<Entity>
29
{
30
  constructor(
31
    readonly Model: ReturnModelType<new () => Entity>,
72✔
32
    readonly filterQueryBuilder: FilterQueryBuilder<Entity> = new FilterQueryBuilder(Model),
72✔
33
  ) {
34
    super(Model);
72✔
35
  }
36

37
  /**
38
   * Query for multiple entities, using a Query from `@nestjs-query/core`.
39
   *
40
   * @example
41
   * ```ts
42
   * const todoItems = await this.service.query({
43
   *   filter: { title: { eq: 'Foo' } },
44
   *   paging: { limit: 10 },
45
   *   sorting: [{ field: "create", direction: SortDirection.DESC }],
46
   * });
47
   * ```
48
   * @param query - The Query used to filter, page, and sort rows.
49
   */
50
  async query(query: Query<Entity>): Promise<DocumentType<Entity>[]> {
51
    const { filterQuery, options } = this.filterQueryBuilder.buildQuery(query);
204✔
52
    const entities = await this.Model.find(filterQuery, {}, options).exec();
204✔
53
    return entities;
204✔
54
  }
55

56
  async aggregate(
57
    filter: Filter<Entity>,
58
    aggregateQuery: AggregateQuery<Entity>,
59
  ): Promise<AggregateResponse<Entity>[]> {
60
    const { aggregate, filterQuery, options } = this.filterQueryBuilder.buildAggregateQuery(aggregateQuery, filter);
52✔
61
    const aggPipeline: unknown[] = [{ $match: filterQuery }, { $group: aggregate }];
52✔
62
    if (options.sort) {
52✔
63
      aggPipeline.push({ $sort: options.sort ?? {} });
12!
64
    }
65
    const aggResult = (await this.Model.aggregate<Record<string, unknown>>(aggPipeline).exec()) as Record<
52✔
66
      string,
67
      unknown
68
    >[];
69
    return AggregateBuilder.convertToAggregateResponse(aggResult);
52✔
70
  }
71

72
  count(filter: Filter<Entity>): Promise<number> {
73
    const filterQuery = this.filterQueryBuilder.buildFilterQuery(filter);
76✔
74
    return this.Model.countDocuments(filterQuery).exec();
76✔
75
  }
76

77
  /**
78
   * Find an entity by it's `id`.
79
   *
80
   * @example
81
   * ```ts
82
   * const todoItem = await this.service.findById(1);
83
   * ```
84
   * @param id - The id of the record to find.
85
   * @param opts - Additional options
86
   */
87
  async findById(id: string | number, opts?: FindByIdOptions<Entity>): Promise<DocumentType<Entity> | undefined> {
88
    const filterQuery = this.filterQueryBuilder.buildIdFilterQuery(id, opts?.filter);
144✔
89
    const doc = await this.Model.findOne(filterQuery);
144✔
90
    if (!doc) {
144✔
91
      return undefined;
44✔
92
    }
93
    return doc;
100✔
94
  }
95

96
  /**
97
   * Gets an entity by it's `id`. If the entity is not found a rejected promise is returned.
98
   *
99
   * @example
100
   * ```ts
101
   * try {
102
   *   const todoItem = await this.service.getById(1);
103
   * } catch(e) {
104
   *   console.error('Unable to find entity with id = 1');
105
   * }
106
   * ```
107
   * @param id - The id of the record to find.
108
   * @param opts - Additional options
109
   */
110
  async getById(id: string, opts?: GetByIdOptions<Entity>): Promise<DocumentType<Entity>> {
111
    const doc = await this.findById(id, opts);
60✔
112
    if (!doc) {
60✔
113
      throw new NotFoundException(`Unable to find ${this.Model.modelName} with id: ${id}`);
16✔
114
    }
115
    return doc;
44✔
116
  }
117

118
  /**
119
   * Creates a single entity.
120
   *
121
   * @example
122
   * ```ts
123
   * const todoItem = await this.service.createOne({title: 'Todo Item', completed: false });
124
   * ```
125
   * @param record - The entity to create.
126
   */
127
  async createOne(record: DeepPartial<Entity>): Promise<DocumentType<Entity>> {
128
    this.ensureIdIsNotPresent(record);
60✔
129
    const doc = await this.Model.create(record);
52✔
130
    return doc;
52✔
131
  }
132

133
  /**
134
   * Create multiple entities.
135
   *
136
   * @example
137
   * ```ts
138
   * const todoItem = await this.service.createMany([
139
   *   {title: 'Todo Item 1', completed: false },
140
   *   {title: 'Todo Item 2', completed: true },
141
   * ]);
142
   * ```
143
   * @param records - The entities to create.
144
   */
145
  async createMany(records: DeepPartial<Entity>[]): Promise<DocumentType<Entity>[]> {
146
    records.forEach((r) => this.ensureIdIsNotPresent(r));
208✔
147
    const entities = await this.Model.create(records);
36✔
148
    return entities;
36✔
149
  }
150

151
  /**
152
   * Update an entity.
153
   *
154
   * @example
155
   * ```ts
156
   * const updatedEntity = await this.service.updateOne(1, { completed: true });
157
   * ```
158
   * @param id - The `id` of the record.
159
   * @param update - A `Partial` of the entity with fields to update.
160
   * @param opts - Additional options
161
   */
162
  async updateOne(
163
    id: string,
164
    update: DeepPartial<Entity>,
165
    opts?: UpdateOneOptions<Entity>,
166
  ): Promise<DocumentType<Entity>> {
167
    this.ensureIdIsNotPresent(update);
68✔
168
    const filterQuery = this.filterQueryBuilder.buildIdFilterQuery(id, opts?.filter);
60✔
169
    const doc = await this.Model.findOneAndUpdate(filterQuery, this.getUpdateQuery(update as DocumentType<Entity>), {
60✔
170
      new: true,
171
    });
172
    if (!doc) {
60✔
173
      throw new NotFoundException(`Unable to find ${this.Model.modelName} with id: ${id}`);
16✔
174
    }
175
    return doc;
44✔
176
  }
177

178
  /**
179
   * Update multiple entities with a `@nestjs-query/core` Filter.
180
   *
181
   * @example
182
   * ```ts
183
   * const { updatedCount } = await this.service.updateMany(
184
   *   { completed: true }, // the update to apply
185
   *   { title: { eq: 'Foo Title' } } // Filter to find records to update
186
   * );
187
   * ```
188
   * @param update - A `Partial` of entity with the fields to update
189
   * @param filter - A Filter used to find the records to update
190
   */
191
  async updateMany(update: DeepPartial<Entity>, filter: Filter<Entity>): Promise<UpdateManyResponse> {
192
    this.ensureIdIsNotPresent(update);
44✔
193
    const filterQuery = this.filterQueryBuilder.buildFilterQuery(filter);
36✔
194
    const res = await this.Model.updateMany(filterQuery, this.getUpdateQuery(update as DocumentType<Entity>)).exec();
36✔
195
    return { updatedCount: res.nModified || 0 };
36!
196
  }
197

198
  /**
199
   * Delete an entity by `id`.
200
   *
201
   * @example
202
   *
203
   * ```ts
204
   * const deletedTodo = await this.service.deleteOne(1);
205
   * ```
206
   *
207
   * @param id - The `id` of the entity to delete.
208
   * @param opts - Additional filter to use when finding the entity to delete.
209
   */
210
  async deleteOne(id: string, opts?: DeleteOneOptions<Entity>): Promise<DocumentType<Entity>> {
211
    const filterQuery = this.filterQueryBuilder.buildIdFilterQuery(id, opts?.filter);
44✔
212
    const doc = await this.Model.findOneAndDelete(filterQuery);
44✔
213
    if (!doc) {
44✔
214
      throw new NotFoundException(`Unable to find ${this.Model.modelName} with id: ${id}`);
16✔
215
    }
216
    return doc;
28✔
217
  }
218

219
  /**
220
   * Delete multiple records with a `@nestjs-query/core` `Filter`.
221
   *
222
   * @example
223
   *
224
   * ```ts
225
   * const { deletedCount } = this.service.deleteMany({
226
   *   created: { lte: new Date('2020-1-1') }
227
   * });
228
   * ```
229
   *
230
   * @param filter - A `Filter` to find records to delete.
231
   */
232
  async deleteMany(filter: Filter<Entity>): Promise<DeleteManyResponse> {
233
    const filterQuery = this.filterQueryBuilder.buildFilterQuery(filter);
20✔
234
    const res = await this.Model.deleteMany(filterQuery).exec();
20✔
235
    return { deletedCount: res.deletedCount || 0 };
20!
236
  }
237

238
  private ensureIdIsNotPresent(e: DeepPartial<Entity>): void {
239
    if (Object.entries(e).find(([k, v]) => (k === 'id' || k === '_id') && typeof v !== `undefined`)) {
1,388✔
240
      throw new Error('Id cannot be specified when updating or creating');
32✔
241
    }
242
  }
243

244
  private getUpdateQuery(entity: DocumentType<Entity>): mongoose.UpdateQuery<DocumentType<Entity>> {
245
    if (entity instanceof this.Model) {
96✔
246
      return entity.modifiedPaths().reduce(
8✔
247
        (update: mongoose.UpdateQuery<DocumentType<Entity>>, k) =>
248
          // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
249
          ({ ...update, [k]: entity.get(k) }),
8✔
250
        {},
251
      );
252
    }
253
    const arrayUpdateQuery: mongoose.UpdateQuery<unknown> = this.buildArrayUpdateQuery(entity as DeepPartial<Entity>);
88✔
254
    return { ...entity, ...arrayUpdateQuery } as mongoose.UpdateQuery<DocumentType<Entity>>;
88✔
255
  }
256

257
  private buildArrayUpdateQuery(entity: DeepPartial<Entity>) {
258
    // eslint-disable-next-line prefer-const
259
    let query = {
88✔
260
      $addToSet: {},
261
      $pull: {},
262
    } as UpdateArrayQuery<Entity>;
263

264
    Object.keys(entity).forEach((key) => {
88✔
265
      if (
160!
266
        this.Model.schema.path(key) instanceof mongoose.Schema.Types.Array &&
160!
267
        typeof entity[key as keyof Entity] === 'object'
268
      ) {
269
        // Converting the type of the object as it has the custom array input type.
270
        const convert = entity[key as keyof Entity] as unknown as { push: Entity[]; pull: Entity[] };
×
271

272
        if (Object.prototype.hasOwnProperty.call(convert, 'push')) {
×
273
          // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
274
          query.$addToSet[key] = { $each: convert.push };
×
275
        }
276

277
        if (Object.prototype.hasOwnProperty.call(convert, 'pull')) {
×
278
          query.$pull[key] = {};
×
279
          convert.pull.forEach((item, index) => {
×
280
            Object.keys(item).forEach((innerKey) => {
×
281
              if (query.$pull[key][innerKey] !== undefined) {
×
282
                query.$pull[key][innerKey].$in.push(convert.pull[index][innerKey as keyof Entity]);
×
283
              } else {
284
                query.$pull[key][innerKey] = { $in: [convert.pull[index][innerKey as keyof Entity]] };
×
285
              }
286
            });
287
          });
288
        }
289

290
        if (
×
291
          Object.prototype.hasOwnProperty.call(entity[key as keyof Entity], 'push') ||
×
292
          Object.prototype.hasOwnProperty.call(entity[key as keyof Entity], 'pull')
293
        ) {
294
          // eslint-disable-next-line no-param-reassign
295
          delete entity[key as keyof Entity];
×
296
        }
297
      }
298
    });
299

300
    return query;
88✔
301
  }
302
}
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

© 2025 Coveralls, Inc