universo-platform-3d

Форк
0
1964 строки · 58.6 Кб
1
import {
2
  BadRequestException,
3
  ConflictException,
4
  ForbiddenException,
5
  Inject,
6
  Injectable,
7
  InternalServerErrorException,
8
  Logger,
9
  NotFoundException,
10
  forwardRef
11
} from '@nestjs/common'
12
import { InjectModel } from '@nestjs/mongoose'
13

14
import { ObjectId } from 'mongodb'
15
import { FilterQuery, Model, PipelineStage, Types } from 'mongoose'
16
import {
17
  PURCHASE_OPTION_TYPE,
18
  PurchaseOption,
19
  PurchaseOptionDocument
20
} from '../marketplace/purchase-option.subdocument.schema'
21
import { ROLE } from '../roles/models/role.enum'
22
import { RoleService } from '../roles/role.service'
23
import {
24
  SpaceObject,
25
  SpaceObjectDocument
26
} from '../space-object/space-object.schema'
27
import { FileUploadService } from '../util/file-upload/file-upload.service'
28
import {
29
  AssetId,
30
  PurchaseOptionId,
31
  SpaceId,
32
  UserId,
33
  aggregationMatchId
34
} from '../util/mongo-object-id-helpers'
35
import {
36
  IPaginatedResponse,
37
  ISort,
38
  SORT_DIRECTION
39
} from '../util/pagination/pagination.interface'
40
import {
41
  PaginationService,
42
  PopulateField
43
} from '../util/pagination/pagination.service'
44
import { AssetUsageApiResponse } from './asset.models'
45
import { Asset, AssetDocument } from './asset.schema'
46
import { AssetSearch } from './asset.search'
47
import {
48
  CreateAssetDto,
49
  CreateMapDto,
50
  CreateMaterialDto,
51
  CreateTextureDto
52
} from './dto/create-asset.dto'
53
import { PaginatedSearchAssetDtoV2 } from './dto/paginated-search-asset.dto'
54
import {
55
  AddAssetPurchaseOptionDto,
56
  UpdateAssetDto
57
} from './dto/update-asset.dto'
58
import { UploadAssetFileDto } from './dto/upload-asset-file.dto'
59
import { MapAsset, MapDocument } from './map.schema'
60
import { Material, MaterialDocument } from './material.schema'
61
import { Texture, TextureDocument } from './texture.schema'
62
import { ASSET_MANAGER_UID } from '../mirror-server-config/asset-manager-uid'
63
import { UserService } from '../user/user.service'
64
import { AggregationPipelines } from '../util/aggregation-pipelines/aggregation-pipelines'
65
import { TAG_TYPES } from '../tag/models/tag-types.enum'
66
import { AddTagToAssetDto } from './dto/add-tag-to-asset.dto'
67
import { ThirdPartyTagEntity } from '../tag/models/tags.schema'
68
import { isArray } from 'lodash'
69
import { isEnum, isMongoId } from 'class-validator'
70
import { AssetAnalyzingService } from '../util/file-analyzing/asset-analyzing.service'
71
import { StorageFile } from '../storage/storage.file'
72
import { StorageService } from '../storage/storage.service'
73
import { Response } from 'express'
74

