universo-platform-3d
1964 строки · 58.6 Кб
1import {
2BadRequestException,
3ConflictException,
4ForbiddenException,
5Inject,
6Injectable,
7InternalServerErrorException,
8Logger,
9NotFoundException,
10forwardRef
11} from '@nestjs/common'
12import { InjectModel } from '@nestjs/mongoose'
13
14import { ObjectId } from 'mongodb'
15import { FilterQuery, Model, PipelineStage, Types } from 'mongoose'
16import {
17PURCHASE_OPTION_TYPE,
18PurchaseOption,
19PurchaseOptionDocument
20} from '../marketplace/purchase-option.subdocument.schema'
21import { ROLE } from '../roles/models/role.enum'
22import { RoleService } from '../roles/role.service'
23import {
24SpaceObject,
25SpaceObjectDocument
26} from '../space-object/space-object.schema'
27import { FileUploadService } from '../util/file-upload/file-upload.service'
28import {
29AssetId,
30PurchaseOptionId,
31SpaceId,
32UserId,
33aggregationMatchId
34} from '../util/mongo-object-id-helpers'
35import {
36IPaginatedResponse,
37ISort,
38SORT_DIRECTION
39} from '../util/pagination/pagination.interface'
40import {
41PaginationService,
42PopulateField
43} from '../util/pagination/pagination.service'
44import { AssetUsageApiResponse } from './asset.models'
45import { Asset, AssetDocument } from './asset.schema'
46import { AssetSearch } from './asset.search'
47import {
48CreateAssetDto,
49CreateMapDto,
50CreateMaterialDto,
51CreateTextureDto
52} from './dto/create-asset.dto'
53import { PaginatedSearchAssetDtoV2 } from './dto/paginated-search-asset.dto'
54import {
55AddAssetPurchaseOptionDto,
56UpdateAssetDto
57} from './dto/update-asset.dto'
58import { UploadAssetFileDto } from './dto/upload-asset-file.dto'
59import { MapAsset, MapDocument } from './map.schema'
60import { Material, MaterialDocument } from './material.schema'
61import { Texture, TextureDocument } from './texture.schema'
62import { ASSET_MANAGER_UID } from '../mirror-server-config/asset-manager-uid'
63import { UserService } from '../user/user.service'
64import { AggregationPipelines } from '../util/aggregation-pipelines/aggregation-pipelines'
65import { TAG_TYPES } from '../tag/models/tag-types.enum'
66import { AddTagToAssetDto } from './dto/add-tag-to-asset.dto'
67import { ThirdPartyTagEntity } from '../tag/models/tags.schema'
68import { isArray } from 'lodash'
69import { isEnum, isMongoId } from 'class-validator'
70import { AssetAnalyzingService } from '../util/file-analyzing/asset-analyzing.service'
71import { StorageFile } from '../storage/storage.file'
72import { StorageService } from '../storage/storage.service'
73import { Response } from 'express'
74
75export type FileUploadServiceType = FileUploadService
76@Injectable()
77export class AssetService {
78constructor(
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)
87private spaceObjectModel: Model<SpaceObjectDocument>,
88private readonly assetSearch: AssetSearch,
89@Inject(forwardRef(() => FileUploadService))
90private readonly fileUploadService: FileUploadService,
91private readonly paginationService: PaginationService,
92private readonly roleService: RoleService,
93@InjectModel(PurchaseOption.name)
94private purchaseModel: Model<PurchaseOptionDocument>,
95@Inject(forwardRef(() => UserService))
96private readonly userService: UserService,
97private readonly assetAnalyzingService: AssetAnalyzingService,
98private readonly storageService: StorageService,
99private readonly logger: Logger
100) {}
101
102public 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*/
110private _getStandardPopulateFieldsAsArray(): string[] {
111return this.standardPopulateFields.map((f) => f.localField)
112}
113
114// business logic: default role for new Assets
115private readonly _defaultRoleForNewAssets = ROLE.OBSERVER
116
117public async getRecentInstancedAssets(
118userId: UserId,
119searchAssetDto?: PaginatedSearchAssetDtoV2
120) {
121const userRecents = await this.userService.getUserRecents(userId)
122const assetsIds = userRecents?.assets?.instanced || []
123
124const pipelineQuery: PipelineStage[] =
125AggregationPipelines.getPipelineForGetByIdOrdered(assetsIds)
126
127if (!searchAssetDto?.includeSoftDeleted) {
128pipelineQuery.push({
129$match: {
130isSoftDeleted: { $exists: false }
131}
132})
133}
134
135return await this.assetModel.aggregate(pipelineQuery)
136}
137
138public async addInstancedAssetToRecents(assetId: AssetId, userId: UserId) {
139const userRecents = await this.userService.getUserRecents(userId)
140const assets = userRecents?.assets?.instanced || []
141
142const existingAssetIndex = assets.indexOf(assetId)
143
144if (existingAssetIndex >= 0) {
145assets.splice(existingAssetIndex, 1)
146} else if (assets.length === 10) {
147assets.pop()
148}
149
150assets.unshift(assetId)
151
152await this.userService.updateUserRecentInstancedAssets(userId, assets)
153}
154
155/**
156* START Section: Create methods for different schemas
157*/
158
159public async createAsset(
160dto: CreateAssetDto & { ownerId: string }
161): Promise<AssetDocument> {
162const created = new this.assetModel({
163owner: dto.ownerId,
164creator: dto.ownerId, // default to ownerId since that owner is creating it.
165...dto
166})
167
168// check if assetsInPack has valid assets
169if (dto.assetPack && dto.assetsInPack) {
170// check if all elements is unique
171const assetsInPackToString = dto.assetsInPack.map((id) => id.toString())
172if (new Set(assetsInPackToString).size !== assetsInPackToString.length) {
173throw new BadRequestException('Asset pack contains duplicate assets')
174}
175
176// check if all assets in the pack are owned by the creator
177await Promise.all(
178dto.assetsInPack.map(async (id) => {
179const asset = await this.assetModel.findById(id)
180if (!asset) {
181throw new NotFoundException(`Asset with id ${id} not found`)
182}
183
184if (
185dto.ownerId !== asset.owner.toString() &&
186dto.ownerId !== asset.creator.toString()
187) {
188throw 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
197const role = await this.roleService.create({
198defaultRole: dto.defaultRole ?? this._defaultRoleForNewAssets,
199creator: dto.ownerId
200})
201created.role = role
202await 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
205return await this.assetModel.findById(created.id).populate('role').exec()
206}
207
208public async createAssetWithUpload(
209dto: CreateAssetDto & { ownerId: string },
210file: Express.Multer.File
211): Promise<AssetDocument> {
212try {
213const created = new this.assetModel({
214owner: dto.ownerId,
215creator: dto.ownerId, // default to ownerId since that owner is creating it.
216...dto
217})
218
219// check if assetsInPack has valid assets
220if (dto.assetPack && dto.assetsInPack) {
221// check if all elements is unique
222const assetsInPackToString = dto.assetsInPack.map((id) => id.toString())
223if (
224new Set(assetsInPackToString).size !== assetsInPackToString.length
225) {
226throw new BadRequestException('Asset pack contains duplicate assets')
227}
228
229// check if all assets in the pack are owned by the creator
230await Promise.all(
231dto.assetsInPack.map(async (id) => {
232const asset = await this.assetModel.findById(id)
233if (!asset) {
234throw new NotFoundException(`Asset with id ${id} not found`)
235}
236
237if (
238dto.ownerId !== asset.owner.toString() &&
239dto.ownerId !== asset.creator.toString()
240) {
241throw new ForbiddenException(
242`Asset for pack with id ${id} is not owned by the creator`
243)
244}
245})
246)
247}
248
249const { publicUrl: currentFile } =
250await this.uploadAssetFilePublicWithRolesCheck({
251assetId: created.id,
252userId: dto.ownerId,
253file
254})
255
256created.currentFile = currentFile
257// create role for this asset
258const role = await this.roleService.create({
259defaultRole: dto.defaultRole ?? this._defaultRoleForNewAssets,
260creator: dto.ownerId
261})
262created.role = role
263await 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
265return this.assetModel.findById(created.id).populate('role').exec()
266} catch (error: any) {
267throw error
268}
269}
270
271public async copyFreeAssetToNewUserWithRolesCheck(
272userId: UserId,
273assetId: AssetId
274) {
275const asset = await this.findOneWithRolesCheck(userId, assetId)
276
277const isCopied = await this.checkIfAssetCopiedByUser(assetId, userId)
278
279if (isCopied?.isCopied) {
280throw new ConflictException('Asset already copied by user')
281}
282
283let isFree = true
284
285if (asset.purchaseOptions && asset.purchaseOptions.length > 0) {
286asset.purchaseOptions.forEach((option) => {
287if (
288option.enabled &&
289option.price &&
290option.type !== PURCHASE_OPTION_TYPE.ONE_TIME_OPTIONAL_DONATION
291) {
292isFree = false
293}
294})
295}
296
297// if asset is free, copy it to the new user with a new role
298if (isFree) {
299// if asset is an asset pack, copy all assets in the pack
300if (asset.assetPack && asset.assetsInPack) {
301return await this.copyManyAssetsForNewUserAdmin(
302userId,
303asset.assetsInPack.map((id) => id.toString())
304)
305}
306
307const newAsset: any = asset
308newAsset._id = new ObjectId()
309// change the owner to the new user, creator must be the same
310newAsset.owner = userId
311newAsset.role = await this.roleService.create({
312defaultRole: this._defaultRoleForNewAssets,
313creator: userId
314})
315
316if (newAsset.purchaseOptions) {
317delete newAsset.purchaseOptions
318}
319
320// update the createdAt and updatedAt fields
321newAsset.createdAt = new Date()
322newAsset.updatedAt = new Date()
323// copy thumbnail and currentFile with new user id and new asset id
324if (newAsset.thumbnail) {
325const copyThumbnail = await this._copyAssetFileToNewUser(
326asset.thumbnail,
327this._changeAssetUrl(
328assetId,
329asset.creator.toString(),
330newAsset._id.toString(),
331userId,
332asset.thumbnail
333)
334)
335newAsset.thumbnail = copyThumbnail
336}
337
338if (newAsset.currentFile) {
339const copyCurrentFile = await this._copyAssetFileToNewUser(
340asset.currentFile,
341this._changeAssetUrl(
342assetId,
343asset.creator.toString(),
344newAsset._id.toString(),
345userId,
346asset.currentFile
347)
348)
349newAsset.currentFile = copyCurrentFile
350}
351
352newAsset.purchasedParentAssetId = assetId.toString()
353newAsset.mirrorPublicLibrary = false
354const copiedAsset = new this.assetModel(newAsset)
355return copiedAsset.save()
356}
357throw new BadRequestException('Asset is not free')
358}
359
360public copyAssetToNewUserAdmin(
361assetId: AssetId,
362newUserId: UserId
363): Promise<AssetDocument> {
364return this.assetModel
365.findByIdAndUpdate(assetId, { owner: newUserId }, { new: true })
366.exec()
367}
368
369public async copyAssetToNewUserWithRolesCheck(
370assetId: AssetId,
371recievingUserId: 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?
375const check = await this.roleService.checkUserRoleForEntity(
376recievingUserId,
377assetId,
378ROLE.OWNER,
379this.assetModel
380)
381if (check === true) {
382return this.copyAssetToNewUserAdmin(assetId, recievingUserId)
383} else {
384throw new NotFoundException()
385}
386}
387
388// restore assets for space objects and return array of object with old and new asset ids
389public async restoreAssetsForSpaceObjects(assets: AssetDocument[]) {
390const newAssetsIds = []
391const bulkOps = []
392
393for (const asset of assets) {
394const newAssetId = new ObjectId()
395
396// if asset has a creator, use that, otherwise use owner (for old assets that don't have creator)
397const 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
402const role = asset.get('role')
403? asset.get('role')
404: await this.roleService.create({
405defaultRole: this._defaultRoleForNewAssets,
406creator: asset.get('owner')
407})
408
409newAssetsIds.push({
410[asset.get('_id').toString()]: newAssetId.toString()
411})
412
413const bulkOp = {
414insertOne: {
415document: {
416...Object.fromEntries(asset.toObject()),
417creator: objCreator,
418tags: asset.get('tags').length > 0 ? asset.get('tags') : undefined,
419role: role,
420_id: newAssetId
421}
422}
423}
424
425bulkOps.push(bulkOp)
426}
427
428await this.assetModel.bulkWrite(bulkOps)
429return newAssetsIds
430}
431
432/**
433* @description creates a Material Asset (subclass of Asset in Mongoose using a discriminator)
434*/
435public async createMaterial(
436dto: CreateMaterialDto & { ownerId: string }
437): Promise<MaterialDocument> {
438const created = new this.materialModel({
439owner: dto.ownerId,
440creator: dto.ownerId, // default to ownerId since that owner is creating it.
441...dto
442})
443// create role for this asset
444const role = await this.roleService.create({
445defaultRole: dto.defaultRole ?? this._defaultRoleForNewAssets,
446creator: dto.ownerId
447})
448created.role = role
449await 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
451return 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*/
457public async createMaterialWithUpload(
458dto: CreateMaterialDto & { ownerId: string },
459file: Express.Multer.File
460): Promise<MaterialDocument> {
461try {
462const created = new this.materialModel({
463owner: dto.ownerId,
464creator: dto.ownerId, // default to ownerId since that owner is creating it.
465...dto
466})
467
468const { publicUrl: currentFile } =
469await this.uploadAssetFilePublicWithRolesCheck({
470assetId: created.id,
471userId: dto.ownerId,
472file
473})
474
475created['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
478const role = await this.roleService.create({
479defaultRole: dto.defaultRole ?? this._defaultRoleForNewAssets,
480creator: dto.ownerId
481})
482
483created.role = role
484await 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
486return this.materialModel.findById(created.id).populate('role').exec()
487} catch (error: any) {
488throw error
489}
490}
491
492/**
493* @description creates a Texture Asset (subclass of Asset in Mongoose using a discriminator)
494*/
495public async createTexture(
496dto: CreateTextureDto & { ownerId: string }
497): Promise<TextureDocument> {
498const created = new this.textureModel({
499owner: dto.ownerId,
500creator: dto.ownerId, // default to ownerId since that owner is creating it.
501...dto
502})
503
504// create role for this asset
505const role = await this.roleService.create({
506defaultRole: dto.defaultRole ?? this._defaultRoleForNewAssets,
507creator: dto.ownerId
508})
509created.role = role
510
511await 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
513return 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*/
519public async createTextureWithUpload(
520dto: CreateTextureDto & { ownerId: string },
521file: Express.Multer.File
522): Promise<TextureDocument> {
523try {
524const created = new this.textureModel({
525owner: dto.ownerId,
526creator: dto.ownerId, // default to ownerId since that owner is creating it.
527...dto
528})
529
530const { publicUrl: currentFile } =
531await this.uploadAssetFilePublicWithRolesCheck({
532assetId: created.id,
533userId: dto.ownerId,
534file
535})
536
537created['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
540const role = await this.roleService.create({
541defaultRole: dto.defaultRole ?? this._defaultRoleForNewAssets,
542creator: dto.ownerId
543})
544created.role = role
545
546await 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
548return this.textureModel.findById(created.id).populate('role').exec()
549} catch (error: any) {
550throw error
551}
552}
553
554/**
555* @description creates a Material Asset (subclass of Asset in Mongoose using a discriminator)
556*/
557public async createMap(
558dto: CreateMapDto & { ownerId: string }
559): Promise<MapDocument> {
560const created = new this.mapAssetModel({
561owner: dto.ownerId,
562creator: dto.ownerId, // default to ownerId since that owner is creating it.
563...dto
564})
565// create role for this asset
566const role = await this.roleService.create({
567defaultRole: dto.defaultRole ?? this._defaultRoleForNewAssets,
568creator: dto.ownerId
569})
570created.role = role
571await 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
573return 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*/
579public async createMapWithUpload(
580dto: CreateMapDto & { ownerId: string },
581file: Express.Multer.File
582): Promise<MapDocument> {
583try {
584const created = new this.mapAssetModel({
585owner: dto.ownerId,
586creator: dto.ownerId, // default to ownerId since that owner is creating it.
587...dto
588})
589
590const { publicUrl: currentFile } =
591await this.uploadAssetFilePublicWithRolesCheck({
592assetId: created.id,
593userId: dto.ownerId,
594file
595})
596
597created['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
600const role = await this.roleService.create({
601defaultRole: dto.defaultRole ?? this._defaultRoleForNewAssets,
602creator: dto.ownerId
603})
604
605created.role = role
606await 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
608return this.mapAssetModel.findById(created.id).populate('role').exec()
609} catch (error: any) {
610throw 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
622public findAllPublicAssetsForUserWithRolesCheck(
623requestingUserId: UserId,
624targetUserId: UserId
625): Promise<AssetDocument[]> {
626const pipeline = [
627...this.roleService.getRoleCheckAggregationPipeline(
628requestingUserId,
629ROLE.DISCOVER
630),
631{ $match: { isSoftDeleted: { $exists: false } } },
632// get assets where the targetUser is an owner
633...this.roleService.userIsOwnerAggregationPipeline(targetUserId)
634]
635return this.assetModel.aggregate(pipeline).exec()
636}
637
638public async findManyAdmin(
639assetIds: Array<string>
640): Promise<AssetDocument[]> {
641return await this.assetModel.find().where('_id').in(assetIds)
642}
643
644/**
645* Find self-created assets
646* TODO add pagination
647*/
648public findAllAssetsForUserIncludingPrivate(
649userId: string,
650searchDto?: PaginatedSearchAssetDtoV2,
651sort: ISort = { updatedAt: SORT_DIRECTION.DESC }, // default: sort by updatedAt descending
652populate = false
653): Promise<AssetDocument[]> {
654const 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
665const andFilter = AssetService.getSearchFilter(searchDto)
666if (andFilter.length > 0) {
667filter.$and.push(...andFilter)
668}
669
670const cursor = this.assetModel.find(filter).limit(1000).sort(sort)
671
672if (populate) {
673cursor.populate(this._getStandardPopulateFieldsAsArray())
674}
675
676return cursor.exec()
677}
678
679/**
680* Find public Mirror Library assets
681*/
682public findMirrorPublicLibraryAssets(
683searchDto?: PaginatedSearchAssetDtoV2,
684sort: ISort = { updatedAt: SORT_DIRECTION.DESC }, // default: sort by updatedAt descending
685populate = false
686): Promise<AssetDocument[]> {
687const filter: FilterQuery<any> = searchDto.includeSoftDeleted
688? {
689$and: [{ mirrorPublicLibrary: true }]
690}
691: {
692$and: [
693{ mirrorPublicLibrary: true },
694{ isSoftDeleted: { $exists: false } }
695]
696}
697
698const andFilter = AssetService.getSearchFilter(searchDto)
699if (andFilter.length > 0) {
700filter.$and.push(...andFilter)
701}
702
703const cursor = this.assetModel.find(filter).limit(1000).sort(sort)
704
705if (populate) {
706cursor.populate(this._getStandardPopulateFieldsAsArray())
707}
708
709return cursor.exec()
710}
711
712public findPaginatedMirrorAssetsWithRolesCheck(
713userId: UserId,
714searchDto?: PaginatedSearchAssetDtoV2,
715populate: PopulateField[] = [] // don't abuse, this is slow
716): Promise<IPaginatedResponse<AssetDocument>> {
717const { page, perPage, startItem, numberOfItems, includeSoftDeleted } =
718searchDto
719
720const filter: FilterQuery<any> = includeSoftDeleted
721? {
722mirrorPublicLibrary: true
723}
724: { mirrorPublicLibrary: true, isSoftDeleted: { $exists: false } }
725
726if (!searchDto?.includeAssetPackAssets) {
727filter.assetPack = { $ne: true }
728}
729
730const andFilter = AssetService.getSearchFilter(searchDto)
731if (andFilter.length > 0) {
732filter.$and = andFilter
733}
734
735let sort =
736searchDto.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
743if (searchDto.mirrorAssetManagerUserSortKey === undefined) {
744if (sort === undefined) sort = {}
745sort.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
750if (
751searchDto.mirrorAssetManagerUserSortKey &&
752searchDto.mirrorAssetManagerUserSortKey !== '0'
753) {
754if (sort === undefined) sort = {}
755sort.mirrorAssetManagerUser = Number(
756searchDto.mirrorAssetManagerUserSortKey
757)
758}
759
760if (
761startItem !== null &&
762startItem !== undefined &&
763numberOfItems !== null &&
764numberOfItems !== undefined
765)
766return this.paginationService.getPaginatedQueryResponseByStartItemWithRolesCheck(
767userId,
768this.assetModel,
769filter,
770ROLE.OBSERVER,
771{ startItem, numberOfItems },
772populate ? this.standardPopulateFields : [],
773sort
774)
775return this.paginationService.getPaginatedQueryResponseWithRolesCheck(
776userId,
777this.assetModel,
778filter,
779ROLE.OBSERVER,
780{ page, perPage },
781populate ? this.standardPopulateFields : [],
782sort
783)
784}
785
786public findPaginatedMyAssetsWithRolesCheck(
787userId: string,
788searchDto?: PaginatedSearchAssetDtoV2,
789populate = false // don't use, this is slow
790): Promise<IPaginatedResponse<AssetDocument>> {
791const { page, perPage, startItem, numberOfItems } = searchDto
792
793const filter: FilterQuery<any> = searchDto.includeSoftDeleted
794? {}
795: { isSoftDeleted: { $exists: false } }
796
797if (!searchDto?.includeAssetPackAssets) {
798filter.assetPack = { $ne: true }
799}
800
801const andFilter = AssetService.getSearchFilter(searchDto)
802if (andFilter.length > 0) {
803filter.$and = andFilter
804}
805
806const sort =
807searchDto.sortKey && searchDto.sortDirection !== undefined
808? {
809[searchDto.sortKey]: searchDto.sortDirection
810}
811: undefined
812
813if (
814startItem !== null &&
815startItem !== undefined &&
816numberOfItems !== null &&
817numberOfItems !== undefined
818)
819return this.paginationService.getPaginatedQueryResponseByStartItemWithRolesCheck(
820userId,
821this.assetModel,
822filter,
823ROLE.OWNER,
824{ startItem, numberOfItems },
825populate ? this.standardPopulateFields : [],
826sort
827)
828
829return this.paginationService.getPaginatedQueryResponseWithRolesCheck(
830userId,
831this.assetModel,
832filter,
833ROLE.OWNER,
834{ page, perPage },
835populate ? this.standardPopulateFields : [],
836sort
837)
838}
839
840public findAllAccessibleAssetsOfUser(
841userId: string,
842searchDto?: PaginatedSearchAssetDtoV2,
843populate = false // don't use, this is slow
844): Promise<IPaginatedResponse<AssetDocument>> {
845const { page, perPage, startItem, numberOfItems } = searchDto
846
847const filter: FilterQuery<any> = searchDto?.includeSoftDeleted
848? {
849$or: [{ mirrorPublicLibrary: true }, { owner: new ObjectId(userId) }]
850}
851: {
852$or: [{ mirrorPublicLibrary: true }, { owner: new ObjectId(userId) }],
853isSoftDeleted: { $exists: false }
854}
855
856if (!searchDto?.includeAssetPackAssets) {
857filter.assetPack = { $ne: true }
858}
859
860const andFilter = AssetService.getSearchFilter(searchDto)
861if (andFilter.length > 0) {
862filter.$and = andFilter
863}
864
865const sort =
866searchDto.sortKey && searchDto.sortDirection !== undefined
867? {
868[searchDto.sortKey]: searchDto.sortDirection
869}
870: undefined
871if (
872startItem !== null &&
873startItem !== undefined &&
874numberOfItems !== null &&
875numberOfItems !== undefined
876)
877return this.paginationService.getPaginatedQueryResponseByStartItemWithRolesCheck(
878userId,
879this.assetModel,
880filter,
881ROLE.DISCOVER,
882{ startItem, numberOfItems },
883populate ? this.standardPopulateFields : [],
884sort
885)
886return this.paginationService.getPaginatedQueryResponseWithRolesCheck(
887userId,
888this.assetModel,
889filter,
890ROLE.DISCOVER,
891{ page, perPage },
892populate ? this.standardPopulateFields : [],
893sort
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*/
903public async findRecentAssetsOfUserWithRolesCheck(
904userId: string,
905includeSoftDeleted = false,
906limit = 20,
907populate = false // don't use, this is slow
908): Promise<AssetDocument[]> {
909// find recently updated spaceobjects owned by the user and get the asset IDs
910const spaceObjects: SpaceObjectDocument[] = (
911await this.paginationService.getPaginatedQueryResponseWithRolesCheck(
912userId,
913this.spaceObjectModel,
914{},
915ROLE.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
921const spaceObjectAssetIds = spaceObjects
922.filter((spaceObject) => spaceObject.asset)
923.map((spaceObject) => spaceObject.asset.toString())
924
925const filter: FilterQuery<any> = {
926$or: [
927includeSoftDeleted
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
937const page = 1
938const perPage = limit
939const assetsPaginated =
940await this.paginationService.getPaginatedQueryResponseWithRolesCheck(
941userId,
942this.assetModel,
943filter,
944ROLE.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 },
946populate ? this.standardPopulateFields : []
947)
948
949return 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*/
956private static getSearchFilter(
957searchDto: PaginatedSearchAssetDtoV2
958): Array<any> {
959const { search, field, type, tagType, tag, assetType, assetTypes } =
960searchDto
961
962const andFilter = []
963//override type with assetType if it exists (deprecating type to use assetType instead)
964let assetTypesAll: string[] = []
965if (assetType) {
966assetTypesAll.push(assetType.toUpperCase())
967} else if (type) {
968assetTypesAll.push(type.toUpperCase())
969}
970if (assetTypes) {
971assetTypesAll = assetTypesAll.concat(assetTypes)
972}
973if (assetTypesAll.length > 0) {
974andFilter.push({ assetType: { $in: assetTypesAll } })
975}
976
977if (field && search) {
978andFilter.push({
979$or: [
980{ [field]: new RegExp(search, 'i') },
981{ 'tags.search': new RegExp(search, 'i') }
982]
983})
984}
985
986if (tag && tagType) {
987const tagSearchKey =
988tagType === TAG_TYPES.THIRD_PARTY
989? `tags.${tagType}.name`
990: `tags.${tagType}`
991
992const tagFilter = { $or: tag.map((t) => ({ [tagSearchKey]: t })) }
993andFilter.push(tagFilter)
994}
995
996return andFilter
997}
998
999public findOneAdmin(id: AssetId): Promise<AssetDocument> {
1000return this.assetModel
1001.findById(id)
1002.populate(this._getStandardPopulateFieldsAsArray())
1003.exec()
1004}
1005
1006public async findOneWithRolesCheck(
1007userId: UserId,
1008assetId: AssetId
1009): Promise<AssetDocument> {
1010const pipeline = [
1011{ $match: { isSoftDeleted: { $exists: false } } },
1012aggregationMatchId(assetId),
1013...this.roleService.getRoleCheckAggregationPipeline(userId, ROLE.OBSERVER)
1014]
1015
1016const [asset]: AssetDocument[] = await this.assetModel
1017.aggregate(pipeline)
1018.exec()
1019
1020if (asset) {
1021return asset
1022} else {
1023throw new NotFoundException()
1024}
1025}
1026
1027public async findAssetUsageWithRolesCheck(
1028userId: UserId,
1029assetId: AssetId
1030): Promise<AssetUsageApiResponse> {
1031const pipeline = [
1032aggregationMatchId(assetId),
1033...this.roleService.getRoleCheckAggregationPipeline(userId, ROLE.DISCOVER)
1034]
1035const data = await this.assetModel.aggregate(pipeline).exec()
1036if (data && data[0]) {
1037// find all space objects that use this asset
1038const spaceKeys = await this.spaceObjectModel
1039.find({
1040asset: assetId
1041})
1042.distinct('space')
1043.exec()
1044
1045return {
1046numberOfSpacesAssetUsedIn: spaceKeys.length
1047}
1048} else {
1049throw new NotFoundException()
1050}
1051}
1052
1053public updateOneAdmin(
1054id: string,
1055updateAssetDto: UpdateAssetDto
1056): Promise<AssetDocument> {
1057return this.assetModel
1058.findByIdAndUpdate(id, updateAssetDto, { new: true })
1059.populate(this._getStandardPopulateFieldsAsArray())
1060.exec()
1061}
1062
1063public async updateOneWithRolesCheck(
1064userId: string,
1065assetId: AssetId,
1066updateAssetDto: UpdateAssetDto
1067): Promise<AssetDocument | MapDocument | TextureDocument | MaterialDocument> {
1068// check the role
1069const roleCheck = await this.roleService.checkUserRoleForEntity(
1070userId,
1071assetId,
1072ROLE.MANAGER, // business logic: manager role is needed to update an asset
1073this.assetModel
1074)
1075
1076const softDeletedCheck = await this.isAssetSoftDeleted(assetId)
1077
1078if (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.
1082switch (updateAssetDto.__t) {
1083case 'MapAsset':
1084return this.mapAssetModel
1085.findByIdAndUpdate(assetId, updateAssetDto, { new: true })
1086.populate(this._getStandardPopulateFieldsAsArray())
1087.exec()
1088case 'Material':
1089return this.materialModel
1090.findByIdAndUpdate(assetId, updateAssetDto, { new: true })
1091.populate(this._getStandardPopulateFieldsAsArray())
1092.exec()
1093case 'Texture':
1094return this.textureModel
1095.findByIdAndUpdate(assetId, updateAssetDto, { new: true })
1096.populate(this._getStandardPopulateFieldsAsArray())
1097.exec()
1098
1099default:
1100return this.assetModel
1101.findByIdAndUpdate(assetId, updateAssetDto, { new: true })
1102.populate(this._getStandardPopulateFieldsAsArray())
1103.exec()
1104}
1105} else {
1106throw new NotFoundException()
1107}
1108}
1109
1110public removeOneAdmin(id: string): Promise<AssetDocument> {
1111if (!Types.ObjectId.isValid(id)) {
1112throw new BadRequestException('ID is not a valid Mongo ObjectID')
1113}
1114return this.assetModel
1115.findOneAndDelete({ _id: id }, { new: true })
1116.exec()
1117.then((data) => {
1118if (data) {
1119return data
1120} else {
1121throw new NotFoundException()
1122}
1123})
1124}
1125
1126public async removeOneWithRolesCheck(
1127userId: UserId,
1128assetId: AssetId
1129): Promise<AssetDocument> {
1130if (!Types.ObjectId.isValid(assetId)) {
1131throw new BadRequestException('ID is not a valid Mongo ObjectID')
1132}
1133
1134// check the role
1135const roleCheck = await this.roleService.checkUserRoleForEntity(
1136userId,
1137assetId,
1138ROLE.MANAGER, // business logic: manager role is needed to delete an asset
1139this.assetModel
1140)
1141
1142if (!roleCheck) {
1143throw new NotFoundException('Asset not found')
1144}
1145
1146const isAssetCanBeDeleted = await this._isAssetCanBeDeleted(assetId)
1147
1148if (!isAssetCanBeDeleted) {
1149throw new ConflictException(
1150'The asset cannot be deleted as it is referenced by one or more space objects'
1151)
1152}
1153
1154return 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* */
1163public async searchAssetsPublic(
1164searchDto: PaginatedSearchAssetDtoV2,
1165populate = false
1166): Promise<IPaginatedResponse<AssetDocument>> {
1167const { page, perPage } = searchDto
1168const matchFilter: FilterQuery<Asset> = {
1169$and: [
1170{
1171$or: [
1172{ 'purchaseOptions.enabled': true },
1173{ mirrorPublicLibrary: true }
1174]
1175},
1176{ isSoftDeleted: { $exists: false } }
1177]
1178}
1179
1180if (!searchDto?.includeAssetPackAssets) {
1181matchFilter.$and.push({ assetPack: { $ne: true } })
1182}
1183
1184const andFilter = AssetService.getSearchFilter(searchDto)
1185
1186if (andFilter.length > 0) {
1187matchFilter.$and.push(...andFilter)
1188}
1189
1190let sort =
1191searchDto.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
1198if (searchDto.sortKey !== 'mirrorPublicLibrary') {
1199if (sort === undefined) sort = {}
1200sort.mirrorPublicLibrary = SORT_DIRECTION.DESC
1201}
1202
1203// if mirrorAssetManagerUserSortKey is undefined, sort.mirrorAssetManagerUser default sort direction is DESC
1204if (searchDto.mirrorAssetManagerUserSortKey === undefined) {
1205if (sort === undefined) sort = {}
1206sort.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
1211if (
1212searchDto.mirrorAssetManagerUserSortKey &&
1213searchDto.mirrorAssetManagerUserSortKey !== '0'
1214) {
1215if (sort === undefined) sort = {}
1216sort.mirrorAssetManagerUser = Number(
1217searchDto.mirrorAssetManagerUserSortKey
1218)
1219}
1220
1221return await this.paginationService.getPaginatedQueryResponseAdmin(
1222this.assetModel,
1223matchFilter,
1224{ page, perPage },
1225populate ? this.standardPopulateFields : [],
1226sort
1227)
1228}
1229
1230public async uploadAssetFilePublicWithRolesCheck({
1231userId,
1232assetId,
1233file
1234}: UploadAssetFileDto) {
1235// check the role
1236const check = await this.roleService.checkUserRoleForEntity(
1237userId,
1238assetId,
1239ROLE.MANAGER,
1240this.assetModel
1241)
1242
1243if (!check) {
1244throw new NotFoundException('Asset not found')
1245}
1246
1247const fileId = new Types.ObjectId()
1248const path = `${userId}/assets/${assetId}/files/${fileId.toString()}`
1249
1250const isAssetEquipable = await this.assetAnalyzingService.isAssetEquipable(
1251file
1252)
1253
1254const fileUploadResult = await this.fileUploadService.uploadFilePublic({
1255file,
1256path
1257})
1258
1259if (isAssetEquipable) {
1260await this.assetModel.updateOne({ _id: assetId }, { isEquipable: true })
1261}
1262
1263return fileUploadResult
1264}
1265
1266public async uploadAssetFileWithRolesCheck({
1267assetId,
1268userId,
1269file
1270}: UploadAssetFileDto) {
1271// check the role
1272const check = await this.roleService.checkUserRoleForEntity(
1273userId,
1274assetId,
1275ROLE.MANAGER,
1276this.assetModel
1277)
1278
1279if (!check) {
1280throw new NotFoundException('Asset not found')
1281}
1282
1283const fileId = new Types.ObjectId()
1284const path = `${userId}/assets/${assetId}/files/${fileId.toString()}`
1285
1286const isAssetEquipable = await this.assetAnalyzingService.isAssetEquipable(
1287file
1288)
1289
1290const fileUploadResult = await this.fileUploadService.uploadFilePrivate({
1291file,
1292path
1293})
1294
1295if (isAssetEquipable) {
1296await this.assetModel.updateOne({ _id: assetId }, { isEquipable: true })
1297}
1298
1299return fileUploadResult
1300}
1301
1302public async uploadAssetThumbnailWithRolesCheck({
1303assetId,
1304userId,
1305file
1306}: UploadAssetFileDto) {
1307// check the role
1308let check = await this.roleService.checkUserRoleForEntity(
1309userId,
1310assetId,
1311ROLE.MANAGER,
1312this.assetModel
1313)
1314// !! Special exception for thumbnails: if the account is EngAssetManager, then allow
1315if (userId === ASSET_MANAGER_UID) {
1316check = true
1317}
1318if (check === true) {
1319const path = `${userId}/assets/${assetId}/images/thumbnail`
1320return this.fileUploadService.uploadThumbnail({ file, path })
1321} else {
1322throw new NotFoundException()
1323}
1324}
1325
1326public async getPaginatedQueryResponseByStartItemWithRolesCheck(
1327userId: string,
1328searchDto?: PaginatedSearchAssetDtoV2,
1329populate = false // don't use, this is slowqueryParams: GetAssetDto
1330) {
1331const { startItem, numberOfItems } = searchDto
1332
1333const filter: FilterQuery<any> = searchDto.includeSoftDeleted
1334? {}
1335: { isSoftDeleted: { $exists: false } }
1336
1337if (!searchDto?.includeAssetPackAssets) {
1338filter.assetPack = { $ne: true }
1339}
1340
1341const andFilter = AssetService.getSearchFilter(searchDto)
1342if (andFilter.length > 0) {
1343filter.$and = andFilter
1344}
1345
1346return await this.paginationService.getPaginatedQueryResponseByStartItemWithRolesCheck(
1347userId,
1348this.assetModel,
1349filter,
1350ROLE.OWNER,
1351{ startItem, numberOfItems },
1352populate ? this.standardPopulateFields : []
1353)
1354}
1355
1356public async addAssetPurchaseOption(
1357userId: string,
1358assetId: string,
1359data: AddAssetPurchaseOptionDto
1360) {
1361// check the role
1362const check = await this.roleService.checkUserRoleForEntity(
1363userId,
1364assetId,
1365ROLE.OWNER, // business logic: manager role is needed to delete an asset
1366this.assetModel
1367)
1368
1369if (check === true) {
1370// Check the license type.
1371if (data.licenseType === PURCHASE_OPTION_TYPE.MIRROR_REV_SHARE) {
1372// Check MIRROR_REV_SHARE already exist or not
1373const checkPurchaseOptionExist = await this.assetModel.findOne({
1374_id: assetId,
1375purchaseOptions: { $elemMatch: { licenseType: data.licenseType } }
1376})
1377// Throw bad request exception if exist
1378if (checkPurchaseOptionExist) {
1379throw new BadRequestException(
1380'This asset is already set for RevShare'
1381)
1382}
1383}
1384} else {
1385throw new NotFoundException()
1386}
1387
1388const createdPurchaseOption = new this.purchaseModel(data)
1389await createdPurchaseOption.save()
1390return await this.assetModel.findByIdAndUpdate(
1391assetId,
1392{ $push: { purchaseOptions: createdPurchaseOption } },
1393{ new: true }
1394)
1395}
1396
1397public async getAssetsByIdsAdmin(
1398assetIds: AssetId[]
1399): Promise<AssetDocument[]> {
1400return await this.assetModel.find({ _id: { $in: assetIds } }).exec()
1401}
1402
1403public async deleteAssetPurchaseOption(
1404userId: string,
1405assetId: string,
1406purchaseOptionId: string
1407) {
1408// check the role
1409const check = await this.roleService.checkUserRoleForEntity(
1410userId,
1411assetId,
1412ROLE.OWNER, // business logic: manager role is needed to delete an asset
1413this.assetModel
1414)
1415
1416if (check === true) {
1417return await this.assetModel.findByIdAndUpdate(
1418assetId,
1419{ $pull: { purchaseOptions: { _id: purchaseOptionId } } },
1420{ new: true }
1421)
1422} else {
1423throw new NotFoundException()
1424}
1425}
1426
1427public async getAssetsByTag(
1428searchDto: PaginatedSearchAssetDtoV2,
1429userId: UserId = undefined
1430) {
1431const { page, perPage, includeSoftDeleted } = searchDto
1432const matchFilter: FilterQuery<Asset> = {}
1433const andFilter = AssetService.getSearchFilter(searchDto)
1434
1435if (!includeSoftDeleted) {
1436andFilter.push({ isSoftDeleted: { $exists: false } })
1437}
1438
1439if (andFilter.length > 0) {
1440matchFilter.$and = andFilter
1441}
1442
1443if (!searchDto?.includeAssetPackAssets) {
1444matchFilter.$and.push({ assetPack: { $ne: true } })
1445}
1446
1447const sort =
1448searchDto.sortKey && searchDto.sortDirection !== undefined
1449? {
1450[searchDto.sortKey]: searchDto.sortDirection
1451}
1452: undefined
1453
1454const paginatedAssetResult =
1455await this.paginationService.getPaginatedQueryResponseWithRolesCheck(
1456userId,
1457this.assetModel,
1458matchFilter,
1459ROLE.OBSERVER,
1460{ page, perPage },
1461[],
1462sort
1463)
1464
1465return paginatedAssetResult
1466}
1467
1468public async addTagToAssetsWithRoleChecks(
1469userId: UserId,
1470addTagToAssetDto: AddTagToAssetDto
1471) {
1472const { assetId, tagName, tagType, thirdPartySourceHomePageUrl } =
1473addTagToAssetDto
1474
1475const ownerRoleCheck = await this.roleService.checkUserRoleForEntity(
1476userId,
1477assetId,
1478ROLE.OWNER,
1479this.assetModel
1480)
1481
1482if (!ownerRoleCheck) {
1483throw new NotFoundException('Asset not found')
1484}
1485
1486if (thirdPartySourceHomePageUrl && tagType === TAG_TYPES.THIRD_PARTY) {
1487const tags = await this.getAssetTagsByType(assetId, tagType)
1488
1489const newThirdPartyTag = new ThirdPartyTagEntity(
1490tagName,
1491thirdPartySourceHomePageUrl
1492)
1493
1494return await this._updateAssetThirdPartyTags(
1495assetId,
1496tags as ThirdPartyTagEntity[],
1497newThirdPartyTag
1498)
1499}
1500
1501const tags = (await this.getAssetTagsByType(assetId, tagType)) as string[]
1502
1503if (tags.length === 15) {
1504throw new BadRequestException(`Asset already has 15 ${tagType} tags`)
1505}
1506
1507if (tags.includes(tagName)) {
1508throw new ConflictException(`Asset already has this ${tagType} tag`)
1509}
1510
1511tags.push(tagName)
1512await this._updateAssetTagsByType(assetId, tagType, tags)
1513
1514return tagName
1515}
1516
1517public async deleteTagFromAssetWithRoleChecks(
1518userId: UserId,
1519assetId: AssetId,
1520tagName: string,
1521tagType: TAG_TYPES
1522) {
1523if (!isMongoId(assetId)) {
1524throw new BadRequestException('Id is not a valid Mongo ObjectId')
1525}
1526
1527if (!isEnum(tagType, TAG_TYPES)) {
1528throw new BadRequestException('Unknown tag type')
1529}
1530
1531const ownerRoleCheck = await this.roleService.checkUserRoleForEntity(
1532userId,
1533assetId,
1534ROLE.OWNER,
1535this.assetModel
1536)
1537
1538if (!ownerRoleCheck) {
1539throw new NotFoundException('Asset not found')
1540}
1541
1542const tagKey = `tags.${tagType}`
1543const valueToMatch =
1544tagType === TAG_TYPES.THIRD_PARTY ? { name: tagName } : tagName
1545
1546await this.assetModel
1547.updateOne({ _id: assetId }, { $pull: { [tagKey]: valueToMatch } })
1548.exec()
1549
1550return { assetId, tagType, tagName }
1551}
1552
1553public async getAssetTagsByType(assetId: AssetId, tagType: TAG_TYPES) {
1554const asset = await this.assetModel
1555.findOne({ _id: assetId })
1556.select('tags')
1557.exec()
1558
1559if (!asset) {
1560throw new NotFoundException('Asset not found')
1561}
1562
1563return asset?.tags?.[tagType] || []
1564}
1565
1566public async updateAssetTagsByTypeWithRoleChecks(
1567userId: UserId,
1568assetId: AssetId,
1569tagType: TAG_TYPES,
1570tags: string[] | ThirdPartyTagEntity[]
1571) {
1572const ownerRoleCheck = await this.roleService.checkUserRoleForEntity(
1573userId,
1574assetId,
1575ROLE.OWNER,
1576this.assetModel
1577)
1578
1579if (!ownerRoleCheck) {
1580throw new NotFoundException('Asset not found')
1581}
1582
1583return 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
1587public async copyManyAssetsForNewUserAdmin(
1588userId: UserId,
1589assetIdList: AssetId[]
1590) {
1591const assets = await this.assetModel.find({ _id: { $in: assetIdList } })
1592
1593await Promise.all(
1594assets.map(async (asset) => {
1595const newAsset: any = asset.toObject()
1596newAsset._id = new ObjectId()
1597// change the owner to the new user, creator must be the same
1598newAsset.owner = new ObjectId(userId)
1599
1600// create new default role for new copied asset
1601newAsset.role = await this.roleService.create({
1602defaultRole: this._defaultRoleForNewAssets,
1603creator: userId
1604})
1605
1606// update the createdAt and updatedAt fields
1607newAsset.createdAt = new Date()
1608newAsset.updatedAt = new Date()
1609
1610// remove purchase options from the copied asset
1611if (newAsset.purchaseOptions) {
1612delete newAsset.purchaseOptions
1613}
1614
1615// copy thumbnail and currentFile with new asset id and new user id
1616if (newAsset.thumbnail) {
1617const copyThumbnail = await this._copyAssetFileToNewUser(
1618asset.thumbnail,
1619this._changeAssetUrl(
1620asset._id.toString(),
1621asset.creator.toString(),
1622newAsset._id.toString(),
1623userId,
1624asset.thumbnail
1625)
1626)
1627newAsset.thumbnail = copyThumbnail
1628}
1629
1630if (newAsset.currentFile) {
1631const copyCurrentFile = await this._copyAssetFileToNewUser(
1632asset.currentFile,
1633this._changeAssetUrl(
1634asset._id.toString(),
1635asset.creator.toString(),
1636newAsset._id.toString(),
1637userId,
1638asset.currentFile
1639)
1640)
1641newAsset.currentFile = copyCurrentFile
1642}
1643
1644newAsset.purchasedParentAssetId = asset._id.toString()
1645newAsset.mirrorPublicLibrary = false
1646// create and save the new asset
1647const copiedAsset = new this.assetModel(newAsset)
1648await copiedAsset.save()
1649})
1650)
1651return
1652}
1653
1654public async checkIfAssetCopiedByUser(assetId: AssetId, userId: UserId) {
1655const asset = await this.assetModel.findOne({
1656purchasedParentAssetId: assetId,
1657owner: new ObjectId(userId)
1658})
1659
1660return {
1661isCopied: !!asset,
1662copiedAssetId: asset?._id
1663}
1664}
1665
1666public async downloadAssetFileWithRoleChecks(
1667userId: UserId,
1668assetId: AssetId,
1669res: Response
1670) {
1671const asset = await this.findOneWithRolesCheck(userId, assetId)
1672
1673console.log('asset', asset)
1674if (!asset) {
1675throw new NotFoundException('Asset not found')
1676}
1677
1678const fileLink = asset.currentFile.replace(
1679'https://storage.googleapis.com/',
1680''
1681)
1682const bucket = fileLink.split('/')[0]
1683const filePath = fileLink.replace(`${bucket}/`, '')
1684
1685let storageFile: StorageFile
1686
1687try {
1688storageFile = await this.storageService.get(bucket, filePath)
1689} catch (e) {
1690console.log('Error fetching file: ', e.message)
1691if (e.message.toString().includes('No such object')) {
1692throw new NotFoundException('File not found')
1693} else {
1694throw new InternalServerErrorException(
1695'Error fetching file: ',
1696e.message
1697)
1698}
1699}
1700res.setHeader('Content-Type', storageFile.contentType)
1701res.setHeader('Cache-Control', 'max-age=60d')
1702res.end(storageFile.buffer)
1703}
1704
1705async getAllAssetsBySpaceIdWithRolesCheck(spaceId: SpaceId, userId: UserId) {
1706const pipeline = [
1707{ $match: { space: new ObjectId(spaceId) } },
1708{
1709$lookup: {
1710from: 'assets',
1711localField: 'asset',
1712foreignField: '_id',
1713as: 'assetInfo'
1714}
1715},
1716{ $unwind: '$assetInfo' },
1717{
1718$replaceRoot: { newRoot: '$assetInfo' }
1719},
1720...this.roleService.getRoleCheckAggregationPipeline(userId, ROLE.OBSERVER)
1721]
1722return await this.spaceObjectModel.aggregate(pipeline).exec()
1723}
1724
1725async addAssetToPackWithRolesCheck(
1726packId: AssetId,
1727assetId: AssetId,
1728userId: UserId
1729) {
1730const pack = await this.assetModel.findById(packId)
1731
1732if (!pack || !pack?.assetPack) {
1733throw new NotFoundException('Pack not found')
1734}
1735
1736const asset = await this.assetModel.findById(assetId)
1737
1738if (!asset) {
1739throw new NotFoundException('Asset not found')
1740}
1741
1742if (
1743asset.role.creator.toString() !== userId ||
1744pack.role.creator.toString() !== userId
1745) {
1746throw new ForbiddenException(
1747'User does not have permission to add asset to pack'
1748)
1749}
1750
1751return await this.assetModel.findByIdAndUpdate(packId, {
1752$push: { assetsInPack: new ObjectId(assetId) }
1753})
1754}
1755
1756async deleteAssetFromPackWithRolesCheck(
1757packId: AssetId,
1758assetId: AssetId,
1759userId: UserId
1760) {
1761const pack = await this.assetModel.findById(packId)
1762
1763if (!pack || !pack?.assetPack) {
1764throw new NotFoundException('Pack not found')
1765}
1766
1767if (pack.role.creator.toString() !== userId) {
1768throw new ForbiddenException(
1769'User does not have permission to delete asset from pack'
1770)
1771}
1772
1773if (!pack.assetsInPack.includes(new ObjectId(assetId))) {
1774throw new NotFoundException('Asset not found in pack')
1775}
1776
1777return await this.assetModel.findByIdAndUpdate(packId, {
1778$pull: { assetsInPack: new ObjectId(assetId) }
1779})
1780}
1781
1782private async _updateAssetTagsByType(
1783assetId: AssetId,
1784tagType: TAG_TYPES,
1785tags: string[] | ThirdPartyTagEntity[]
1786) {
1787const searchKey = `tags.${tagType}`
1788
1789await this.assetModel
1790.updateOne({ _id: assetId }, { $set: { [searchKey]: tags } })
1791.exec()
1792
1793return tags
1794}
1795
1796private async _updateAssetThirdPartyTags(
1797assetId: AssetId,
1798thirdPartyTags: ThirdPartyTagEntity[],
1799newThirdPartyTag: ThirdPartyTagEntity
1800) {
1801if (thirdPartyTags.length === 15) {
1802throw new BadRequestException(
1803`Space already has 15 ${TAG_TYPES.THIRD_PARTY} tags`
1804)
1805}
1806
1807const existingTag = thirdPartyTags.find(
1808(tag) => tag.name === newThirdPartyTag.name
1809)
1810
1811if (existingTag) {
1812throw new ConflictException(`Space already has this thirdParty tag`)
1813}
1814
1815thirdPartyTags.push(newThirdPartyTag)
1816
1817await this._updateAssetTagsByType(
1818assetId,
1819TAG_TYPES.THIRD_PARTY,
1820thirdPartyTags
1821)
1822
1823return 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*/
1832private async _isAssetCanBeDeleted(assetId: AssetId) {
1833const pipeline: PipelineStage[] = [
1834{ $match: { asset: new ObjectId(assetId) } },
1835{
1836$lookup: {
1837from: 'spaces',
1838localField: 'space',
1839foreignField: '_id',
1840as: 'spaceInfo'
1841}
1842},
1843{
1844$match: {
1845spaceInfo: { $ne: [] }
1846}
1847},
1848{
1849$count: 'spaceObjectsCount'
1850},
1851{
1852$project: {
1853_id: 0,
1854spaceObjectsCount: 1
1855}
1856}
1857]
1858
1859const [aggregationResult]: { spaceObjectsCount: number }[] =
1860await this.spaceObjectModel.aggregate(pipeline).exec()
1861
1862return !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*/
1871public async undoAssetSoftDelete(userId: UserId, assetId: AssetId) {
1872if (!isMongoId(assetId)) {
1873throw new BadRequestException('AssetId is not a valid Mongo ObjectID')
1874}
1875
1876const roleCheck = await this.roleService.checkUserRoleForEntity(
1877userId,
1878assetId,
1879ROLE.MANAGER,
1880this.assetModel
1881)
1882
1883if (!roleCheck) {
1884throw new NotFoundException('Asset not found')
1885}
1886
1887await this.assetModel.updateOne(
1888{ _id: new ObjectId(assetId) },
1889{ $unset: { isSoftDeleted: 1, softDeletedAt: 1 } }
1890)
1891
1892return assetId
1893}
1894
1895public async isAssetSoftDeleted(assetId: AssetId) {
1896const 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
1907return asset.length > 0
1908}
1909
1910private _changeAssetUrl(
1911parentAssetId: AssetId,
1912parentUserId: UserId,
1913assetId: AssetId,
1914userId: UserId,
1915parentUrl: string
1916) {
1917return (parentUrl = parentUrl
1918.replace(parentAssetId, assetId)
1919.replace(parentUserId, userId))
1920}
1921
1922private async _copyAssetFileToNewUser(
1923parentFileUrl: string,
1924assetUrl: string
1925) {
1926try {
1927if (process.env.ASSET_STORAGE_DRIVER === 'GCP') {
1928await this.fileUploadService.copyFileInBucket(
1929process.env.GCS_BUCKET_PUBLIC,
1930parentFileUrl.replace(
1931`https://storage.googleapis.com/${process.env.GCS_BUCKET_PUBLIC}/`,
1932''
1933),
1934assetUrl.replace(
1935`https://storage.googleapis.com/${process.env.GCS_BUCKET_PUBLIC}/`,
1936''
1937)
1938)
1939return assetUrl
1940}
1941
1942if (
1943!process.env.ASSET_STORAGE_DRIVER ||
1944process.env.ASSET_STORAGE_DRIVER === 'LOCAL'
1945) {
1946await this.fileUploadService.copyFileLocal(
1947parentFileUrl.replace(`${process.env.ASSET_STORAGE_URL}/`, ''),
1948assetUrl.replace(`${process.env.ASSET_STORAGE_URL}/`, '')
1949)
1950return assetUrl
1951}
1952} catch (error) {
1953this.logger.error(
1954`Error copying file from ${parentFileUrl} to ${assetUrl}`,
1955error
1956)
1957throw new InternalServerErrorException(
1958`Error copying file from ${parentFileUrl} to ${assetUrl}`
1959)
1960}
1961}
1962}
1963
1964export type AssetServiceType = AssetService // this is used to solve circular dependency issue with swc https://github.com/swc-project/swc/issues/5047#issuecomment-1302444311
1965