From 1b16e79d3fd91e87821cc7cb9acf4c25d4e44dc6 Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Sat, 22 Aug 2026 21:14:42 +0330 Subject: [PATCH] Add tag filter to blogs, portfolios, and videos list APIs. Storefronts can filter published content by metadata.tags the same way as products; website API docs are updated to match. Co-authored-by: Cursor --- docs/website-api/AI_PROMPT.md | 2 +- ...eshkee-Website-API.postman_collection.json | 15 ++++++++++++ docs/website-api/openapi.json | 24 +++++++++++++++++++ src/blogs/blogs.service.ts | 8 +++++++ src/blogs/dto/blog.dto.ts | 8 +++++++ src/portfolios/dto/portfolio.dto.ts | 8 +++++++ src/portfolios/portfolios.service.ts | 8 +++++++ src/videos/dto/video.dto.ts | 8 +++++++ src/videos/videos.service.ts | 8 +++++++ src/website-docs/static/AI_PROMPT.md | 2 +- ...eshkee-Website-API.postman_collection.json | 15 ++++++++++++ src/website-docs/static/openapi.json | 24 +++++++++++++++++++ 12 files changed, 128 insertions(+), 2 deletions(-) diff --git a/docs/website-api/AI_PROMPT.md b/docs/website-api/AI_PROMPT.md index a310890..03129f3 100644 --- a/docs/website-api/AI_PROMPT.md +++ b/docs/website-api/AI_PROMPT.md @@ -31,7 +31,7 @@ You are building a **Meshkee business website (storefront)**. You must use the M ### Typical bootstrap sequence 1. `GET /tenants/{domain}` → branding + `businessId` + `specialProductsSource` (`product` or `store_item`) 2. Homepage: business-info, **static-images**, sliders, category-groups, brand-groups, store-specials (`source` repeats the tenant setting; items are store listings when `source` is `store_item`) -3. Catalog: categories, products (`GET /products/{slug}` includes `relatedProducts`: same category then same brand, in-stock first), store-items (`name` instant search: in-stock first, then `updatedAt`), **user-products** (customer stock listings) +3. Catalog: categories, products (`GET /products/{slug}` includes `relatedProducts`: same category then same brand, in-stock first), store-items (`name` instant search: in-stock first, then `updatedAt`), **user-products** (customer stock listings). List filters: products/blogs/portfolios/videos accept `?tag=` (exact match on `metadata.tags`). 4. Auth: register/login → store tokens. Optional: `POST /auth/send-otp` then `POST /auth/login-otp` (passwordless) or `POST /auth/reset-password` (forgot password). `POST /auth/verify-otp` only marks the cell verified (no tokens). 5. Cart checkout with `addressId` or inline `shippingAddress` + `payment` - For online pay: `payment.type = "e_payment_gate"`, `gatewayType` (e.g. `"mellat"` or `"zarinpal"`), and absolute `returnUrl` diff --git a/docs/website-api/Meshkee-Website-API.postman_collection.json b/docs/website-api/Meshkee-Website-API.postman_collection.json index 6234119..9a5da17 100644 --- a/docs/website-api/Meshkee-Website-API.postman_collection.json +++ b/docs/website-api/Meshkee-Website-API.postman_collection.json @@ -715,6 +715,11 @@ "key": "title", "value": "", "disabled": true + }, + { + "key": "tag", + "value": "", + "disabled": true } ] } @@ -822,6 +827,11 @@ "key": "title", "value": "", "disabled": true + }, + { + "key": "tag", + "value": "", + "disabled": true } ] } @@ -924,6 +934,11 @@ "key": "title", "value": "", "disabled": true + }, + { + "key": "tag", + "value": "", + "disabled": true } ] } diff --git a/docs/website-api/openapi.json b/docs/website-api/openapi.json index 9742ed2..c1d0320 100644 --- a/docs/website-api/openapi.json +++ b/docs/website-api/openapi.json @@ -933,6 +933,14 @@ "schema": { "type": "string" } + }, + { + "name": "tag", + "in": "query", + "description": "Filter by exact tag in metadata.tags", + "schema": { + "type": "string" + } } ], "responses": { @@ -1094,6 +1102,14 @@ "schema": { "type": "string" } + }, + { + "name": "tag", + "in": "query", + "description": "Filter by exact tag in metadata.tags", + "schema": { + "type": "string" + } } ], "responses": { @@ -1244,6 +1260,14 @@ "schema": { "type": "string" } + }, + { + "name": "tag", + "in": "query", + "description": "Filter by exact tag in metadata.tags", + "schema": { + "type": "string" + } } ], "responses": { diff --git a/src/blogs/blogs.service.ts b/src/blogs/blogs.service.ts index 0bd0bd8..49b93c4 100644 --- a/src/blogs/blogs.service.ts +++ b/src/blogs/blogs.service.ts @@ -502,6 +502,14 @@ export class BlogsService { title: { contains: query.title.trim(), mode: 'insensitive' }, } : {}), + ...(query.tag?.trim() + ? { + metadata: { + path: ['tags'], + array_contains: query.tag.trim(), + }, + } + : {}), }; } diff --git a/src/blogs/dto/blog.dto.ts b/src/blogs/dto/blog.dto.ts index a41cf36..77a6422 100644 --- a/src/blogs/dto/blog.dto.ts +++ b/src/blogs/dto/blog.dto.ts @@ -39,6 +39,10 @@ export class ListBlogsDto { @IsOptional() @IsString() title?: string; + + @IsOptional() + @IsString() + tag?: string; } export class ListPublicBlogsDto { @@ -65,6 +69,10 @@ export class ListPublicBlogsDto { @IsOptional() @IsString() title?: string; + + @IsOptional() + @IsString() + tag?: string; } export class CreateBlogDto { diff --git a/src/portfolios/dto/portfolio.dto.ts b/src/portfolios/dto/portfolio.dto.ts index af523db..9f589e6 100644 --- a/src/portfolios/dto/portfolio.dto.ts +++ b/src/portfolios/dto/portfolio.dto.ts @@ -35,6 +35,10 @@ export class ListPortfoliosDto { @IsOptional() @IsString() title?: string; + + @IsOptional() + @IsString() + tag?: string; } export class ListPublicPortfoliosDto { @@ -57,6 +61,10 @@ export class ListPublicPortfoliosDto { @IsOptional() @IsString() title?: string; + + @IsOptional() + @IsString() + tag?: string; } export class CreatePortfolioDto { diff --git a/src/portfolios/portfolios.service.ts b/src/portfolios/portfolios.service.ts index 2688a6d..e46bd36 100644 --- a/src/portfolios/portfolios.service.ts +++ b/src/portfolios/portfolios.service.ts @@ -607,6 +607,14 @@ export class PortfoliosService { ], } : {}), + ...(query.tag?.trim() + ? { + metadata: { + path: ['tags'], + array_contains: query.tag.trim(), + }, + } + : {}), }; } diff --git a/src/videos/dto/video.dto.ts b/src/videos/dto/video.dto.ts index 68c49eb..74ff71a 100644 --- a/src/videos/dto/video.dto.ts +++ b/src/videos/dto/video.dto.ts @@ -39,6 +39,10 @@ export class ListVideosDto { @IsOptional() @IsString() title?: string; + + @IsOptional() + @IsString() + tag?: string; } export class ListPublicVideosDto { @@ -65,6 +69,10 @@ export class ListPublicVideosDto { @IsOptional() @IsString() title?: string; + + @IsOptional() + @IsString() + tag?: string; } export class CreateVideoDto { diff --git a/src/videos/videos.service.ts b/src/videos/videos.service.ts index 97a1613..5dd8d97 100644 --- a/src/videos/videos.service.ts +++ b/src/videos/videos.service.ts @@ -566,6 +566,14 @@ export class VideosService { ], } : {}), + ...(query.tag?.trim() + ? { + metadata: { + path: ['tags'], + array_contains: query.tag.trim(), + }, + } + : {}), }; } diff --git a/src/website-docs/static/AI_PROMPT.md b/src/website-docs/static/AI_PROMPT.md index a310890..03129f3 100644 --- a/src/website-docs/static/AI_PROMPT.md +++ b/src/website-docs/static/AI_PROMPT.md @@ -31,7 +31,7 @@ You are building a **Meshkee business website (storefront)**. You must use the M ### Typical bootstrap sequence 1. `GET /tenants/{domain}` → branding + `businessId` + `specialProductsSource` (`product` or `store_item`) 2. Homepage: business-info, **static-images**, sliders, category-groups, brand-groups, store-specials (`source` repeats the tenant setting; items are store listings when `source` is `store_item`) -3. Catalog: categories, products (`GET /products/{slug}` includes `relatedProducts`: same category then same brand, in-stock first), store-items (`name` instant search: in-stock first, then `updatedAt`), **user-products** (customer stock listings) +3. Catalog: categories, products (`GET /products/{slug}` includes `relatedProducts`: same category then same brand, in-stock first), store-items (`name` instant search: in-stock first, then `updatedAt`), **user-products** (customer stock listings). List filters: products/blogs/portfolios/videos accept `?tag=` (exact match on `metadata.tags`). 4. Auth: register/login → store tokens. Optional: `POST /auth/send-otp` then `POST /auth/login-otp` (passwordless) or `POST /auth/reset-password` (forgot password). `POST /auth/verify-otp` only marks the cell verified (no tokens). 5. Cart checkout with `addressId` or inline `shippingAddress` + `payment` - For online pay: `payment.type = "e_payment_gate"`, `gatewayType` (e.g. `"mellat"` or `"zarinpal"`), and absolute `returnUrl` diff --git a/src/website-docs/static/Meshkee-Website-API.postman_collection.json b/src/website-docs/static/Meshkee-Website-API.postman_collection.json index 6234119..9a5da17 100644 --- a/src/website-docs/static/Meshkee-Website-API.postman_collection.json +++ b/src/website-docs/static/Meshkee-Website-API.postman_collection.json @@ -715,6 +715,11 @@ "key": "title", "value": "", "disabled": true + }, + { + "key": "tag", + "value": "", + "disabled": true } ] } @@ -822,6 +827,11 @@ "key": "title", "value": "", "disabled": true + }, + { + "key": "tag", + "value": "", + "disabled": true } ] } @@ -924,6 +934,11 @@ "key": "title", "value": "", "disabled": true + }, + { + "key": "tag", + "value": "", + "disabled": true } ] } diff --git a/src/website-docs/static/openapi.json b/src/website-docs/static/openapi.json index 9742ed2..c1d0320 100644 --- a/src/website-docs/static/openapi.json +++ b/src/website-docs/static/openapi.json @@ -933,6 +933,14 @@ "schema": { "type": "string" } + }, + { + "name": "tag", + "in": "query", + "description": "Filter by exact tag in metadata.tags", + "schema": { + "type": "string" + } } ], "responses": { @@ -1094,6 +1102,14 @@ "schema": { "type": "string" } + }, + { + "name": "tag", + "in": "query", + "description": "Filter by exact tag in metadata.tags", + "schema": { + "type": "string" + } } ], "responses": { @@ -1244,6 +1260,14 @@ "schema": { "type": "string" } + }, + { + "name": "tag", + "in": "query", + "description": "Filter by exact tag in metadata.tags", + "schema": { + "type": "string" + } } ], "responses": {