75
export type FileUploadServiceType = FileUploadService
76
@Injectable()
77
export class AssetService {
78
  constructor(
79
    @InjectModel(Asset.name) private assetModel: Model<AssetDocument>,
80
    // Material is a discriminator (subclass in Mongoose) of Asset
81
    @InjectModel(Material.name) private materialModel: Model<MaterialDocument>,
82
    // Texture is a discriminator (subclass in Mongoose) of Asset
83
    @InjectModel(Texture.name) private textureModel: Model<TextureDocument>,
84
    // MapAsset is a discriminator (subclass in Mongoose) of Asset
85
    @InjectModel(MapAsset.name) private mapAssetModel: Model<MapDocument>,
86
    @InjectModel(SpaceObject.name)
87
    private spaceObjectModel: Model<SpaceObjectDocument>,
88
    private readonly assetSearch: AssetSearch,
89
    @Inject(forwardRef(() => FileUploadService))
90
    private readonly fileUploadService: FileUploadService,
91
    private readonly paginationService: PaginationService,
92
    private readonly roleService: RoleService,
93
    @InjectModel(PurchaseOption.name)
94
    private purchaseModel: Model<PurchaseOptionDocument>,
95
    @Inject(forwardRef(() => UserService))
96
    private readonly userService: UserService,
97
    private readonly assetAnalyzingService: AssetAnalyzingService,
98
    private readonly storageService: StorageService,
99
    private readonly logger: Logger
100
  ) {}
101

102
  public readonly standardPopulateFields: PopulateField[] = [
103
    { localField: 'creator', from: 'users', unwind: true }, // TODO: filter out properties for user so that not all are passed back
104
    { localField: 'owner', from: 'users', unwind: true },
105
    { localField: 'customData', from: 'customdatas', unwind: true }
106
  ]
107
  /**
108
   * @deprecated use aggregation pipelines instead
109
   */
110
  private _getStandardPopulateFieldsAsArray(): string[] {
111
    return this.standardPopulateFields.map((f) => f.localField)
112
  }
113

114
  // business logic: default role for new Assets
115
  private readonly _defaultRoleForNewAssets = ROLE.OBSERVER
116

117
  public async getRecentInstancedAssets(
118
    userId: UserId,
119
    searchAssetDto?: PaginatedSearchAssetDtoV2
120
  ) {
121
    const userRecents = await this.userService.getUserRecents(userId)
122
    const assetsIds = userRecents?.assets?.instanced || []
123

124
    const pipelineQuery: PipelineStage[] =
125
      AggregationPipelines.getPipelineForGetByIdOrdered(assetsIds)
126

127
    if (!searchAssetDto?.includeSoftDeleted) {
128
      pipelineQuery.push({
129
        $match: {
130
          isSoftDeleted: { $exists: false }
131
        }
132
      })
133
    }
134

135
    return await this.assetModel.aggregate(pipelineQuery)
136
  }
137

138
  public async addInstancedAssetToRecents(assetId: AssetId, userId: UserId) {
139
    const userRecents = await this.userService.getUserRecents(userId)
140
    const assets = userRecents?.assets?.instanced || []
141

142
    const existingAssetIndex = assets.indexOf(assetId)
143

144
    if (existingAssetIndex >= 0) {
145
      assets.splice(existingAssetIndex, 1)
146
    } else if (assets.length === 10) {
147
      assets.pop()
148
    }
149

150
    assets.unshift(assetId)
151

152
    await this.userService.updateUserRecentInstancedAssets(userId, assets)
153
  }
154

155
  /**
156
   * START Section: Create methods for different schemas
157
   */
158

159
  public async createAsset(
160
    dto: CreateAssetDto & { ownerId: string }
161
  ): Promise<AssetDocument> {
162
    const created = new this.assetModel({
163
      owner: dto.ownerId,
164
      creator: dto.ownerId, // default to ownerId since that owner is creating it.
165
      ...dto
166
    })
167

168
    // check if assetsInPack has valid assets
169
    if (dto.assetPack && dto.assetsInPack) {
170
      // check if all elements is unique
171
      const assetsInPackToString = dto.assetsInPack.map((id) => id.toString())
172
      if (new Set(assetsInPackToString).size !== assetsInPackToString.length) {
173
        throw new BadRequestException('Asset pack contains duplicate assets')
174
      }
175

176
      // check if all assets in the pack are owned by the creator
177
      await Promise.all(
178
        dto.assetsInPack.map(async (id) => {
179
          const asset = await this.assetModel.findById(id)
180
          if (!asset) {
181
            throw new NotFoundException(`Asset with id ${id} not found`)
182
          }
183

184
          if (
185
            dto.ownerId !== asset.owner.toString() &&
186
            dto.ownerId !== asset.creator.toString()
187
          ) {
188
            throw new ForbiddenException(
189
              `Asset for pack with id ${id} is not owned by the creator`
190
            )
191
          }
192
        })
193
      )
194
    }
195

196
    // create role for this asset
197
    const role = await this.roleService.create({
198
      defaultRole: dto.defaultRole ?? this._defaultRoleForNewAssets,
199
      creator: dto.ownerId
200
    })
201
    created.role = role
202
    await created.save()
203

204
    // 2023-06-23 12:24:45 this is janky to do a find() now, but is a temp fix for a GD client request where we need to return the populated role after creating an asset. This will be fixed when role is changed from a separate collection to an embedded subdocument
205
    return await this.assetModel.findById(created.id).populate('role').exec()
206
  }
207

208
  public async createAssetWithUpload(
209
    dto: CreateAssetDto & { ownerId: string },
210
    file: Express.Multer.File
211
  ): Promise<AssetDocument> {
212
    try {
213
      const created = new this.assetModel({
214
        owner: dto.ownerId,
215
        creator: dto.ownerId, // default to ownerId since that owner is creating it.
216
        ...dto
217
      })
218

219
      // check if assetsInPack has valid assets
220
      if (dto.assetPack && dto.assetsInPack) {
221
        // check if all elements is unique
222
        const assetsInPackToString = dto.assetsInPack.map((id) => id.toString())
223
        if (
224
          new Set(assetsInPackToString).size !== assetsInPackToString.length
225
        ) {
226
          throw new BadRequestException('Asset pack contains duplicate assets')
227
        }
228

229
        // check if all assets in the pack are owned by the creator
230
        await Promise.all(
231
          dto.assetsInPack.map(async (id) => {
232
            const asset = await this.assetModel.findById(id)
233
            if (!asset) {
234
              throw new NotFoundException(`Asset with id ${id} not found`)
235
            }
236

237
            if (
238
              dto.ownerId !== asset.owner.toString() &&
239
              dto.ownerId !== asset.creator.toString()
240
            ) {
241
              throw new ForbiddenException(
242
                `Asset for pack with id ${id} is not owned by the creator`
243
              )
244
            }
245
          })
246
        )
247
      }
248

249
      const { publicUrl: currentFile } =
250
        await this.uploadAssetFilePublicWithRolesCheck({
251
          assetId: created.id,
252
          userId: dto.ownerId,
253
          file
254
        })
255

256
      created.currentFile = currentFile
257
      // create role for this asset
258
      const role = await this.roleService.create({
259
        defaultRole: dto.defaultRole ?? this._defaultRoleForNewAssets,
260
        creator: dto.ownerId
261
      })
262
      created.role = role
263
      await created.save()
264
      // 2023-06-23 12:24:45 this is janky to do a find() now, but is a temp fix for a GD client request where we need to return the populated role after creating an asset. This will be fixed when role is changed from a separate collection to an embedded subdocument
265
      return this.assetModel.findById(created.id).populate('role').exec()
266
    } catch (error: any) {
267
      throw error
268
    }
269
  }
270

271
  public async copyFreeAssetToNewUserWithRolesCheck(
272
    userId: UserId,
273
    assetId: AssetId
274
  ) {
275
    const asset = await this.findOneWithRolesCheck(userId, assetId)
276

277
    const isCopied = await this.checkIfAssetCopiedByUser(assetId, userId)
278

279
    if (isCopied?.isCopied) {
280
      throw new ConflictException('Asset already copied by user')
281
    }
282

283
    let isFree = true
284

285
    if (asset.purchaseOptions && asset.purchaseOptions.length > 0) {
286
      asset.purchaseOptions.forEach((option) => {
287
        if (
288
          option.enabled &&
289
          option.price &&
290
          option.type !== PURCHASE_OPTION_TYPE.ONE_TIME_OPTIONAL_DONATION
291
        ) {
292
          isFree = false
293
        }
294
      })
295
    }
296

297
    // if asset is free, copy it to the new user with a new role
298
    if (isFree) {
299
      // if asset is an asset pack, copy all assets in the pack
300
      if (asset.assetPack && asset.assetsInPack) {
301
        return await this.copyManyAssetsForNewUserAdmin(
302
          userId,
303
          asset.assetsInPack.map((id) => id.toString())
304
        )
305
      }
306

307
      const newAsset: any = asset
308
      newAsset._id = new ObjectId()
309
      // change the owner to the new user, creator must be the same
310
      newAsset.owner = userId
311
      newAsset.role = await this.roleService.create({
312
        defaultRole: this._defaultRoleForNewAssets,
313
        creator: userId
314
      })
315

316
      if (newAsset.purchaseOptions) {
317
        delete newAsset.purchaseOptions
318
      }
319

320
      // update the createdAt and updatedAt fields
321
      newAsset.createdAt = new Date()
322
      newAsset.updatedAt = new Date()
323
      // copy thumbnail and currentFile with new user id and new asset id
324
      if (newAsset.thumbnail) {
325
        const copyThumbnail = await this._copyAssetFileToNewUser(
326
          asset.thumbnail,
327
          this._changeAssetUrl(
328
            assetId,
329
            asset.creator.toString(),
330
            newAsset._id.toString(),
331
            userId,
332
            asset.thumbnail
333
          )
334
        )
335
        newAsset.thumbnail = copyThumbnail
336
      }
337

338
      if (newAsset.currentFile) {
339
        const copyCurrentFile = await this._copyAssetFileToNewUser(
340
          asset.currentFile,
341
          this._changeAssetUrl(
342
            assetId,
343
            asset.creator.toString(),
344
            newAsset._id.toString(),
345
            userId,
346
            asset.currentFile
347
          )
348
        )
349
        newAsset.currentFile = copyCurrentFile
350
      }
351

352
      newAsset.purchasedParentAssetId = assetId.toString()
353
      newAsset.mirrorPublicLibrary = false
354
      const copiedAsset = new this.assetModel(newAsset)
355
      return copiedAsset.save()
356
    }
357
    throw new BadRequestException('Asset is not free')
358
  }
359

360
  public copyAssetToNewUserAdmin(
361
    assetId: AssetId,
362
    newUserId: UserId
363
  ): Promise<AssetDocument> {
364
    return this.assetModel
365
      .findByIdAndUpdate(assetId, { owner: newUserId }, { new: true })
366
      .exec()
367
  }
368

369
  public async copyAssetToNewUserWithRolesCheck(
370
    assetId: AssetId,
371
    recievingUserId: UserId
372
  ): Promise<AssetDocument> {
373
    // check the role
374
    // TODO: how should we handle where a space is duplicatable, but the asset permissions aren't there?
375
    const check = await this.roleService.checkUserRoleForEntity(
376
      recievingUserId,
377
      assetId,
378
      ROLE.OWNER,
379
      this.assetModel
380
    )
381
    if (check === true) {
382
      return this.copyAssetToNewUserAdmin(assetId, recievingUserId)
383
    } else {
384
      throw new NotFoundException()
385
    }
386
  }
387

388
  // restore assets for space objects and return array of object with old and new asset ids
389
  public async restoreAssetsForSpaceObjects(assets: AssetDocument[]) {
390
    const newAssetsIds = []
391
    const bulkOps = []
392

393
    for (const asset of assets) {
394
      const newAssetId = new ObjectId()
395

396
      // if asset has a creator, use that, otherwise use owner (for old assets that don't have creator)
397
      const objCreator = asset.get('creator')
398
        ? asset.get('creator')
399
        : asset.get('owner')
400

401
      // if asset has a role, use that, otherwise create a new default role
402
      const role = asset.get('role')
403
        ? asset.get('role')
404
        : await this.roleService.create({
405
            defaultRole: this._defaultRoleForNewAssets,
406
            creator: asset.get('owner')
407
          })
408

409
      newAssetsIds.push({
410
        [asset.get('_id').toString()]: newAssetId.toString()
411
      })
412

413
      const bulkOp = {
414
        insertOne: {
415
          document: {
416
            ...Object.fromEntries(asset.toObject()),
417
            creator: objCreator,
418
            tags: asset.get('tags').length > 0 ? asset.get('tags') : undefined,
419
            role: role,
420
            _id: newAssetId
421
          }
422
        }
423
      }
424

425
      bulkOps.push(bulkOp)
426
    }
427

428
    await this.assetModel.bulkWrite(bulkOps)
429
    return newAssetsIds
430
  }
431

432
  /**
433
   * @description creates a Material Asset (subclass of Asset in Mongoose using a discriminator)
434
   */
435
  public async createMaterial(
436
    dto: CreateMaterialDto & { ownerId: string }
437
  ): Promise<MaterialDocument> {
438
    const created = new this.materialModel({
439
      owner: dto.ownerId,
440
      creator: dto.ownerId, // default to ownerId since that owner is creating it.
441
      ...dto
442
    })
443
    // create role for this asset
444
    const role = await this.roleService.create({
445
      defaultRole: dto.defaultRole ?? this._defaultRoleForNewAssets,
446
      creator: dto.ownerId
447
    })
448
    created.role = role
449
    await created.save()
450
    // 2023-06-23 12:24:45 this is janky to do a find() now, but is a temp fix for a GD client request where we need to return the populated role after creating an asset. This will be fixed when role is changed from a separate collection to an embedded subdocument
451
    return this.materialModel.findById(created.id).populate('role').exec()
452
  }
453

454
  /**
455
   * @description creates a Material Asset (subclass of Asset in Mongoose using a discriminator)
456
   */
457
  public async createMaterialWithUpload(
458
    dto: CreateMaterialDto & { ownerId: string },
459
    file: Express.Multer.File
460
  ): Promise<MaterialDocument> {
461
    try {
462
      const created = new this.materialModel({
463
        owner: dto.ownerId,
464
        creator: dto.ownerId, // default to ownerId since that owner is creating it.
465
        ...dto
466
      })
467

468
      const { publicUrl: currentFile } =
469
        await this.uploadAssetFilePublicWithRolesCheck({
470
          assetId: created.id,
471
          userId: dto.ownerId,
472
          file
473
        })
474

475
      created['currentFile'] = currentFile // should be dot notation, but need to figure out a way to tell Typescript that this is a union of Asset and Material
476

477
      // create role for this asset
478
      const role = await this.roleService.create({
479
        defaultRole: dto.defaultRole ?? this._defaultRoleForNewAssets,
480
        creator: dto.ownerId
481
      })
482

483
      created.role = role
484
      await created.save()
485
      // 2023-06-23 12:24:45 this is janky to do a find() now, but is a temp fix for a GD client request where we need to return the populated role after creating an asset. This will be fixed when role is changed from a separate collection to an embedded subdocument
486
      return this.materialModel.findById(created.id).populate('role').exec()
487
    } catch (error: any) {
488
      throw error
489
    }
490
  }
491

492
  /**
493
   * @description creates a Texture Asset (subclass of Asset in Mongoose using a discriminator)
494
   */
495
  public async createTexture(
496
    dto: CreateTextureDto & { ownerId: string }
497
  ): Promise<TextureDocument> {
498
    const created = new this.textureModel({
499
      owner: dto.ownerId,
500
      creator: dto.ownerId, // default to ownerId since that owner is creating it.
501
      ...dto
502
    })
503

504
    // create role for this asset
505
    const role = await this.roleService.create({
506
      defaultRole: dto.defaultRole ?? this._defaultRoleForNewAssets,
507
      creator: dto.ownerId
508
    })
509
    created.role = role
510

511
    await created.save()
512
    // 2023-06-23 12:24:45 this is janky to do a find() now, but is a temp fix for a GD client request where we need to return the populated role after creating an asset. This will be fixed when role is changed from a separate collection to an embedded subdocument
513
    return this.textureModel.findById(created.id).populate('role').exec()
514
  }
515

516
  /**
517
   * @description creates a Texture Asset (subclass of Asset in Mongoose using a discriminator)
518
   */
519
  public async createTextureWithUpload(
520
    dto: CreateTextureDto & { ownerId: string },
521
    file: Express.Multer.File
522
  ): Promise<TextureDocument> {
523
    try {
524
      const created = new this.textureModel({
525
        owner: dto.ownerId,
526
        creator: dto.ownerId, // default to ownerId since that owner is creating it.
527
        ...dto
528
      })
529

530
      const { publicUrl: currentFile } =
531
        await this.uploadAssetFilePublicWithRolesCheck({
532
          assetId: created.id,
533
          userId: dto.ownerId,
534
          file
535
        })
536

537
      created['currentFile'] = currentFile // should be dot notation, but need to figure out a way to tell Typescript that this is a union of Asset and Texture
538

539
      // create role for this asset
540
      const role = await this.roleService.create({
541
        defaultRole: dto.defaultRole ?? this._defaultRoleForNewAssets,
542
        creator: dto.ownerId
543
      })
544
      created.role = role
545

546
      await created.save()
547
      // 2023-06-23 12:24:45 this is janky to do a find() now, but is a temp fix for a GD client request where we need to return the populated role after creating an asset. This will be fixed when role is changed from a separate collection to an embedded subdocument
548
      return this.textureModel.findById(created.id).populate('role').exec()
549
    } catch (error: any) {
550
      throw error
551
    }
552
  }
553

554
  /**
555
   * @description creates a Material Asset (subclass of Asset in Mongoose using a discriminator)
556
   */
557
  public async createMap(
558
    dto: CreateMapDto & { ownerId: string }
559
  ): Promise<MapDocument> {
560
    const created = new this.mapAssetModel({
561
      owner: dto.ownerId,
562
      creator: dto.ownerId, // default to ownerId since that owner is creating it.
563
      ...dto
564
    })
565
    // create role for this asset
566
    const role = await this.roleService.create({
567
      defaultRole: dto.defaultRole ?? this._defaultRoleForNewAssets,
568
      creator: dto.ownerId
569
    })
570
    created.role = role
571
    await created.save()
572
    // 2023-06-23 12:24:45 this is janky to do a find() now, but is a temp fix for a GD client request where we need to return the populated role after creating an asset. This will be fixed when role is changed from a separate collection to an embedded subdocument
573
    return this.mapAssetModel.findById(created.id).populate('role').exec()
574
  }
575

576
  /**
577
   * @description creates a Material Asset (subclass of Asset in Mongoose using a discriminator)
578
   */
579
  public async createMapWithUpload(
580
    dto: CreateMapDto & { ownerId: string },
581
    file: Express.Multer.File
582
  ): Promise<MapDocument> {
583
    try {
584
      const created = new this.mapAssetModel({
585
        owner: dto.ownerId,
586
        creator: dto.ownerId, // default to ownerId since that owner is creating it.
587
        ...dto
588
      })
589

590
      const { publicUrl: currentFile } =
591
        await this.uploadAssetFilePublicWithRolesCheck({
592
          assetId: created.id,
593
          userId: dto.ownerId,
594
          file
595
        })
596

597
      created['currentFile'] = currentFile // should be dot notation, but need to figure out a way to tell Typescript that this is a union of Asset and Material
598

599
      // create role for this asset
600
      const role = await this.roleService.create({
601
        defaultRole: dto.defaultRole ?? this._defaultRoleForNewAssets,
602
        creator: dto.ownerId
603
      })
604

605
      created.role = role
606
      await created.save()
607
      // 2023-06-23 12:24:45 this is janky to do a find() now, but is a temp fix for a GD client request where we need to return the populated role after creating an asset. This will be fixed when role is changed from a separate collection to an embedded subdocument
608
      return this.mapAssetModel.findById(created.id).populate('role').exec()
609
    } catch (error: any) {
610
      throw error
611
    }
612
  }
613
  /**
614
   * END Section: Create functions for different schemas
615
   */
616

617
  /**
618
   * @description This is used ONLY for public assets that the user has designated as public. These are specific to the user and are not Mirror Library assets
619
   * @date 2022-06-18 16:00
620
   */
621

622
  public findAllPublicAssetsForUserWithRolesCheck(
623
    requestingUserId: UserId,
624
    targetUserId: UserId
625
  ): Promise<AssetDocument[]> {
626
    const pipeline = [
627
      ...this.roleService.getRoleCheckAggregationPipeline(
628
        requestingUserId,
629
        ROLE.DISCOVER
630
      ),
631
      { $match: { isSoftDeleted: { $exists: false } } },
632
      // get assets where the targetUser is an owner
633
      ...this.roleService.userIsOwnerAggregationPipeline(targetUserId)
634
    ]
635
    return this.assetModel.aggregate(pipeline).exec()
636
  }
637

638
  public async findManyAdmin(
639
    assetIds: Array<string>
640
  ): Promise<AssetDocument[]> {
641
    return await this.assetModel.find().where('_id').in(assetIds)
642
  }
643

644
  /**
645
   * Find self-created assets
646
   * TODO add pagination
647
   */
648
  public findAllAssetsForUserIncludingPrivate(
649
    userId: string,
650
    searchDto?: PaginatedSearchAssetDtoV2,
651
    sort: ISort = { updatedAt: SORT_DIRECTION.DESC }, // default: sort by updatedAt descending
652
    populate = false
653
  ): Promise<AssetDocument[]> {
654
    const filter: FilterQuery<any> = searchDto.includeSoftDeleted
655
      ? {
656
          $and: [{ owner: new ObjectId(userId) }]
657
        }
658
      : {
659
          $and: [
660
            { owner: new ObjectId(userId) },
661
            { isSoftDeleted: { $exists: false } }
662
          ]
663
        }
664

665
    const andFilter = AssetService.getSearchFilter(searchDto)
666
    if (andFilter.length > 0) {
667
      filter.$and.push(...andFilter)
668
    }
669

670
    const cursor = this.assetModel.find(filter).limit(1000).sort(sort)
671

672
    if (populate) {
673
      cursor.populate(this._getStandardPopulateFieldsAsArray())
674
    }
675

676
    return cursor.exec()
677
  }
678

679
  /**
680
   * Find public Mirror Library assets
681
   */
682
  public findMirrorPublicLibraryAssets(
683
    searchDto?: PaginatedSearchAssetDtoV2,
684
    sort: ISort = { updatedAt: SORT_DIRECTION.DESC }, // default: sort by updatedAt descending
685
    populate = false
686
  ): Promise<AssetDocument[]> {
687
    const filter: FilterQuery<any> = searchDto.includeSoftDeleted
688
      ? {
689
          $and: [{ mirrorPublicLibrary: true }]
690
        }
691
      : {
692
          $and: [
693
            { mirrorPublicLibrary: true },
694
            { isSoftDeleted: { $exists: false } }
695
          ]
696
        }
697

698
    const andFilter = AssetService.getSearchFilter(searchDto)
699
    if (andFilter.length > 0) {
700
      filter.$and.push(...andFilter)
701
    }
702

703
    const cursor = this.assetModel.find(filter).limit(1000).sort(sort)
704

705
    if (populate) {
706
      cursor.populate(this._getStandardPopulateFieldsAsArray())
707
    }
708

709
    return cursor.exec()
710
  }
711

712
  public findPaginatedMirrorAssetsWithRolesCheck(
713
    userId: UserId,
714
    searchDto?: PaginatedSearchAssetDtoV2,
715
    populate: PopulateField[] = [] // don't abuse, this is slow
716
  ): Promise<IPaginatedResponse<AssetDocument>> {
717
    const { page, perPage, startItem, numberOfItems, includeSoftDeleted } =
718
      searchDto
719

720
    const filter: FilterQuery<any> = includeSoftDeleted
721
      ? {
722
          mirrorPublicLibrary: true
723
        }
724
      : { mirrorPublicLibrary: true, isSoftDeleted: { $exists: false } }
725

726
    if (!searchDto?.includeAssetPackAssets) {
727
      filter.assetPack = { $ne: true }
728
    }
729

730
    const andFilter = AssetService.getSearchFilter(searchDto)
731
    if (andFilter.length > 0) {
732
      filter.$and = andFilter
733
    }
734

735
    let sort =
736
      searchDto.sortKey && searchDto.sortDirection !== undefined
737
        ? {
738
            [searchDto.sortKey]: searchDto.sortDirection
739
          }
740
        : undefined
741

742
    // if mirrorAssetManagerUserSortKey is undefined, sort.mirrorAssetManagerUser default sort direction is DESC
743
    if (searchDto.mirrorAssetManagerUserSortKey === undefined) {
744
      if (sort === undefined) sort = {}
745
      sort.mirrorAssetManagerUser = SORT_DIRECTION.DESC
746
    }
747

748
    // if mirrorAssetManagerUserSortKey is defined and not 0, sort.mirrorAssetManagerUser = searchDto.mirrorAssetManagerUserSortKey
749
    // if mirrorAssetManagerUserSortKey is defined and 0, this means that sorting by mirrorAssetManagerUser is disabled
750
    if (
751
      searchDto.mirrorAssetManagerUserSortKey &&
752
      searchDto.mirrorAssetManagerUserSortKey !== '0'
753
    ) {
754
      if (sort === undefined) sort = {}
755
      sort.mirrorAssetManagerUser = Number(
756
        searchDto.mirrorAssetManagerUserSortKey
757
      )
758
    }
759

760
    if (
761
      startItem !== null &&
762
      startItem !== undefined &&
763
      numberOfItems !== null &&
764
      numberOfItems !== undefined
765
    )
766
      return this.paginationService.getPaginatedQueryResponseByStartItemWithRolesCheck(
767
        userId,
768
        this.assetModel,
769
        filter,
770
        ROLE.OBSERVER,
771
        { startItem, numberOfItems },
772
        populate ? this.standardPopulateFields : [],
773
        sort
774
      )
775
    return this.paginationService.getPaginatedQueryResponseWithRolesCheck(
776
      userId,
777
      this.assetModel,
778
      filter,
779
      ROLE.OBSERVER,
780
      { page, perPage },
781
      populate ? this.standardPopulateFields : [],
782
      sort
783
    )
784
  }
785

786
  public findPaginatedMyAssetsWithRolesCheck(
787
    userId: string,
788
    searchDto?: PaginatedSearchAssetDtoV2,
789
    populate = false // don't use, this is slow
790
  ): Promise<IPaginatedResponse<AssetDocument>> {
791
    const { page, perPage, startItem, numberOfItems } = searchDto
792

793
    const filter: FilterQuery<any> = searchDto.includeSoftDeleted
794
      ? {}
795
      : { isSoftDeleted: { $exists: false } }
796

797
    if (!searchDto?.includeAssetPackAssets) {
798
      filter.assetPack = { $ne: true }
799
    }
800

801
    const andFilter = AssetService.getSearchFilter(searchDto)
802
    if (andFilter.length > 0) {
803
      filter.$and = andFilter
804
    }
805

806
    const sort =
807
      searchDto.sortKey && searchDto.sortDirection !== undefined
808
        ? {
809
            [searchDto.sortKey]: searchDto.sortDirection
810
          }
811
        : undefined
812

813
    if (
814
      startItem !== null &&
815
      startItem !== undefined &&
816
      numberOfItems !== null &&
817
      numberOfItems !== undefined
818
    )
819
      return this.paginationService.getPaginatedQueryResponseByStartItemWithRolesCheck(
820
        userId,
821
        this.assetModel,
822
        filter,
823
        ROLE.OWNER,
824
        { startItem, numberOfItems },
825
        populate ? this.standardPopulateFields : [],
826
        sort
827
      )
828

829
    return this.paginationService.getPaginatedQueryResponseWithRolesCheck(
830
      userId,
831
      this.assetModel,
832
      filter,
833
      ROLE.OWNER,
834
      { page, perPage },
835
      populate ? this.standardPopulateFields : [],
836
      sort
837
    )
838
  }
839

840
  public findAllAccessibleAssetsOfUser(
841
    userId: string,
842
    searchDto?: PaginatedSearchAssetDtoV2,
843
    populate = false // don't use, this is slow
844
  ): Promise<IPaginatedResponse<AssetDocument>> {
845
    const { page, perPage, startItem, numberOfItems } = searchDto
846

847
    const filter: FilterQuery<any> = searchDto?.includeSoftDeleted
848
      ? {
849
          $or: [{ mirrorPublicLibrary: true }, { owner: new ObjectId(userId) }]
850
        }
851
      : {
852
          $or: [{ mirrorPublicLibrary: true }, { owner: new ObjectId(userId) }],
853
          isSoftDeleted: { $exists: false }
854
        }
855

856
    if (!searchDto?.includeAssetPackAssets) {
857
      filter.assetPack = { $ne: true }
858
    }
859

860
    const andFilter = AssetService.getSearchFilter(searchDto)
861
    if (andFilter.length > 0) {
862
      filter.$and = andFilter
863
    }
864

865
    const sort =
866
      searchDto.sortKey && searchDto.sortDirection !== undefined
867
        ? {
868
            [searchDto.sortKey]: searchDto.sortDirection
869
          }
870
        : undefined
871
    if (
872
      startItem !== null &&
873
      startItem !== undefined &&
874
      numberOfItems !== null &&
875
      numberOfItems !== undefined
876
    )
877
      return this.paginationService.getPaginatedQueryResponseByStartItemWithRolesCheck(
878
        userId,
879
        this.assetModel,
880
        filter,
881
        ROLE.DISCOVER,
882
        { startItem, numberOfItems },
883
        populate ? this.standardPopulateFields : [],
884
        sort
885
      )
886
    return this.paginationService.getPaginatedQueryResponseWithRolesCheck(
887
      userId,
888
      this.assetModel,
889
      filter,
890
      ROLE.DISCOVER,
891
      { page, perPage },
892
      populate ? this.standardPopulateFields : [],
893
      sort
894
    )
895
  }
896

897
  /**
898
   * @description This is a helper method to get 1. Recently created/updated Assets and 2. The Assets of recently-created SpaceObjects
899
   * The intent is for a user to call this to get ITS OWN recent assets.
900
   * This should not be called for other users.
901
   * @date 2023-04-26 14:38
902
   */
903
  public async findRecentAssetsOfUserWithRolesCheck(
904
    userId: string,
905
    includeSoftDeleted = false,
906
    limit = 20,
907
    populate = false // don't use, this is slow
908
  ): Promise<AssetDocument[]> {
909
    // find recently updated spaceobjects owned by the user and get the asset IDs
910
    const spaceObjects: SpaceObjectDocument[] = (
911
      await this.paginationService.getPaginatedQueryResponseWithRolesCheck(
912
        userId,
913
        this.spaceObjectModel,
914
        {},
915
        ROLE.OWNER,
916
        { perPage: 500 } // don't use the above limit here since this is for SpaceObject, not Asset
917
      )
918
    ).data
919

920
    // get all the asset IDs from these SpaceObjects
921
    const spaceObjectAssetIds = spaceObjects
922
      .filter((spaceObject) => spaceObject.asset)
923
      .map((spaceObject) => spaceObject.asset.toString())
924

925
    const filter: FilterQuery<any> = {
926
      $or: [
927
        includeSoftDeleted
928
          ? { mirrorPublicLibrary: true }
929
          : { mirrorPublicLibrary: true, isSoftDeleted: { $exists: false } },
930
        // note that both are here since objectId vs string inconsistency currently
931
        { creator: userId }, // TODO this should really be owner, but we need to fix the pipeline order. If role isnt populated, we can't check for role.users[userId]
932
        { creator: new ObjectId(userId) }, // TODO this should really be owner, but we need to fix the pipeline order. If role isnt populated, we can't check for role.users[userId]
933
        { _id: { $in: spaceObjectAssetIds } }
934
      ]
935
    }
936

937
    const page = 1
938
    const perPage = limit
939
    const assetsPaginated =
940
      await this.paginationService.getPaginatedQueryResponseWithRolesCheck(
941
        userId,
942
        this.assetModel,
943
        filter,
944
        ROLE.DISCOVER, // note that DISCOVER is for any asset that can be used, but above, we check for ROLE.OWNER so that this user's assets show up
945
        { page, perPage },
946
        populate ? this.standardPopulateFields : []
947
      )
948

949
    return assetsPaginated.data
950
  }
951

952
  /**
953
   * @description Future note: this was implemented incorrectly. Static methods don't really have a use in NestJS since services are already singletons.
954
   * @date 2023-04-23 01:04
955
   */
956
  private static getSearchFilter(
957
    searchDto: PaginatedSearchAssetDtoV2
958
  ): Array<any> {
959
    const { search, field, type, tagType, tag, assetType, assetTypes } =
960
      searchDto
961

962
    const andFilter = []
963
    //override type with assetType if it exists (deprecating type to use assetType instead)
964
    let assetTypesAll: string[] = []
965
    if (assetType) {
966
      assetTypesAll.push(assetType.toUpperCase())
967
    } else if (type) {
968
      assetTypesAll.push(type.toUpperCase())
969
    }
970
    if (assetTypes) {
971
      assetTypesAll = assetTypesAll.concat(assetTypes)
972
    }
973
    if (assetTypesAll.length > 0) {
974
      andFilter.push({ assetType: { $in: assetTypesAll } })
975
    }
976

977
    if (field && search) {
978
      andFilter.push({
979
        $or: [
980
          { [field]: new RegExp(search, 'i') },
981
          { 'tags.search': new RegExp(search, 'i') }
982
        ]
983
      })
984
    }
985

986
    if (tag && tagType) {
987
      const tagSearchKey =
988
        tagType === TAG_TYPES.THIRD_PARTY
989
          ? `tags.${tagType}.name`
990
          : `tags.${tagType}`
991

992
      const tagFilter = { $or: tag.map((t) => ({ [tagSearchKey]: t })) }
993
      andFilter.push(tagFilter)
994
    }
995

996
    return andFilter
997
  }
998

999
  public findOneAdmin(id: AssetId): Promise<AssetDocument> {
1000
    return this.assetModel
1001
      .findById(id)
1002
      .populate(this._getStandardPopulateFieldsAsArray())
1003
      .exec()
1004
  }
1005

1006
  public async findOneWithRolesCheck(
1007
    userId: UserId,
1008
    assetId: AssetId
1009
  ): Promise<AssetDocument> {
1010
    const pipeline = [
1011
      { $match: { isSoftDeleted: { $exists: false } } },
1012
      aggregationMatchId(assetId),
1013
      ...this.roleService.getRoleCheckAggregationPipeline(userId, ROLE.OBSERVER)
1014
    ]
1015

1016
    const [asset]: AssetDocument[] = await this.assetModel
1017
      .aggregate(pipeline)
1018
      .exec()
1019

1020
    if (asset) {
1021
      return asset
1022
    } else {
1023
      throw new NotFoundException()
1024
    }
1025
  }
1026

1027
  public async findAssetUsageWithRolesCheck(
1028
    userId: UserId,
1029
    assetId: AssetId
1030
  ): Promise<AssetUsageApiResponse> {
1031
    const pipeline = [
1032
      aggregationMatchId(assetId),
1033
      ...this.roleService.getRoleCheckAggregationPipeline(userId, ROLE.DISCOVER)
1034
    ]
1035
    const data = await this.assetModel.aggregate(pipeline).exec()
1036
    if (data && data[0]) {
1037
      // find all space objects that use this asset
1038
      const spaceKeys = await this.spaceObjectModel
1039
        .find({
1040
          asset: assetId
1041
        })
1042
        .distinct('space')
1043
        .exec()
1044

1045
      return {
1046
        numberOfSpacesAssetUsedIn: spaceKeys.length
1047
      }
1048
    } else {
1049
      throw new NotFoundException()
1050
    }
1051
  }
1052

1053
  public updateOneAdmin(
1054
    id: string,
1055
    updateAssetDto: UpdateAssetDto
1056
  ): Promise<AssetDocument> {
1057
    return this.assetModel
1058
      .findByIdAndUpdate(id, updateAssetDto, { new: true })
1059
      .populate(this._getStandardPopulateFieldsAsArray())
1060
      .exec()
1061
  }
1062

1063
  public async updateOneWithRolesCheck(
1064
    userId: string,
1065
    assetId: AssetId,
1066
    updateAssetDto: UpdateAssetDto
1067
  ): Promise<AssetDocument | MapDocument | TextureDocument | MaterialDocument> {
1068
    // check the role
1069
    const roleCheck = await this.roleService.checkUserRoleForEntity(
1070
      userId,
1071
      assetId,
1072
      ROLE.MANAGER, // business logic: manager role is needed to update an asset
1073
      this.assetModel
1074
    )
1075

1076
    const softDeletedCheck = await this.isAssetSoftDeleted(assetId)
1077

1078
    if (roleCheck && !softDeletedCheck) {
1079
      // do === check here to avoid accidentally truthy since checkUserRoleForEntity returns a promise
1080

1081
      // Mongoose doesn't know about the discriminator classes and thus won't work with properties of the discriminator if the discriminator model isn't used.
1082
      switch (updateAssetDto.__t) {
1083
        case 'MapAsset':
1084
          return this.mapAssetModel
1085
            .findByIdAndUpdate(assetId, updateAssetDto, { new: true })
1086
            .populate(this._getStandardPopulateFieldsAsArray())
1087
            .exec()
1088
        case 'Material':
1089
          return this.materialModel
1090
            .findByIdAndUpdate(assetId, updateAssetDto, { new: true })
1091
            .populate(this._getStandardPopulateFieldsAsArray())
1092
            .exec()
1093
        case 'Texture':
1094
          return this.textureModel
1095
            .findByIdAndUpdate(assetId, updateAssetDto, { new: true })
1096
            .populate(this._getStandardPopulateFieldsAsArray())
1097
            .exec()
1098

1099
        default:
1100
          return this.assetModel
1101
            .findByIdAndUpdate(assetId, updateAssetDto, { new: true })
1102
            .populate(this._getStandardPopulateFieldsAsArray())
1103
            .exec()
1104
      }
1105
    } else {
1106
      throw new NotFoundException()
1107
    }
1108
  }
1109

1110
  public removeOneAdmin(id: string): Promise<AssetDocument> {
1111
    if (!Types.ObjectId.isValid(id)) {
1112
      throw new BadRequestException('ID is not a valid Mongo ObjectID')
1113
    }
1114
    return this.assetModel
1115
      .findOneAndDelete({ _id: id }, { new: true })
1116
      .exec()
1117
      .then((data) => {
1118
        if (data) {
1119
          return data
1120
        } else {
1121
          throw new NotFoundException()
1122
        }
1123
      })
1124
  }
1125

1126
  public async removeOneWithRolesCheck(
1127
    userId: UserId,
1128
    assetId: AssetId
1129
  ): Promise<AssetDocument> {
1130
    if (!Types.ObjectId.isValid(assetId)) {
1131
      throw new BadRequestException('ID is not a valid Mongo ObjectID')
1132
    }
1133

1134
    // check the role
1135
    const roleCheck = await this.roleService.checkUserRoleForEntity(
1136
      userId,
1137
      assetId,
1138
      ROLE.MANAGER, // business logic: manager role is needed to delete an asset
1139
      this.assetModel
1140
    )
1141

1142
    if (!roleCheck) {
1143
      throw new NotFoundException('Asset not found')
1144
    }
1145

1146
    const isAssetCanBeDeleted = await this._isAssetCanBeDeleted(assetId)
1147

1148
    if (!isAssetCanBeDeleted) {
1149
      throw new ConflictException(
1150
        'The asset cannot be deleted as it is referenced by one or more space objects'
1151
      )
1152
    }
1153

1154
    return await this.assetModel.findOneAndUpdate(
1155
      { _id: assetId },
1156
      { isSoftDeleted: true, softDeletedAt: new Date() },
1157
      { new: true }
1158
    )
1159
  }
1160

1161
  /** TODO add filter by public here
1162
   * */
1163
  public async searchAssetsPublic(
1164
    searchDto: PaginatedSearchAssetDtoV2,
1165
    populate = false
1166
  ): Promise<IPaginatedResponse<AssetDocument>> {
1167
    const { page, perPage } = searchDto
1168
    const matchFilter: FilterQuery<Asset> = {
1169
      $and: [
1170
        {
1171
          $or: [
1172
            { 'purchaseOptions.enabled': true },
1173
            { mirrorPublicLibrary: true }
1174
          ]
1175
        },
1176
        { isSoftDeleted: { $exists: false } }
1177
      ]
1178
    }
1179

1180
    if (!searchDto?.includeAssetPackAssets) {
1181
      matchFilter.$and.push({ assetPack: { $ne: true } })
1182
    }
1183

1184
    const andFilter = AssetService.getSearchFilter(searchDto)
1185

1186
    if (andFilter.length > 0) {
1187
      matchFilter.$and.push(...andFilter)
1188
    }
1189

1190
    let sort =
1191
      searchDto.sortKey && searchDto.sortDirection !== undefined
1192
        ? {
1193
            [searchDto.sortKey]: searchDto.sortDirection
1194
          }
1195
        : undefined
1196

1197
    // if sortKey is not 'mirrorPublicLibrary' default sort direction is DESC
1198
    if (searchDto.sortKey !== 'mirrorPublicLibrary') {
1199
      if (sort === undefined) sort = {}
1200
      sort.mirrorPublicLibrary = SORT_DIRECTION.DESC
1201
    }
1202

1203
    // if mirrorAssetManagerUserSortKey is undefined, sort.mirrorAssetManagerUser default sort direction is DESC
1204
    if (searchDto.mirrorAssetManagerUserSortKey === undefined) {
1205
      if (sort === undefined) sort = {}
1206
      sort.mirrorAssetManagerUser = SORT_DIRECTION.DESC
1207
    }
1208

1209
    // if mirrorAssetManagerUserSortKey is defined and not 0, sort.mirrorAssetManagerUser = searchDto.mirrorAssetManagerUserSortKey
1210
    // if mirrorAssetManagerUserSortKey is defined and 0, this means that sorting by mirrorAssetManagerUser is disabled
1211
    if (
1212
      searchDto.mirrorAssetManagerUserSortKey &&
1213
      searchDto.mirrorAssetManagerUserSortKey !== '0'
1214
    ) {
1215
      if (sort === undefined) sort = {}
1216
      sort.mirrorAssetManagerUser = Number(
1217
        searchDto.mirrorAssetManagerUserSortKey
1218
      )
1219
    }
1220

1221
    return await this.paginationService.getPaginatedQueryResponseAdmin(
1222
      this.assetModel,
1223
      matchFilter,
1224
      { page, perPage },
1225
      populate ? this.standardPopulateFields : [],
1226
      sort
1227
    )
1228
  }
1229

1230
  public async uploadAssetFilePublicWithRolesCheck({
1231
    userId,
1232
    assetId,
1233
    file
1234
  }: UploadAssetFileDto) {
1235
    // check the role
1236
    const check = await this.roleService.checkUserRoleForEntity(
1237
      userId,
1238
      assetId,
1239
      ROLE.MANAGER,
1240
      this.assetModel
1241
    )
1242

1243
    if (!check) {
1244
      throw new NotFoundException('Asset not found')
1245
    }
1246

1247
    const fileId = new Types.ObjectId()
1248
    const path = `${userId}/assets/${assetId}/files/${fileId.toString()}`
1249

1250
    const isAssetEquipable = await this.assetAnalyzingService.isAssetEquipable(
1251
      file
1252
    )
1253

1254
    const fileUploadResult = await this.fileUploadService.uploadFilePublic({
1255
      file,
1256
      path
1257
    })
1258

1259
    if (isAssetEquipable) {
1260
      await this.assetModel.updateOne({ _id: assetId }, { isEquipable: true })
1261
    }
1262

1263
    return fileUploadResult
1264
  }
1265

1266
  public async uploadAssetFileWithRolesCheck({
1267
    assetId,
1268
    userId,
1269
    file
1270
  }: UploadAssetFileDto) {
1271
    // check the role
1272
    const check = await this.roleService.checkUserRoleForEntity(
1273
      userId,
1274
      assetId,
1275
      ROLE.MANAGER,
1276
      this.assetModel
1277
    )
1278

1279
    if (!check) {
1280
      throw new NotFoundException('Asset not found')
1281
    }
1282

1283
    const fileId = new Types.ObjectId()
1284
    const path = `${userId}/assets/${assetId}/files/${fileId.toString()}`
1285

1286
    const isAssetEquipable = await this.assetAnalyzingService.isAssetEquipable(
1287
      file
1288
    )
1289

1290
    const fileUploadResult = await this.fileUploadService.uploadFilePrivate({
1291
      file,
1292
      path
1293
    })
1294

1295
    if (isAssetEquipable) {
1296
      await this.assetModel.updateOne({ _id: assetId }, { isEquipable: true })
1297
    }
1298

1299
    return fileUploadResult
1300
  }
1301

1302
  public async uploadAssetThumbnailWithRolesCheck({
1303
    assetId,
1304
    userId,
1305
    file
1306
  }: UploadAssetFileDto) {
1307
    // check the role
1308
    let check = await this.roleService.checkUserRoleForEntity(
1309
      userId,
1310
      assetId,
1311
      ROLE.MANAGER,
1312
      this.assetModel
1313
    )
1314
    // !! Special exception for thumbnails: if the account is EngAssetManager, then allow
1315
    if (userId === ASSET_MANAGER_UID) {
1316
      check = true
1317
    }
1318
    if (check === true) {
1319
      const path = `${userId}/assets/${assetId}/images/thumbnail`
1320
      return this.fileUploadService.uploadThumbnail({ file, path })
1321
    } else {
1322
      throw new NotFoundException()
1323
    }
1324
  }
1325

1326
  public async getPaginatedQueryResponseByStartItemWithRolesCheck(
1327
    userId: string,
1328
    searchDto?: PaginatedSearchAssetDtoV2,
1329
    populate = false // don't use, this is slowqueryParams: GetAssetDto
1330
  ) {
1331
    const { startItem, numberOfItems } = searchDto
1332

1333
    const filter: FilterQuery<any> = searchDto.includeSoftDeleted
1334
      ? {}
1335
      : { isSoftDeleted: { $exists: false } }
1336

1337
    if (!searchDto?.includeAssetPackAssets) {
1338
      filter.assetPack = { $ne: true }
1339
    }
1340

1341
    const andFilter = AssetService.getSearchFilter(searchDto)
1342
    if (andFilter.length > 0) {
1343
      filter.$and = andFilter
1344
    }
1345

1346
    return await this.paginationService.getPaginatedQueryResponseByStartItemWithRolesCheck(
1347
      userId,
1348
      this.assetModel,
1349
      filter,
1350
      ROLE.OWNER,
1351
      { startItem, numberOfItems },
1352
      populate ? this.standardPopulateFields : []
1353
    )
1354
  }
1355

1356
  public async addAssetPurchaseOption(
1357
    userId: string,
1358
    assetId: string,
1359
    data: AddAssetPurchaseOptionDto
1360
  ) {
1361
    // check the role
1362
    const check = await this.roleService.checkUserRoleForEntity(
1363
      userId,
1364
      assetId,
1365
      ROLE.OWNER, // business logic: manager role is needed to delete an asset
1366
      this.assetModel
1367
    )
1368

1369
    if (check === true) {
1370
      // Check the license type.
1371
      if (data.licenseType === PURCHASE_OPTION_TYPE.MIRROR_REV_SHARE) {
1372
        // Check MIRROR_REV_SHARE already exist or not
1373
        const checkPurchaseOptionExist = await this.assetModel.findOne({
1374
          _id: assetId,
1375
          purchaseOptions: { $elemMatch: { licenseType: data.licenseType } }
1376
        })
1377
        // Throw bad request exception if exist
1378
        if (checkPurchaseOptionExist) {
1379
          throw new BadRequestException(
1380
            'This asset is already set for RevShare'
1381
          )
1382
        }
1383
      }
1384
    } else {
1385
      throw new NotFoundException()
1386
    }
1387

1388
    const createdPurchaseOption = new this.purchaseModel(data)
1389
    await createdPurchaseOption.save()
1390
    return await this.assetModel.findByIdAndUpdate(
1391
      assetId,
1392
      { $push: { purchaseOptions: createdPurchaseOption } },
1393
      { new: true }
1394
    )
1395
  }
1396

1397
  public async getAssetsByIdsAdmin(
1398
    assetIds: AssetId[]
1399
  ): Promise<AssetDocument[]> {
1400
    return await this.assetModel.find({ _id: { $in: assetIds } }).exec()
1401
  }
1402

1403
  public async deleteAssetPurchaseOption(
1404
    userId: string,
1405
    assetId: string,
1406
    purchaseOptionId: string
1407
  ) {
1408
    // check the role
1409
    const check = await this.roleService.checkUserRoleForEntity(
1410
      userId,
1411
      assetId,
1412
      ROLE.OWNER, // business logic: manager role is needed to delete an asset
1413
      this.assetModel
1414
    )
1415

1416
    if (check === true) {
1417
      return await this.assetModel.findByIdAndUpdate(
1418
        assetId,
1419
        { $pull: { purchaseOptions: { _id: purchaseOptionId } } },
1420
        { new: true }
1421
      )
1422
    } else {
1423
      throw new NotFoundException()
1424
    }
1425
  }
1426

1427
  public async getAssetsByTag(
1428
    searchDto: PaginatedSearchAssetDtoV2,
1429
    userId: UserId = undefined
1430
  ) {
1431
    const { page, perPage, includeSoftDeleted } = searchDto
1432
    const matchFilter: FilterQuery<Asset> = {}
1433
    const andFilter = AssetService.getSearchFilter(searchDto)
1434

1435
    if (!includeSoftDeleted) {
1436
      andFilter.push({ isSoftDeleted: { $exists: false } })
1437
    }
1438

1439
    if (andFilter.length > 0) {
1440
      matchFilter.$and = andFilter
1441
    }
1442

1443
    if (!searchDto?.includeAssetPackAssets) {
1444
      matchFilter.$and.push({ assetPack: { $ne: true } })
1445
    }
1446

1447
    const sort =
1448
      searchDto.sortKey && searchDto.sortDirection !== undefined
1449
        ? {
1450
            [searchDto.sortKey]: searchDto.sortDirection
1451
          }
1452
        : undefined
1453

1454
    const paginatedAssetResult =
1455
      await this.paginationService.getPaginatedQueryResponseWithRolesCheck(
1456
        userId,
1457
        this.assetModel,
1458
        matchFilter,
1459
        ROLE.OBSERVER,
1460
        { page, perPage },
1461
        [],
1462
        sort
1463
      )
1464

1465
    return paginatedAssetResult
1466
  }
1467

1468
  public async addTagToAssetsWithRoleChecks(
1469
    userId: UserId,
1470
    addTagToAssetDto: AddTagToAssetDto
1471
  ) {
1472
    const { assetId, tagName, tagType, thirdPartySourceHomePageUrl } =
1473
      addTagToAssetDto
1474

1475
    const ownerRoleCheck = await this.roleService.checkUserRoleForEntity(
1476
      userId,
1477
      assetId,
1478
      ROLE.OWNER,
1479
      this.assetModel
1480
    )
1481

1482
    if (!ownerRoleCheck) {
1483
      throw new NotFoundException('Asset not found')
1484
    }
1485

1486
    if (thirdPartySourceHomePageUrl && tagType === TAG_TYPES.THIRD_PARTY) {
1487
      const tags = await this.getAssetTagsByType(assetId, tagType)
1488

1489
      const newThirdPartyTag = new ThirdPartyTagEntity(
1490
        tagName,
1491
        thirdPartySourceHomePageUrl
1492
      )
1493

1494
      return await this._updateAssetThirdPartyTags(
1495
        assetId,
1496
        tags as ThirdPartyTagEntity[],
1497
        newThirdPartyTag
1498
      )
1499
    }
1500

1501
    const tags = (await this.getAssetTagsByType(assetId, tagType)) as string[]
1502

1503
    if (tags.length === 15) {
1504
      throw new BadRequestException(`Asset already has 15 ${tagType} tags`)
1505
    }
1506

1507
    if (tags.includes(tagName)) {
1508
      throw new ConflictException(`Asset already has this ${tagType} tag`)
1509
    }
1510

1511
    tags.push(tagName)
1512
    await this._updateAssetTagsByType(assetId, tagType, tags)
1513

1514
    return tagName
1515
  }
1516

1517
  public async deleteTagFromAssetWithRoleChecks(
1518
    userId: UserId,
1519
    assetId: AssetId,
1520
    tagName: string,
1521
    tagType: TAG_TYPES
1522
  ) {
1523
    if (!isMongoId(assetId)) {
1524
      throw new BadRequestException('Id is not a valid Mongo ObjectId')
1525
    }
1526

1527
    if (!isEnum(tagType, TAG_TYPES)) {
1528
      throw new BadRequestException('Unknown tag type')
1529
    }
1530

1531
    const ownerRoleCheck = await this.roleService.checkUserRoleForEntity(
1532
      userId,
1533
      assetId,
1534
      ROLE.OWNER,
1535
      this.assetModel
1536
    )
1537

1538
    if (!ownerRoleCheck) {
1539
      throw new NotFoundException('Asset not found')
1540
    }
1541

1542
    const tagKey = `tags.${tagType}`
1543
    const valueToMatch =
1544
      tagType === TAG_TYPES.THIRD_PARTY ? { name: tagName } : tagName
1545

1546
    await this.assetModel
1547
      .updateOne({ _id: assetId }, { $pull: { [tagKey]: valueToMatch } })
1548
      .exec()
1549

1550
    return { assetId, tagType, tagName }
1551
  }
1552

1553
  public async getAssetTagsByType(assetId: AssetId, tagType: TAG_TYPES) {
1554
    const asset = await this.assetModel
1555
      .findOne({ _id: assetId })
1556
      .select('tags')
1557
      .exec()
1558

1559
    if (!asset) {
1560
      throw new NotFoundException('Asset not found')
1561
    }
1562

1563
    return asset?.tags?.[tagType] || []
1564
  }
1565

1566
  public async updateAssetTagsByTypeWithRoleChecks(
1567
    userId: UserId,
1568
    assetId: AssetId,
1569
    tagType: TAG_TYPES,
1570
    tags: string[] | ThirdPartyTagEntity[]
1571
  ) {
1572
    const ownerRoleCheck = await this.roleService.checkUserRoleForEntity(
1573
      userId,
1574
      assetId,
1575
      ROLE.OWNER,
1576
      this.assetModel
1577
    )
1578

1579
    if (!ownerRoleCheck) {
1580
      throw new NotFoundException('Asset not found')
1581
    }
1582

1583
    return await this._updateAssetTagsByType(assetId, tagType, tags)
1584
  }
1585

1586
  // This method should ONLY be used when the user has purchased assets. It's an admin method, hence why there aren't role checks
1587
  public async copyManyAssetsForNewUserAdmin(
1588
    userId: UserId,
1589
    assetIdList: AssetId[]
1590
  ) {
1591
    const assets = await this.assetModel.find({ _id: { $in: assetIdList } })
1592

1593
    await Promise.all(
1594
      assets.map(async (asset) => {
1595
        const newAsset: any = asset.toObject()
1596
        newAsset._id = new ObjectId()
1597
        // change the owner to the new user, creator must be the same
1598
        newAsset.owner = new ObjectId(userId)
1599

1600
        // create new default role for new copied asset
1601
        newAsset.role = await this.roleService.create({
1602
          defaultRole: this._defaultRoleForNewAssets,
1603
          creator: userId
1604
        })
1605

1606
        // update the createdAt and updatedAt fields
1607
        newAsset.createdAt = new Date()
1608
        newAsset.updatedAt = new Date()
1609

1610
        // remove purchase options from the copied asset
1611
        if (newAsset.purchaseOptions) {
1612
          delete newAsset.purchaseOptions
1613
        }
1614

1615
        // copy thumbnail and currentFile with new asset id and new user id
1616
        if (newAsset.thumbnail) {
1617
          const copyThumbnail = await this._copyAssetFileToNewUser(
1618
            asset.thumbnail,
1619
            this._changeAssetUrl(
1620
              asset._id.toString(),
1621
              asset.creator.toString(),
1622
              newAsset._id.toString(),
1623
              userId,
1624
              asset.thumbnail
1625
            )
1626
          )
1627
          newAsset.thumbnail = copyThumbnail
1628
        }
1629

1630
        if (newAsset.currentFile) {
1631
          const copyCurrentFile = await this._copyAssetFileToNewUser(
1632
            asset.currentFile,
1633
            this._changeAssetUrl(
1634
              asset._id.toString(),
1635
              asset.creator.toString(),
1636
              newAsset._id.toString(),
1637
              userId,
1638
              asset.currentFile
1639
            )
1640
          )
1641
          newAsset.currentFile = copyCurrentFile
1642
        }
1643

1644
        newAsset.purchasedParentAssetId = asset._id.toString()
1645
        newAsset.mirrorPublicLibrary = false
1646
        // create and save the new asset
1647
        const copiedAsset = new this.assetModel(newAsset)
1648
        await copiedAsset.save()
1649
      })
1650
    )
1651
    return
1652
  }
1653

1654
  public async checkIfAssetCopiedByUser(assetId: AssetId, userId: UserId) {
1655
    const asset = await this.assetModel.findOne({
1656
      purchasedParentAssetId: assetId,
1657
      owner: new ObjectId(userId)
1658
    })
1659

1660
    return {
1661
      isCopied: !!asset,
1662
      copiedAssetId: asset?._id
1663
    }
1664
  }
1665

1666
  public async downloadAssetFileWithRoleChecks(
1667
    userId: UserId,
1668
    assetId: AssetId,
1669
    res: Response
1670
  ) {
1671
    const asset = await this.findOneWithRolesCheck(userId, assetId)
1672

1673
    console.log('asset', asset)
1674
    if (!asset) {
1675
      throw new NotFoundException('Asset not found')
1676
    }
1677

1678
    const fileLink = asset.currentFile.replace(
1679
      'https://storage.googleapis.com/',
1680
      ''
1681
    )
1682
    const bucket = fileLink.split('/')[0]
1683
    const filePath = fileLink.replace(`${bucket}/`, '')
1684

1685
    let storageFile: StorageFile
1686

1687
    try {
1688
      storageFile = await this.storageService.get(bucket, filePath)
1689
    } catch (e) {
1690
      console.log('Error fetching file: ', e.message)
1691
      if (e.message.toString().includes('No such object')) {
1692
        throw new NotFoundException('File not found')
1693
      } else {
1694
        throw new InternalServerErrorException(
1695
          'Error fetching file: ',
1696
          e.message
1697
        )
1698
      }
1699
    }
1700
    res.setHeader('Content-Type', storageFile.contentType)
1701
    res.setHeader('Cache-Control', 'max-age=60d')
1702
    res.end(storageFile.buffer)
1703
  }
1704

1705
  async getAllAssetsBySpaceIdWithRolesCheck(spaceId: SpaceId, userId: UserId) {
1706
    const pipeline = [
1707
      { $match: { space: new ObjectId(spaceId) } },
1708
      {
1709
        $lookup: {
1710
          from: 'assets',
1711
          localField: 'asset',
1712
          foreignField: '_id',
1713
          as: 'assetInfo'
1714
        }
1715
      },
1716
      { $unwind: '$assetInfo' },
1717
      {
1718
        $replaceRoot: { newRoot: '$assetInfo' }
1719
      },
1720
      ...this.roleService.getRoleCheckAggregationPipeline(userId, ROLE.OBSERVER)
1721
    ]
1722
    return await this.spaceObjectModel.aggregate(pipeline).exec()
1723
  }
1724

1725
  async addAssetToPackWithRolesCheck(
1726
    packId: AssetId,
1727
    assetId: AssetId,
1728
    userId: UserId
1729
  ) {
1730
    const pack = await this.assetModel.findById(packId)
1731

1732
    if (!pack || !pack?.assetPack) {
1733
      throw new NotFoundException('Pack not found')
1734
    }
1735

1736
    const asset = await this.assetModel.findById(assetId)
1737

1738
    if (!asset) {
1739
      throw new NotFoundException('Asset not found')
1740
    }
1741

1742
    if (
1743
      asset.role.creator.toString() !== userId ||
1744
      pack.role.creator.toString() !== userId
1745
    ) {
1746
      throw new ForbiddenException(
1747
        'User does not have permission to add asset to pack'
1748
      )
1749
    }
1750

1751
    return await this.assetModel.findByIdAndUpdate(packId, {
1752
      $push: { assetsInPack: new ObjectId(assetId) }
1753
    })
1754
  }
1755

1756
  async deleteAssetFromPackWithRolesCheck(
1757
    packId: AssetId,
1758
    assetId: AssetId,
1759
    userId: UserId
1760
  ) {
1761
    const pack = await this.assetModel.findById(packId)
1762

1763
    if (!pack || !pack?.assetPack) {
1764
      throw new NotFoundException('Pack not found')
1765
    }
1766

1767
    if (pack.role.creator.toString() !== userId) {
1768
      throw new ForbiddenException(
1769
        'User does not have permission to delete asset from pack'
1770
      )
1771
    }
1772

1773
    if (!pack.assetsInPack.includes(new ObjectId(assetId))) {
1774
      throw new NotFoundException('Asset not found in pack')
1775
    }
1776

1777
    return await this.assetModel.findByIdAndUpdate(packId, {
1778
      $pull: { assetsInPack: new ObjectId(assetId) }
1779
    })
1780
  }
1781

1782
  private async _updateAssetTagsByType(
1783
    assetId: AssetId,
1784
    tagType: TAG_TYPES,
1785
    tags: string[] | ThirdPartyTagEntity[]
1786
  ) {
1787
    const searchKey = `tags.${tagType}`
1788

1789
    await this.assetModel
1790
      .updateOne({ _id: assetId }, { $set: { [searchKey]: tags } })
1791
      .exec()
1792

1793
    return tags
1794
  }
1795

1796
  private async _updateAssetThirdPartyTags(
1797
    assetId: AssetId,
1798
    thirdPartyTags: ThirdPartyTagEntity[],
1799
    newThirdPartyTag: ThirdPartyTagEntity
1800
  ) {
1801
    if (thirdPartyTags.length === 15) {
1802
      throw new BadRequestException(
1803
        `Space already has 15 ${TAG_TYPES.THIRD_PARTY} tags`
1804
      )
1805
    }
1806

1807
    const existingTag = thirdPartyTags.find(
1808
      (tag) => tag.name === newThirdPartyTag.name
1809
    )
1810

1811
    if (existingTag) {
1812
      throw new ConflictException(`Space already has this thirdParty tag`)
1813
    }
1814

1815
    thirdPartyTags.push(newThirdPartyTag)
1816

1817
    await this._updateAssetTagsByType(
1818
      assetId,
1819
      TAG_TYPES.THIRD_PARTY,
1820
      thirdPartyTags
1821
    )
1822

1823
    return newThirdPartyTag
1824
  }
1825

1826
  /**
1827
   * @description This is a helper method to determine if an asset can be deleted.
1828
   * (Check if there are space objects using this asset in existing spaces.)
1829
   *
1830
   * @date 2023-11-21
1831
   */
1832
  private async _isAssetCanBeDeleted(assetId: AssetId) {
1833
    const pipeline: PipelineStage[] = [
1834
      { $match: { asset: new ObjectId(assetId) } },
1835
      {
1836
        $lookup: {
1837
          from: 'spaces',
1838
          localField: 'space',
1839
          foreignField: '_id',
1840
          as: 'spaceInfo'
1841
        }
1842
      },
1843
      {
1844
        $match: {
1845
          spaceInfo: { $ne: [] }
1846
        }
1847
      },
1848
      {
1849
        $count: 'spaceObjectsCount'
1850
      },
1851
      {
1852
        $project: {
1853
          _id: 0,
1854
          spaceObjectsCount: 1
1855
        }
1856
      }
1857
    ]
1858

1859
    const [aggregationResult]: { spaceObjectsCount: number }[] =
1860
      await this.spaceObjectModel.aggregate(pipeline).exec()
1861

1862
    return !aggregationResult?.spaceObjectsCount
1863
  }
1864

1865
  /**
1866
   * @description This method is used to undo soft delete of an asset.
1867
   * (Remove isSoftDeleted and softDeletedAt fields from the asset document)
1868
   *
1869
   * @date 2023-11-23
1870
   */
1871
  public async undoAssetSoftDelete(userId: UserId, assetId: AssetId) {
1872
    if (!isMongoId(assetId)) {
1873
      throw new BadRequestException('AssetId is not a valid Mongo ObjectID')
1874
    }
1875

1876
    const roleCheck = await this.roleService.checkUserRoleForEntity(
1877
      userId,
1878
      assetId,
1879
      ROLE.MANAGER,
1880
      this.assetModel
1881
    )
1882

1883
    if (!roleCheck) {
1884
      throw new NotFoundException('Asset not found')
1885
    }
1886

1887
    await this.assetModel.updateOne(
1888
      { _id: new ObjectId(assetId) },
1889
      { $unset: { isSoftDeleted: 1, softDeletedAt: 1 } }
1890
    )
1891

1892
    return assetId
1893
  }
1894

1895
  public async isAssetSoftDeleted(assetId: AssetId) {
1896
    const asset = await this.assetModel.aggregate([
1897
      {
1898
        $match: { _id: new ObjectId(assetId), isSoftDeleted: true }
1899
      },
1900
      {
1901
        $project: {
1902
          _id: 1
1903
        }
1904
      }
1905
    ])
1906

1907
    return asset.length > 0
1908
  }
1909

1910
  private _changeAssetUrl(
1911
    parentAssetId: AssetId,
1912
    parentUserId: UserId,
1913
    assetId: AssetId,
1914
    userId: UserId,
1915
    parentUrl: string
1916
  ) {
1917
    return (parentUrl = parentUrl
1918
      .replace(parentAssetId, assetId)
1919
      .replace(parentUserId, userId))
1920
  }
1921

1922
  private async _copyAssetFileToNewUser(
1923
    parentFileUrl: string,
1924
    assetUrl: string
1925
  ) {
1926
    try {
1927
      if (process.env.ASSET_STORAGE_DRIVER === 'GCP') {
1928
        await this.fileUploadService.copyFileInBucket(
1929
          process.env.GCS_BUCKET_PUBLIC,
1930
          parentFileUrl.replace(
1931
            `https://storage.googleapis.com/${process.env.GCS_BUCKET_PUBLIC}/`,
1932
            ''
1933
          ),
1934
          assetUrl.replace(
1935
            `https://storage.googleapis.com/${process.env.GCS_BUCKET_PUBLIC}/`,
1936
            ''
1937
          )
1938
        )
1939
        return assetUrl
1940
      }
1941

1942
      if (
1943
        !process.env.ASSET_STORAGE_DRIVER ||
1944
        process.env.ASSET_STORAGE_DRIVER === 'LOCAL'
1945
      ) {
1946
        await this.fileUploadService.copyFileLocal(
1947
          parentFileUrl.replace(`${process.env.ASSET_STORAGE_URL}/`, ''),
1948
          assetUrl.replace(`${process.env.ASSET_STORAGE_URL}/`, '')
1949
        )
1950
        return assetUrl
1951
      }
1952
    } catch (error) {
1953
      this.logger.error(
1954
        `Error copying file from ${parentFileUrl} to ${assetUrl}`,
1955
        error
1956
      )
1957
      throw new InternalServerErrorException(
1958
        `Error copying file from ${parentFileUrl} to ${assetUrl}`
1959
      )
1960
    }
1961
  }
1962
}
1963

1964
export type AssetServiceType = AssetService // this is used to solve circular dependency issue with swc https://github.com/swc-project/swc/issues/5047#issuecomment-1302444311
1965

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.