@rezamirzapour/pod-sdk 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @rezamirzapour/pod-sdk
2
2
 
3
- > Unified, type-safe, enterprise-grade SDK for **POD Platform** microservices (SSO, CustomPost, Podspace, Podform, Notification, Social, CMS, IUMS), engineered specifically for **Next.js App Router**, **Server Components**, and **Server Actions**.
3
+ > Unified, type-safe, enterprise-grade SDK for **POD Platform** microservices (CMS Content, CMS Product, SSO, CustomPost, Podspace, Podform, Notification, Social, IUMS), engineered specifically for **Next.js App Router**, **Server Components**, and **Server Actions**.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@rezamirzapour/pod-sdk.svg)](https://www.npmjs.com/package/@rezamirzapour/pod-sdk)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
@@ -10,10 +10,11 @@
10
10
 
11
11
  ## Features
12
12
 
13
- - 🌐 **Unified Multi-Service Architecture**: Single entry-point managing 8 POD microservices with official endpoints and configurations.
13
+ - 🌐 **Unified Multi-Service Architecture**: Single entry-point managing official POD microservices with official endpoints and configurations.
14
14
  - ⚡ **Powered by `@rezamirzapour/http`**: Automatic exponential backoff retries, Next.js incremental static regeneration (ISR) caching (`next.revalidate`, tags), and isomorphic execution.
15
- - 🧬 **First-Class TypeScript Generics (`<T, P>`)**: CMS and CustomPost endpoints allow you to define and receive strongly typed metadata structures according to your business schemas.
16
- - 🎨 **Automatic CMS Data Formatter**: Automatically translates POD CMS nested `metadata.content` arrays into flat, strongly typed objects (`formatted[fieldCode]`) and resolves image hashes to CDN URLs.
15
+ - 🧬 **First-Class TypeScript Generics (`<T, P>`)**: CMS Content, CMS Product, and CustomPost endpoints allow you to define and receive strongly typed metadata structures according to your business schemas.
16
+ - 🎨 **Automatic CMS Data Formatter**: Automatically translates nested `metadata.content` and `metadata.product` arrays into flat, strongly typed objects (`formatted[fieldCode]`) and resolves image hashes to Podspace CDN URLs.
17
+ - 🛍️ **Full CMS Product Module (RAD API Swagger)**: Complete support for published products, price/discount filters, barcodes, batch publish/unpublish, archiving, and AI timeline search.
17
18
  - 📦 **Generic CRUD Repository for CustomPost**: Instantiate typed data repositories on POD CustomPost in 2 lines with `create`, `getById`, `getAll`, `createAndBindEntityId`, and `archive`.
18
19
  - 🔐 **Zero-Dependency Web Crypto RSA Signing**: Built-in RSA-SHA256 signature calculation for POD SSO OTP handshakes using standard Web Crypto API (Node.js, Edge Runtime, Browsers).
19
20
  - 🚀 **Next.js App Router Ready**: Seamless support for Server Components, Server Actions, Route Handlers, and Client Components.
@@ -23,16 +24,18 @@
23
24
 
24
25
  ## Microservices Included
25
26
 
26
- | Service | Property | Description |
27
- | :--- | :--- | :--- |
28
- | **CMS** | `sdk.cms` | Fetch, publish, edit, and categorize CMS articles with generic metadata `<T, P>` and automatic image formatting. |
29
- | **CustomPost** | `sdk.customPost` | Search timeline by metadata `<T>`, custom post CRUD, and high-level typed repository. |
30
- | **SSO** | `sdk.sso` | OAuth2 handshake, OTP dispatch with digital RSA signature, OTP verify, token generation, and user profile. |
31
- | **Podspace** | `sdk.podspace` | File upload (FormData) and public/private download URL resolution. |
32
- | **Podform** | `sdk.podform` | Survey & form response submission, question/form structure retrieval. |
33
- | **Notification** | `sdk.notification` | SMS delivery and bulk messaging with tracking. |
34
- | **Social** | `sdk.social` | User comments, reactions (likes/dislikes), rates, and social post interactions. |
35
- | **IUMS** | `sdk.iums` | Student and user identity inspection by national code or student ID. |
27
+ | Service | Accessor | Swagger Tag | Description |
28
+ | :--- | :--- | :--- | :--- |
29
+ | **CMS Content** | `sdk.cms` | `content` | Fetch, publish, edit, draft, archive, and categorize CMS articles with generic metadata `<T, P>`. |
30
+ | **CMS Product** | `sdk.product` / `sdk.cms.products` | `product` | Product catalog, price & discount range filters, barcode lookup, batch publish, and AI search. |
31
+ | **CMS Tags & Tree** | `sdk.tags` / `sdk.cms.tags` | `tags` | Manage tag categories and hierarchical tag trees (nodes, parent updates, ancestors, codes). |
32
+ | **CustomPost** | `sdk.customPost` | - | Search timeline by metadata `<T>`, custom post CRUD, and high-level typed repository. |
33
+ | **SSO** | `sdk.sso` | - | OAuth2 handshake, OTP dispatch with digital RSA signature, OTP verify, token generation, and user profile. |
34
+ | **Podspace** | `sdk.podspace` | - | File upload (FormData) and public/private download URL resolution. |
35
+ | **Podform** | `sdk.podform` | - | Survey & form response submission, question/form structure retrieval. |
36
+ | **Notification** | `sdk.notification` | - | SMS delivery and bulk messaging with tracking. |
37
+ | **Social** | `sdk.social` | - | User comments, reactions (likes/dislikes), rates, and social post interactions. |
38
+ | **IUMS** | `sdk.iums` | - | Student and user identity inspection by national code or student ID. |
36
39
 
37
40
  ---
38
41
 
@@ -73,10 +76,13 @@ export const podSdk = createPodSdk({
73
76
 
74
77
  ---
75
78
 
76
- ## Deep Dive: CMS & Generic Data Formatting
79
+ ## Deep Dive 1: CMS Content & Data Formatting
77
80
 
78
81
  POD CMS stores dynamic fields inside a `metadata.content` array of `{ code, value, type }` objects. `@rezamirzapour/pod-sdk` automatically formats these into clean typed key-value pairs (`item.formatted`) and allows an optional custom `normalizer` function (`item.__normalized`).
79
82
 
83
+ > [!NOTE]
84
+ > `getContent2` has been removed. For authenticated/management content retrieval (using `Access-Token`), call `podSdk.cms.getAllContents()` or pass headers to `getContent()`.
85
+
80
86
  ### 🏗️ Clean Architecture Pattern: Dedicated Data Access Layer (`NewsApi`)
81
87
 
82
88
  In enterprise Next.js applications, avoid calling SDK clients directly inside UI components. Instead, encapsulate CMS queries, generics, normalizers, and caching logic inside a dedicated Data Access Layer (`services/newsApi.ts`).
@@ -221,12 +227,8 @@ export class NewsApi {
221
227
  }
222
228
  ```
223
229
 
224
- ---
225
-
226
230
  #### Step 2: Consume `NewsApi` in Next.js Server Components (`app/news/page.tsx`)
227
231
 
228
- Notice how clean the UI layer becomes! No low-level SDK parameters, headers, or raw hashes leaked into your views:
229
-
230
232
  ```tsx
231
233
  // app/news/page.tsx (Server Component)
232
234
  import { NewsApi } from '@/services/newsApi';
@@ -276,237 +278,298 @@ export default async function NewsPage({ searchParams }: NewsPageProps) {
276
278
 
277
279
  ---
278
280
 
279
- #### Step 3: Detail Page with `NewsApi.getNewsById` (`app/news/[id]/page.tsx`)
280
-
281
- ```tsx
282
- // app/news/[id]/page.tsx (Server Component)
283
- import { NewsApi } from '@/services/newsApi';
284
- import { notFound } from 'next/navigation';
285
-
286
- interface NewsDetailPageProps {
287
- params: { id: string };
288
- }
289
-
290
- export default async function NewsDetailPage({ params }: NewsDetailPageProps) {
291
- const news = await NewsApi.getNewsById(Number(params.id));
292
-
293
- if (!news) {
294
- notFound();
295
- }
281
+ ## Deep Dive 2: CMS Product Module (RAD API Swagger)
296
282
 
297
- return (
298
- <article className="max-w-3xl mx-auto py-10 px-4">
299
- {news.coverImage && (
300
- <img
301
- src={news.coverImage}
302
- alt={news.title}
303
- className="w-full h-72 object-cover rounded-xl mb-6"
304
- />
305
- )}
306
- <h1 className="text-3xl font-bold mb-3">{news.title}</h1>
307
- <p className="text-sm text-gray-500 mb-6">{news.readTime} min read</p>
308
- <div className="prose lg:prose-lg" dangerouslySetInnerHTML={{ __html: news.body }} />
309
- </article>
310
- );
311
- }
312
- ```
283
+ The CMS Product module connects directly to POD RAD API endpoints ([Swagger documentation](https://rad-sandbox.sandpod.ir/api/documentation?tag=product)). It handles products with prices, discounts, barcodes, dynamic `metadata.product` field formatting, and batch management.
313
284
 
314
- ---
285
+ ### Accessing Products
315
286
 
316
- #### Step 4: Server Action for CMS Publishing (`app/actions/news.ts`)
287
+ You can access product methods either via `podSdk.product` or `podSdk.cms.products`:
317
288
 
318
289
  ```typescript
319
- // app/actions/news.ts
320
- 'use server';
321
-
322
- import { NewsApi } from '@/services/newsApi';
323
- import { revalidatePath } from 'next/cache';
324
-
325
- export async function publishArticle(formData: FormData) {
326
- const title = formData.get('title') as string;
327
- const lead = formData.get('lead') as string;
328
- const body = formData.get('body') as string;
329
- const imageHash = formData.get('imageHash') as string;
330
-
331
- const result = await NewsApi.publishNews({
332
- title,
333
- lead,
334
- body,
335
- imageHash,
336
- });
337
-
338
- revalidatePath('/news');
339
- return { success: !result.hasError };
340
- }
290
+ // Both point to the same CmsProductService instance
291
+ podSdk.product.getProducts(...);
292
+ podSdk.cms.products.getProducts(...);
341
293
  ```
342
294
 
343
- ---
344
-
345
- ## Deep Dive: CustomPost & Generic Typed Repositories
346
-
347
- `CustomPost` allows persisting arbitrary JSON schemas on the POD Platform. `@rezamirzapour/pod-sdk` provides two complementary approaches:
348
-
349
- 1. **Direct Service Method Calls with Generics (`searchTimelineByMetadata<T>`, `getCustomPost<T>`, `addCustomPost<T>`)**
350
- 2. **High-Level Typed CRUD Repository (`CustomPostCrudService<DataType>`)**
351
-
352
- ### 1. Direct Generic Method Usage
295
+ ### 🛍️ Clean Architecture Pattern: Product Data Access Layer (`ProductsApi`)
353
296
 
354
297
  ```typescript
355
- // lib/blog.ts
298
+ // services/productsApi.ts
356
299
  import { podSdk } from '@/lib/pod';
357
300
 
358
- export interface BlogPostMeta {
359
- slug: string;
360
- title: string;
361
- summary: string;
362
- views: number;
363
- publishedAt: string;
301
+ // 1. Raw formatted product metadata (T)
302
+ export interface LaptopFormatted {
303
+ cpu: string;
304
+ ram: string;
305
+ storage: string;
306
+ displayPhoto: string; // Auto-transformed from hash to Podspace CDN URL
364
307
  }
365
308
 
366
- // 1. Search posts with typed metadata query
367
- export async function getPublishedPosts() {
368
- const response = await podSdk.customPost.searchTimelineByMetadata<BlogPostMeta>({
369
- entityName: 'blog_post',
370
- metadata: { views: 100 }, // Partial match or query
371
- size: 20,
372
- offset: 0,
373
- });
374
-
375
- // response.result[i].item.metadata is strongly typed as BlogPostMeta
376
- return response.result.map((r) => r.item.metadata);
309
+ // 2. Computed / normalized presentation fields (P)
310
+ export interface LaptopNormalized {
311
+ finalPrice: number;
312
+ discountBadge: string;
377
313
  }
378
314
 
379
- // 2. Add a new custom post with typed metadata
380
- export async function createBlogPost(meta: BlogPostMeta) {
381
- return podSdk.customPost.addCustomPost<BlogPostMeta>({
382
- name: 'blog_post',
383
- content: meta.title,
384
- metadata: meta, // Strongly typed
385
- enable: true,
386
- });
387
- }
388
- ```
389
-
390
- ### 2. High-Level Generic CRUD Repository (`createCrud<DataType>`)
391
-
392
- Create an ORM-like typed repository for any entity type and expose it through a dedicated service layer:
393
-
394
- #### Step 1: Create the Products Repository & API (`services/productsApi.ts`)
395
-
396
- ```typescript
397
- // services/productsApi.ts
398
- import { podSdk } from '@/lib/pod';
399
-
400
- export interface ProductData {
401
- title: string;
402
- sku: string;
315
+ // 3. Clean Domain Product Model
316
+ export interface ProductItemModel {
317
+ entityId: number;
318
+ name: string;
403
319
  price: number;
404
- inStock: boolean;
405
- category: string;
406
- entityId?: number;
320
+ finalPrice: number;
321
+ discountBadge: string;
322
+ photoUrl: string;
323
+ specs: {
324
+ cpu: string;
325
+ ram: string;
326
+ storage: string;
327
+ };
407
328
  }
408
329
 
409
- // 1. Instantiate typed CRUD service for "product_entity"
410
- const productCrud = podSdk.createCrud<ProductData>({
411
- name: 'product_entity',
412
- type: 'commerce',
413
- detailedType: 'gadgets',
414
- });
415
-
416
- // 2. Encapsulate business queries and domain operations
417
330
  export class ProductsApi {
418
- /**
419
- * Retrieves all products formatted as clean domain objects
420
- */
421
- public static async getProducts(): Promise<ProductData[]> {
422
- const result = await productCrud.getAll();
423
- return result.result.map((r) => r.item.metadata.data);
424
- }
331
+ private static readonly PRODUCT_TYPE = 'laptops';
425
332
 
426
333
  /**
427
- * Retrieves a single product by numerical entity ID
334
+ * Fetches published products with price & discount filters
428
335
  */
429
- public static async getProductById(id: number): Promise<ProductData | null> {
430
- const res = await productCrud.getById(id);
431
- return res.result?.[0]?.metadata.data || null;
432
- }
336
+ public static async getProducts(params: {
337
+ fromPrice?: number;
338
+ toPrice?: number;
339
+ barcode?: string;
340
+ page?: number;
341
+ pageSize?: number;
342
+ } = {}): Promise<ProductItemModel[]> {
343
+ const { fromPrice, toPrice, barcode, page = 1, pageSize = 20 } = params;
344
+ const offset = (page - 1) * pageSize;
433
345
 
434
- /**
435
- * Creates a product and automatically binds the generated entityId
436
- */
437
- public static async createProduct(data: Omit<ProductData, 'entityId'>) {
438
- return productCrud.createAndBindEntityId({
439
- ...data,
440
- inStock: true,
441
- });
346
+ const response = await podSdk.product.getProducts<LaptopFormatted, LaptopNormalized>(
347
+ {
348
+ productTypeUniqueId: this.PRODUCT_TYPE,
349
+ fromPrice,
350
+ toPrice,
351
+ barcode,
352
+ size: pageSize,
353
+ offset,
354
+ },
355
+ {
356
+ normalizer: (item) => {
357
+ const discount = item.discount || 0;
358
+ const finalPrice = item.price ? item.price * (1 - discount / 100) : 0;
359
+ return {
360
+ finalPrice,
361
+ discountBadge: discount > 0 ? `${discount}% OFF` : '',
362
+ };
363
+ },
364
+ revalidate: 3600, // Next.js ISR: cache for 1 hour
365
+ }
366
+ );
367
+
368
+ if (response.hasError || !Array.isArray(response.result)) {
369
+ return [];
370
+ }
371
+
372
+ return response.result.map((item) => ({
373
+ entityId: item.entityId,
374
+ name: item.name || '',
375
+ price: item.price || 0,
376
+ finalPrice: item.__normalized.finalPrice,
377
+ discountBadge: item.__normalized.discountBadge,
378
+ photoUrl: item.formatted.displayPhoto,
379
+ specs: {
380
+ cpu: item.formatted.cpu,
381
+ ram: item.formatted.ram,
382
+ storage: item.formatted.storage,
383
+ },
384
+ }));
442
385
  }
443
386
 
444
387
  /**
445
- * Updates an existing product
388
+ * Retrieves single product by entityId
446
389
  */
447
- public static async updateProduct(entityId: number, data: ProductData) {
448
- return productCrud.update(entityId, data);
390
+ public static async getProductById(entityId: number) {
391
+ const res = await podSdk.product.getProductByEntityId<LaptopFormatted, LaptopNormalized>({
392
+ entityId,
393
+ productTypeUniqueId: this.PRODUCT_TYPE,
394
+ });
395
+ return res.result?.[0] || null;
449
396
  }
450
397
 
451
398
  /**
452
- * Soft-archives a product without deleting history
399
+ * Look up product by barcode scanner
453
400
  */
454
- public static async archiveProduct(entityId: number, data: ProductData) {
455
- return productCrud.archive(entityId, data);
401
+ public static async getProductByBarcode(barcode: string) {
402
+ return podSdk.product.getProductByBarcode<LaptopFormatted>(barcode);
456
403
  }
457
404
 
458
405
  /**
459
- * Deletes a product permanently
406
+ * Adds and publishes a new product
460
407
  */
461
- public static async deleteProduct(entityId: number) {
462
- return productCrud.delete(entityId);
408
+ public static async createProduct(data: {
409
+ name: string;
410
+ price: number;
411
+ cpu: string;
412
+ ram: string;
413
+ storage: string;
414
+ photoHash: string;
415
+ }) {
416
+ return podSdk.product.addProduct({
417
+ productTypeUniqueId: this.PRODUCT_TYPE,
418
+ body: {
419
+ name: data.name,
420
+ enable: true,
421
+ fieldCode: ['cpu', 'ram', 'storage', 'displayPhoto', 'price'],
422
+ fieldValue: [data.cpu, data.ram, data.storage, data.photoHash, data.price],
423
+ },
424
+ });
463
425
  }
464
426
  }
465
427
  ```
466
428
 
429
+ ### CMS Methods Reference
430
+
431
+ #### Content Methods (`podSdk.cms`)
432
+
433
+ | Method | HTTP | Path | Description |
434
+ | :--- | :--- | :--- | :--- |
435
+ | `getContent<T, P>(params, opts)` | `GET` | `/api/core/contents/enable` | Get published enabled contents with generic typing. |
436
+ | `getAllContents<T, P>(params, opts)` | `GET` | `/api/core/contents` | Get contents with managing credentials (`Access-Token`). |
437
+ | `getMyContents<T, P>(params, opts)` | `GET` | `/api/core/contents/my` | Get current user/client contents. |
438
+ | `getContentsByType<T, P>(typeId, params)` | `GET` | `/api/core/contents/{contentTypeUniqueId}` | Get contents by ContentType. |
439
+ | `getContentByEntityId<T, P>(params)` | `GET` | `/api/core/contents/enable/{entityId}` | Get single published content item. |
440
+ | `getContentByUniqueId<T, P>(uniqueId)` | `GET` | `/api/core/contents/{uniqueId}/byContentUniqueId/enable` | Get content by uniqueId. |
441
+ | `searchContent<T, P>(params)` | `GET` | `/api/core/contents/search` | Search published contents by keyword. |
442
+ | `addContent<T, P>(params)` | `POST` | `/api/core/contents/{contentTypeUniqueId}/add-publish` | Add and publish content item. |
443
+ | `createContent<T, P>(params)` | `POST` | `/api/core/contents/{contentTypeUniqueId}` | Create unpublished/draft content. |
444
+ | `editContent<T, P>(entityId, params)` | `POST` | `/api/core/contents/{typeId}/{entityId}/edit-publish` | Update and publish content item. |
445
+ | `updateContent<T, P>(entityId, params)` | `POST` | `/api/core/contents/{typeId}/{entityId}` | Update content item. |
446
+ | `patchContent<T, P>(entityId, params)` | `PATCH` | `/api/core/contents/{typeId}/{entityId}` | Partial update content. |
447
+ | `publishContent(typeId, entityId)` | `POST` | `/api/core/contents/{typeId}/publish/{entityId}` | Publish single content item. |
448
+ | `unpublishContent(typeId, entityId)` | `POST` | `/api/core/contents/{typeId}/unpublish/{entityId}` | Unpublish single content item. |
449
+ | `batchPublish(params)` | `PUT` | `/api/core/contents/{typeId}/publish` | Batch publish multiple contents. |
450
+ | `batchUnpublish(params)` | `PUT` | `/api/core/contents/{typeId}/unpublish` | Batch unpublish multiple contents. |
451
+ | `archiveContent(params)` | `POST` | `/api/core/contents/{typeId}/archive` | Archive content items. |
452
+ | `unarchiveContent(params)` | `POST` | `/api/core/contents/{typeId}/unarchive` | Unarchive content items. |
453
+ | `getArchivedContents(params)` | `GET` | `/api/core/contents/archive` | Get archived contents list. |
454
+ | `getDrafts(params)` | `GET` | `/api/core/contents/draft` | Get drafts list. |
455
+ | `createDraft(params)` | `POST` | `/api/core/contents/{typeId}/draft` | Create a draft. |
456
+ | `getComments(typeId, entityId)` | `GET` | `/api/core/contents/{typeId}/comments/{entityId}` | Get comments on content. |
457
+ | `getLikes(uniqueId, entityId)` | `GET` | `/api/core/contents/{uniqueId}/like/{entityId}` | Get likes of content item. |
458
+ | `getCategories(params)` | `GET` | `/api/core/tags/root/tree/enable` | Get category/tag tree. |
459
+ | `timelineSearch(query)` | `GET` | `/api/core/contents/ai/timeline-search/enable` | Advanced AI timeline search. |
460
+
461
+ #### Product Methods (`podSdk.product` / `podSdk.cms.products`)
462
+
463
+ | Method | HTTP | Path | Description |
464
+ | :--- | :--- | :--- | :--- |
465
+ | `getProducts<T, P>(params, opts)` | `GET` | `/api/core/products/enable` | Get enabled published products with price/discount filters. |
466
+ | `getAllProducts<T, P>(params, opts)` | `GET` | `/api/core/products` | Get products with provider credentials (`Access-Token`). |
467
+ | `getManageProducts<T, P>(params, opts)`| `GET` | `/api/core/products/manage` | Get products for manager view. |
468
+ | `getProductsByType<T, P>(typeId, params)`| `GET` | `/api/core/products/{productTypeUniqueId}` | Get products by ProductType. |
469
+ | `getProductByEntityId<T, P>(params)` | `GET` | `/api/core/products/enable/{entityId}` | Get single product by entityId. |
470
+ | `getProductByUniqueId<T, P>(uniqueId)` | `GET` | `/api/core/products/{uniqueId}/byProductUniqueId/enable` | Get product by uniqueId. |
471
+ | `getProductByBarcode<T, P>(barcode)` | `GET` | `/api/core/products/barcode/enable` | Lookup products matching a barcode. |
472
+ | `searchProducts<T, P>(params)` | `GET` | `/api/core/products/enable/search` | Search published products. |
473
+ | `addProduct<T, P>(params)` | `POST` | `/api/core/products/{productTypeUniqueId}/add-publish` | Create and publish new product. |
474
+ | `createProduct<T, P>(params)` | `POST` | `/api/core/products/{productTypeUniqueId}` | Create draft/unpublished product. |
475
+ | `editProduct<T, P>(entityId, params)` | `POST` | `/api/core/products/{typeId}/{entityId}/edit-publish` | Edit and publish existing product. |
476
+ | `updateProduct<T, P>(entityId, params)` | `POST` | `/api/core/products/{typeId}/{entityId}` | Update existing product. |
477
+ | `patchProduct<T, P>(entityId, params)` | `PATCH` | `/api/core/products/{typeId}/{entityId}` | Partial update product. |
478
+ | `publishProduct(typeId, entityId)` | `POST` | `/api/core/products/{typeId}/publish/{entityId}` | Publish product. |
479
+ | `unpublishProduct(typeId, entityId)` | `POST` | `/api/core/products/{typeId}/unpublish/{entityId}` | Unpublish product. |
480
+ | `batchPublish(params)` | `PUT` | `/api/core/products/publish` | Batch publish multiple products. |
481
+ | `batchUnpublish(params)` | `PUT` | `/api/core/products/unpublish` | Batch unpublish multiple products. |
482
+ | `archiveProduct(params)` | `POST` | `/api/core/products/archive` | Archive products. |
483
+ | `unarchiveProduct(params)` | `POST` | `/api/core/products/unarchive` | Unarchive products. |
484
+ | `getArchivedProducts(params)` | `GET` | `/api/core/products/archive` | Get archived products list. |
485
+ | `timelineSearch(query)` | `GET` | `/api/core/products/ai/timeline-search/enable` | AI timeline search on products. |
486
+
467
487
  ---
468
488
 
469
- #### Step 2: Consume `ProductsApi` in Server Components (`app/products/page.tsx`)
489
+ ## Deep Dive 3: CMS Tags & Tag Categories (RAD API Swagger: tag=tags)
470
490
 
471
- ```tsx
472
- // app/products/page.tsx (Server Component)
473
- import { ProductsApi } from '@/services/productsApi';
491
+ Manage tag categories and multi-level hierarchical tag trees ([Swagger documentation](https://rad-sandbox.sandpod.ir/api/documentation?tag=tags)).
474
492
 
475
- export default async function ProductsPage() {
476
- const products = await ProductsApi.getProducts();
493
+ ### Accessing Tag Services
477
494
 
478
- return (
479
- <main className="container mx-auto py-8 px-4">
480
- <h1 className="text-2xl font-bold mb-6">Products Catalog</h1>
481
- <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
482
- {products.map((p) => (
483
- <div key={p.sku} className="border p-4 rounded-lg shadow-sm">
484
- <h2 className="font-semibold text-lg">{p.title}</h2>
485
- <p className="text-gray-500 text-sm">SKU: {p.sku}</p>
486
- <p className="text-blue-600 font-bold mt-2">${p.price.toLocaleString()}</p>
487
- </div>
488
- ))}
489
- </div>
490
- </main>
491
- );
492
- }
495
+ Access via `podSdk.tags` or `podSdk.cms.tags`:
496
+
497
+ ```typescript
498
+ // Category Management (with Access-Token)
499
+ const categories = await podSdk.tags.getTagCategories({ size: 20 });
500
+ await podSdk.tags.createTagCategory({ name: 'Technology', desc: 'Tech articles' });
501
+ await podSdk.tags.publishTagCategory(categoryId);
502
+
503
+ // Public Hierarchical Tag Tree (without token, defaults to 'root')
504
+ const rootTree = await podSdk.tags.getTagTree('root', { levelCount: 3 });
505
+
506
+ // Backward-compatible shortcut
507
+ const categoriesTree = await podSdk.cms.getCategories({ levelCount: 3 });
508
+
509
+ // Tree Node Management (create, update parent, code, ancestors)
510
+ await podSdk.tags.createTagTreeItem('technology', {
511
+ name: 'Next.js',
512
+ code: 'NEXTJS',
513
+ parentId: 10,
514
+ });
515
+ await podSdk.tags.updateTagTreeParent('technology', nodeId, newParentId);
516
+ await podSdk.tags.updateTagTreeCode('technology', nodeId, 'NEW_CODE');
517
+ const ancestors = await podSdk.tags.getTagTreeAncestors('technology', nodeId);
493
518
  ```
494
519
 
520
+ ### Tag Methods Reference (`podSdk.tags` / `podSdk.cms.tags`)
521
+
522
+ | Method | HTTP | Path | Description |
523
+ | :--- | :--- | :--- | :--- |
524
+ | `getTagCategories(params)` | `GET` | `/api/core/tags/category` | List tag categories (manage). |
525
+ | `createTagCategory(params)` | `POST` | `/api/core/tags/category` | Create new tag category. |
526
+ | `getTagCategory(id)` | `GET` | `/api/core/tags/category/{id}` | Show single tag category. |
527
+ | `updateTagCategory(id, params)` | `PUT` | `/api/core/tags/category/{id}` | Update tag category. |
528
+ | `publishTagCategory(id)` | `PUT` | `/api/core/tags/category/{id}/publish` | Publish tag category. |
529
+ | `unpublishTagCategory(id)` | `PUT` | `/api/core/tags/category/{id}/unpublish` | Unpublish tag category. |
530
+ | `getTagTree(categoryId, params)` | `GET` | `/api/core/tags/{categoryId}/tree/enable` | Get enabled tag tree (public). |
531
+ | `getManageTagTree(categoryId, params)` | `GET` | `/api/core/tags/{categoryId}/tree/manage` | Get tag tree (manage with token). |
532
+ | `createTagTreeItem(categoryId, params)` | `POST` | `/api/core/tags/{categoryId}/tree` | Create tag tree node for category. |
533
+ | `getTagTreeItem(categoryId, id)` | `GET` | `/api/core/tags/{categoryId}/tree/{id}/enable` | Show enabled tag tree node. |
534
+ | `getManageTagTreeItem(categoryId, id)` | `GET` | `/api/core/tags/{categoryId}/tree/{id}/manage` | Show tag tree node (manage). |
535
+ | `updateTagTreeItem(categoryId, id, params)` | `PUT` | `/api/core/tags/{categoryId}/tree/{id}` | Update tag tree node. |
536
+ | `publishTagTreeItem(categoryId, id)` | `PUT` | `/api/core/tags/{categoryId}/tree/publish/{id}` | Publish tag tree node. |
537
+ | `unpublishTagTreeItem(categoryId, id)` | `PUT` | `/api/core/tags/{categoryId}/tree/unpublish/{id}` | Unpublish tag tree node. |
538
+ | `updateTagTreeParent(categoryId, id, parentId)` | `PUT` | `/api/core/tags/{categoryId}/tree/parent/{id}` | Update parent of tag tree node. |
539
+ | `getTagTreeAncestors(categoryId, id, params)` | `GET` | `/api/core/tags/{categoryId}/tree/parent/{id}/enable` | Get node ancestors (public). |
540
+ | `getManageTagTreeAncestors(categoryId, id, params)`| `GET` | `/api/core/tags/{categoryId}/tree/parent/{id}/manage` | Get node ancestors (manage). |
541
+ | `updateTagTreeCode(categoryId, id, code)` | `PUT` | `/api/core/tags/{categoryId}/tree/code/{id}` | Update code of tag tree node. |
542
+
495
543
  ---
496
544
 
497
- #### Step 3: Mutate via Server Actions (`app/actions/products.ts`)
545
+ ## Deep Dive 4: CustomPost & Generic Typed Repositories
546
+
547
+ `CustomPost` allows persisting arbitrary JSON schemas on the POD Platform. `@rezamirzapour/pod-sdk` provides two complementary approaches:
548
+
549
+ 1. **Direct Service Method Calls with Generics (`searchTimelineByMetadata<T>`, `getCustomPost<T>`, `addCustomPost<T>`)**
550
+ 2. **High-Level Typed CRUD Repository (`CustomPostCrudService<DataType>`)**
498
551
 
499
552
  ```typescript
500
- // app/actions/products.ts
501
- 'use server';
553
+ // lib/blog.ts
554
+ import { podSdk } from '@/lib/pod';
502
555
 
503
- import { ProductsApi } from '@/services/productsApi';
504
- import { revalidatePath } from 'next/cache';
556
+ export interface BlogPostMeta {
557
+ slug: string;
558
+ title: string;
559
+ summary: string;
560
+ views: number;
561
+ }
505
562
 
506
- export async function addProduct(data: { title: string; sku: string; price: number; category: string }) {
507
- const result = await ProductsApi.createProduct(data);
508
- revalidatePath('/products');
509
- return result;
563
+ // Search posts with typed metadata query
564
+ export async function getPublishedPosts() {
565
+ const response = await podSdk.customPost.searchTimelineByMetadata<BlogPostMeta>({
566
+ entityName: 'blog_post',
567
+ metadata: { views: 100 },
568
+ size: 20,
569
+ offset: 0,
570
+ });
571
+
572
+ return response.result.map((r) => r.item.metadata);
510
573
  }
511
574
  ```
512
575
 
@@ -514,7 +577,7 @@ export async function addProduct(data: { title: string; sku: string; price: numb
514
577
 
515
578
  ## Other POD Microservices
516
579
 
517
- ### 3. POD SSO (OTP Handshake with Web Crypto RSA Signature)
580
+ ### 4. POD SSO (OTP Handshake with Web Crypto RSA Signature)
518
581
 
519
582
  ```typescript
520
583
  // app/actions/auth.ts
@@ -524,7 +587,6 @@ import { podSdk } from '@/lib/pod';
524
587
 
525
588
  // Step 1: Handshake and send OTP
526
589
  export async function sendOtp(phoneNumber: string, clientIp: string) {
527
- // Handshake to obtain keyId
528
590
  const handshake = await podSdk.sso.handshake(`web-${Date.now()}`, clientIp);
529
591
  if (handshake.hasError || !handshake.result?.keyId) {
530
592
  throw new Error(handshake.message || 'SSO Handshake failed');
@@ -545,7 +607,6 @@ export async function verifyOtp(authorization: string, phoneNumber: string, code
545
607
  throw new Error(verify.message || 'OTP verification failed');
546
608
  }
547
609
 
548
- // Exchange authorization code for JWT tokens
549
610
  const tokens = await podSdk.sso.generateToken(verify.result.code);
550
611
  return tokens.result; // { access_token, refresh_token, expires_in }
551
612
  }
@@ -559,7 +620,7 @@ export async function getProfile(accessToken: string) {
559
620
 
560
621
  ---
561
622
 
562
- ### 4. Podspace (File Uploads & CDN URLs)
623
+ ### 5. Podspace (File Uploads & CDN URLs)
563
624
 
564
625
  ```typescript
565
626
  // app/actions/upload.ts
@@ -580,7 +641,7 @@ export async function uploadFile(formData: FormData) {
580
641
 
581
642
  ---
582
643
 
583
- ### 5. Notification (SMS Delivery)
644
+ ### 6. Notification (SMS Delivery)
584
645
 
585
646
  ```typescript
586
647
  // app/actions/notify.ts
@@ -598,7 +659,7 @@ export async function sendSms(phoneNumber: string, text: string) {
598
659
 
599
660
  ---
600
661
 
601
- ### 6. Social (Comments & Likes)
662
+ ### 7. Social (Comments & Likes)
602
663
 
603
664
  ```typescript
604
665
  // app/actions/social.ts
@@ -620,7 +681,7 @@ export async function likePost(postId: number) {
620
681
 
621
682
  ---
622
683
 
623
- ### 7. IUMS (Identity & University Management Service)
684
+ ### 8. IUMS (Identity & University Management Service)
624
685
 
625
686
  ```typescript
626
687
  // app/actions/iums.ts