diff --git a/src/features/assets/controller/artCards.ts b/src/features/assets/controller/artCards.ts new file mode 100644 index 00000000..0c0e18f7 --- /dev/null +++ b/src/features/assets/controller/artCards.ts @@ -0,0 +1,87 @@ +import debug from 'debug'; +import { z } from 'zod'; +import { nanoid } from 'nanoid'; +import { Router } from 'express'; + +import { APIResponse } from '../../../services'; +import { middleware } from '../../users'; +import * as model from '../model'; +import { schemaAssetArtCardsStatus } from './schemas'; + +const logger = debug('features:assets:controller:artCards'); +const route = Router(); + +route.use(middleware.checkAuth); + +route.get('/', async (req, res) => { + try { + const page = parseInt(req.query.page as string, 10) || 1; + const limit = parseInt(req.query.limit as string, 10) || 10; + const status = req.query.status as string; + const search = req.query.search as string; + + const total = await model.countAssetsWithLicenseArtCards({ status }); + + const data = await model.findAssetsWithArtCardsPaginated({ + query: { + status, + search, + }, + limit, + skip: (page - 1) * limit, + sort: { + 'assetMetadata.context.formData.title': 1, + }, + }); + + const totalPage = Math.ceil(total / limit); + + res.json({ + code: 'vitruveo.studio.api.assets.artCards.success', + message: 'artCards asset success', + transaction: nanoid(), + data: { + data, + page, + totalPage, + total, + limit, + }, + } as APIResponse); + } catch (error) { + logger('artCards asset failed: %O', error); + res.status(500).json({ + code: 'vitruveo.studio.api.assets.artCards.failed', + message: `artCards asset failed: ${error}`, + args: error, + transaction: nanoid(), + } as APIResponse); + } +}); + +route.patch('/:id', async (req, res) => { + try { + const { id } = req.params; + const { status } = req.body as { + status: z.infer; + }; + + await model.updateAssetArtCardsStatus({ id, status }); + + res.json({ + code: 'vitruveo.studio.api.assets.artCards.update.success', + message: 'artCards asset update success', + transaction: nanoid(), + } as APIResponse); + } catch (error) { + logger('artCards asset update failed: %O', error); + res.status(500).json({ + code: 'vitruveo.studio.api.assets.artCards.update.failed', + message: `artCards asset update failed: ${error}`, + args: error, + transaction: nanoid(), + } as APIResponse); + } +}); + +export { route }; diff --git a/src/features/assets/controller/core.ts b/src/features/assets/controller/core.ts index 47001fec..d5b27b0f 100644 --- a/src/features/assets/controller/core.ts +++ b/src/features/assets/controller/core.ts @@ -110,6 +110,11 @@ route.get('/', async (req, res) => { creatorId: creatorId || req.auth.id, }); + const licenseArtCards = + await model.countAssetsWithLicenseArtCardsByCreator({ + creatorId: creatorId || req.auth.id, + }); + res.json({ code: 'vitruveo.studio.api.assets.reader.success', message: 'Reader success', @@ -122,6 +127,7 @@ route.get('/', async (req, res) => { limit, collection, collections, + licenseArtCards, }, } as APIResponse); } catch (error) { diff --git a/src/features/assets/controller/index.ts b/src/features/assets/controller/index.ts index 43bf9ed5..a06ff568 100644 --- a/src/features/assets/controller/index.ts +++ b/src/features/assets/controller/index.ts @@ -10,6 +10,7 @@ import { route as consignRouter } from './consign'; import { route as scopeRouter } from './scope'; import { route as adminRouter } from './admin'; import { route as slideshowRouter } from './slideshow'; +import { route as artCardsRouter } from './artCards'; const router = Router(); @@ -23,6 +24,7 @@ router.use('/assets/preview', previewRouter); router.use('/assets/store', storeRouter); router.use('/assets/scope', scopeRouter); router.use('/assets/admin', adminRouter); +router.use('/assets/artCards', artCardsRouter); router.use('/assets', coreRouter); export { router }; diff --git a/src/features/assets/controller/schemas.ts b/src/features/assets/controller/schemas.ts index 12163e7c..3fff8872 100644 --- a/src/features/assets/controller/schemas.ts +++ b/src/features/assets/controller/schemas.ts @@ -195,6 +195,11 @@ export const schemaAuxiliaryMedia = z.object({ }), }), }); +export const schemaAssetArtCardsStatus = z.enum([ + 'pending', + 'approved', + 'rejected', +]); export const schemaLicenses = z.object({ licenses: z.object({ @@ -233,6 +238,13 @@ export const schemaLicenses = z.object({ unitPrice: z.number(), availableLicenses: z.number().min(0).default(1), }), + artCards: z + .object({ + version: z.string(), + added: z.boolean(), + status: schemaAssetArtCardsStatus.default('pending'), + }) + .optional(), }), framework: z.object({ createdAt: z.date(), diff --git a/src/features/assets/model/db.ts b/src/features/assets/model/db.ts index 9e736003..4b4c61b9 100644 --- a/src/features/assets/model/db.ts +++ b/src/features/assets/model/db.ts @@ -30,6 +30,10 @@ import type { FindMyAssetsParams, CountArtsByCreatorParams, UpateAssetsUsernameParams, + CountAssetsWithLicenseArtCardsByCreatorParams, + FindAssetsWithArtCardsPaginatedParams, + UpdateAssetArtCardsStatusParams, + CountAssetsWithLicenseArtCardsParams, } from './types'; import { FindOptions, getDb, ObjectId } from '../../../services/mongo'; import { buildFilterColorsQuery } from '../utils/color'; @@ -525,6 +529,14 @@ export const countAssets = async ({ >; }; +export const countAssetsWithLicenseArtCardsByCreator = async ({ + creatorId, +}: CountAssetsWithLicenseArtCardsByCreatorParams) => + assets().countDocuments({ + 'framework.createdBy': creatorId, + 'licenses.artCards.added': true, + }); + export const findCollectionsByCreatorId = async ({ creatorId, }: FindCollectionsByCreatorParams) => @@ -743,6 +755,61 @@ export const findAssetsById = async ({ id }: FindAssetsByIdParams) => { return result; }; +export const countAssetsWithLicenseArtCards = async ({ + status, +}: CountAssetsWithLicenseArtCardsParams) => + assets().countDocuments({ + 'licenses.artCards.added': true, + 'licenses.artCards.status': status, + }); + +export const findAssetsWithArtCardsPaginated = ({ + limit, + query, + skip, + sort, +}: FindAssetsWithArtCardsPaginatedParams) => + assets() + .aggregate([ + { + $match: { + 'licenses.artCards.added': true, + 'licenses.artCards.status': query.status, + }, + }, + { + $match: { + $or: [ + { + 'assetMetadata.context.formData.title': { + $regex: query.search ?? '.*', + $options: 'i', + }, + }, + { + 'creator.username': { + $regex: query.search ?? '.*', + $options: 'i', + }, + }, + ], + }, + }, + { $sort: sort }, + { $skip: skip }, + { $limit: limit }, + ]) + .toArray(); + +export const updateAssetArtCardsStatus = ({ + id, + status, +}: UpdateAssetArtCardsStatusParams) => + assets().updateOne( + { _id: new ObjectId(id) }, + { $set: { 'licenses.artCards.status': status } } + ); + export const findAssetMintedByAddress = async ({ address, sort, diff --git a/src/features/assets/model/schema.ts b/src/features/assets/model/schema.ts index 9406768c..36c2855b 100644 --- a/src/features/assets/model/schema.ts +++ b/src/features/assets/model/schema.ts @@ -135,6 +135,15 @@ export const AssetsSchema = z.object({ unitPrice: z.number(), availableLicenses: z.number(), }), + artCards: z + .object({ + version: z.string(), + added: z.boolean(), + status: z + .enum(['pending', 'approved', 'rejected']) + .default('pending'), + }) + .optional(), }), assetMetadata: z.object({ context: z.object({ diff --git a/src/features/assets/model/types.ts b/src/features/assets/model/types.ts index bba1881e..38a7f1d8 100644 --- a/src/features/assets/model/types.ts +++ b/src/features/assets/model/types.ts @@ -51,6 +51,7 @@ export interface AssetsPaginatedResponse { limit: number; collection: string; collections: Document[]; + licenseArtCards: number; } export interface FindAssetsTagsParams { @@ -65,6 +66,10 @@ export interface FindCollectionsByCreatorParams { creatorId: string; } +export interface CountAssetsWithLicenseArtCardsByCreatorParams { + creatorId: string; +} + export interface FindAssetsCollectionsParams { name: string; showAdditionalAssets: string; @@ -86,6 +91,22 @@ export interface FindAssetsByIdParams { id: string | ObjectId; } +export interface CountAssetsWithLicenseArtCardsParams { + status: string; +} + +export interface FindAssetsWithArtCardsPaginatedParams { + query: any; + sort: any; + skip: number; + limit: number; +} + +export interface UpdateAssetArtCardsStatusParams { + id: string; + status: string; +} + export interface FindMyAssetsParams { query: { [key: string]: unknown }; } diff --git a/src/features/assets/watcher/index.ts b/src/features/assets/watcher/index.ts index ea01ada6..dd95b53d 100644 --- a/src/features/assets/watcher/index.ts +++ b/src/features/assets/watcher/index.ts @@ -132,13 +132,17 @@ uniqueExecution({ if (!creator) return; await Promise.all( - Object.entries(asset.licenses).map((item) => { - const [key, license] = item as [ - string, - AssetsDocument['licenses'][keyof AssetsDocument['licenses']], - ]; + Object.entries(asset.licenses) + .filter(([key]) => key !== 'artCards') + .map((item) => { + const [key, license] = item as [ + string, + AssetsDocument['licenses'][keyof AssetsDocument['licenses']], + ]; + + if (!license?.added) + return Promise.resolve(); - if (license.added) { return dispatchQueue({ license: key, id: asset._id.toString(), @@ -161,9 +165,7 @@ uniqueExecution({ asset.assetMetadata.context .formData.description, }); - } - return Promise.resolve(); - }) + }) ).catch((error) => logger( 'Error sending to exchange rss: %O',