@rezamirzapour/pod-sdk 1.0.2 → 1.0.3

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,17 @@
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
+ | **CustomPost** | `sdk.customPost` | - | Search timeline by metadata `<T>`, custom post CRUD, and high-level typed repository. |
32
+ | **SSO** | `sdk.sso` | - | OAuth2 handshake, OTP dispatch with digital RSA signature, OTP verify, token generation, and user profile. |
33
+ | **Podspace** | `sdk.podspace` | - | File upload (FormData) and public/private download URL resolution. |
34
+ | **Podform** | `sdk.podform` | - | Survey & form response submission, question/form structure retrieval. |
35
+ | **Notification** | `sdk.notification` | - | SMS delivery and bulk messaging with tracking. |
36
+ | **Social** | `sdk.social` | - | User comments, reactions (likes/dislikes), rates, and social post interactions. |
37
+ | **IUMS** | `sdk.iums` | - | Student and user identity inspection by national code or student ID. |
36
38
 
37
39
  ---
38
40
 
@@ -73,10 +75,13 @@ export const podSdk = createPodSdk({
73
75
 
74
76
  ---
75
77
 
76
- ## Deep Dive: CMS & Generic Data Formatting
78
+ ## Deep Dive 1: CMS Content & Data Formatting
77
79
 
78
80
  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
81
 
82
+ > [!NOTE]
83
+ > `getContent2` has been removed. For authenticated/management content retrieval (using `Access-Token`), call `podSdk.cms.getAllContents()` or pass headers to `getContent()`.
84
+
80
85
  ### 🏗️ Clean Architecture Pattern: Dedicated Data Access Layer (`NewsApi`)
81
86
 
82
87
  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 +226,8 @@ export class NewsApi {
221
226
  }
222
227
  ```
223
228
 
224
- ---
225
-
226
229
  #### Step 2: Consume `NewsApi` in Next.js Server Components (`app/news/page.tsx`)
227
230
 
228
- Notice how clean the UI layer becomes! No low-level SDK parameters, headers, or raw hashes leaked into your views:
229
-
230
231
  ```tsx
231
232
  // app/news/page.tsx (Server Component)
232
233
  import { NewsApi } from '@/services/newsApi';
@@ -276,237 +277,242 @@ export default async function NewsPage({ searchParams }: NewsPageProps) {
276
277
 
277
278
  ---
278
279
 
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
- }
280
+ ## Deep Dive 2: CMS Product Module (RAD API Swagger)
296
281
 
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
- ```
282
+ 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
283
 
314
- ---
284
+ ### Accessing Products
315
285
 
316
- #### Step 4: Server Action for CMS Publishing (`app/actions/news.ts`)
286
+ You can access product methods either via `podSdk.product` or `podSdk.cms.products`:
317
287
 
318
288
  ```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
- }
289
+ // Both point to the same CmsProductService instance
290
+ podSdk.product.getProducts(...);
291
+ podSdk.cms.products.getProducts(...);
341
292
  ```
342
293
 
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
294
+ ### 🛍️ Clean Architecture Pattern: Product Data Access Layer (`ProductsApi`)
353
295
 
354
296
  ```typescript
355
- // lib/blog.ts
297
+ // services/productsApi.ts
356
298
  import { podSdk } from '@/lib/pod';
357
299
 
358
- export interface BlogPostMeta {
359
- slug: string;
360
- title: string;
361
- summary: string;
362
- views: number;
363
- publishedAt: string;
300
+ // 1. Raw formatted product metadata (T)
301
+ export interface LaptopFormatted {
302
+ cpu: string;
303
+ ram: string;
304
+ storage: string;
305
+ displayPhoto: string; // Auto-transformed from hash to Podspace CDN URL
364
306
  }
365
307
 
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);
308
+ // 2. Computed / normalized presentation fields (P)
309
+ export interface LaptopNormalized {
310
+ finalPrice: number;
311
+ discountBadge: string;
377
312
  }
378
313
 
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;
314
+ // 3. Clean Domain Product Model
315
+ export interface ProductItemModel {
316
+ entityId: number;
317
+ name: string;
403
318
  price: number;
404
- inStock: boolean;
405
- category: string;
406
- entityId?: number;
319
+ finalPrice: number;
320
+ discountBadge: string;
321
+ photoUrl: string;
322
+ specs: {
323
+ cpu: string;
324
+ ram: string;
325
+ storage: string;
326
+ };
407
327
  }
408
328
 
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
329
  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
- }
330
+ private static readonly PRODUCT_TYPE = 'laptops';
425
331
 
426
332
  /**
427
- * Retrieves a single product by numerical entity ID
333
+ * Fetches published products with price & discount filters
428
334
  */
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
- }
335
+ public static async getProducts(params: {
336
+ fromPrice?: number;
337
+ toPrice?: number;
338
+ barcode?: string;
339
+ page?: number;
340
+ pageSize?: number;
341
+ } = {}): Promise<ProductItemModel[]> {
342
+ const { fromPrice, toPrice, barcode, page = 1, pageSize = 20 } = params;
343
+ const offset = (page - 1) * pageSize;
433
344
 
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
- });
345
+ const response = await podSdk.product.getProducts<LaptopFormatted, LaptopNormalized>(
346
+ {
347
+ productTypeUniqueId: this.PRODUCT_TYPE,
348
+ fromPrice,
349
+ toPrice,
350
+ barcode,
351
+ size: pageSize,
352
+ offset,
353
+ },
354
+ {
355
+ normalizer: (item) => {
356
+ const discount = item.discount || 0;
357
+ const finalPrice = item.price ? item.price * (1 - discount / 100) : 0;
358
+ return {
359
+ finalPrice,
360
+ discountBadge: discount > 0 ? `${discount}% OFF` : '',
361
+ };
362
+ },
363
+ revalidate: 3600, // Next.js ISR: cache for 1 hour
364
+ }
365
+ );
366
+
367
+ if (response.hasError || !Array.isArray(response.result)) {
368
+ return [];
369
+ }
370
+
371
+ return response.result.map((item) => ({
372
+ entityId: item.entityId,
373
+ name: item.name || '',
374
+ price: item.price || 0,
375
+ finalPrice: item.__normalized.finalPrice,
376
+ discountBadge: item.__normalized.discountBadge,
377
+ photoUrl: item.formatted.displayPhoto,
378
+ specs: {
379
+ cpu: item.formatted.cpu,
380
+ ram: item.formatted.ram,
381
+ storage: item.formatted.storage,
382
+ },
383
+ }));
442
384
  }
443
385
 
444
386
  /**
445
- * Updates an existing product
387
+ * Retrieves single product by entityId
446
388
  */
447
- public static async updateProduct(entityId: number, data: ProductData) {
448
- return productCrud.update(entityId, data);
389
+ public static async getProductById(entityId: number) {
390
+ const res = await podSdk.product.getProductByEntityId<LaptopFormatted, LaptopNormalized>({
391
+ entityId,
392
+ productTypeUniqueId: this.PRODUCT_TYPE,
393
+ });
394
+ return res.result?.[0] || null;
449
395
  }
450
396
 
451
397
  /**
452
- * Soft-archives a product without deleting history
398
+ * Look up product by barcode scanner
453
399
  */
454
- public static async archiveProduct(entityId: number, data: ProductData) {
455
- return productCrud.archive(entityId, data);
400
+ public static async getProductByBarcode(barcode: string) {
401
+ return podSdk.product.getProductByBarcode<LaptopFormatted>(barcode);
456
402
  }
457
403
 
458
404
  /**
459
- * Deletes a product permanently
405
+ * Adds and publishes a new product
460
406
  */
461
- public static async deleteProduct(entityId: number) {
462
- return productCrud.delete(entityId);
407
+ public static async createProduct(data: {
408
+ name: string;
409
+ price: number;
410
+ cpu: string;
411
+ ram: string;
412
+ storage: string;
413
+ photoHash: string;
414
+ }) {
415
+ return podSdk.product.addProduct({
416
+ productTypeUniqueId: this.PRODUCT_TYPE,
417
+ body: {
418
+ name: data.name,
419
+ enable: true,
420
+ fieldCode: ['cpu', 'ram', 'storage', 'displayPhoto', 'price'],
421
+ fieldValue: [data.cpu, data.ram, data.storage, data.photoHash, data.price],
422
+ },
423
+ });
463
424
  }
464
425
  }
465
426
  ```
466
427
 
467
- ---
468
-
469
- #### Step 2: Consume `ProductsApi` in Server Components (`app/products/page.tsx`)
470
-
471
- ```tsx
472
- // app/products/page.tsx (Server Component)
473
- import { ProductsApi } from '@/services/productsApi';
428
+ ### CMS Methods Reference
429
+
430
+ #### Content Methods (`podSdk.cms`)
431
+
432
+ | Method | HTTP | Path | Description |
433
+ | :--- | :--- | :--- | :--- |
434
+ | `getContent<T, P>(params, opts)` | `GET` | `/api/core/contents/enable` | Get published enabled contents with generic typing. |
435
+ | `getAllContents<T, P>(params, opts)` | `GET` | `/api/core/contents` | Get contents with managing credentials (`Access-Token`). |
436
+ | `getMyContents<T, P>(params, opts)` | `GET` | `/api/core/contents/my` | Get current user/client contents. |
437
+ | `getContentsByType<T, P>(typeId, params)` | `GET` | `/api/core/contents/{contentTypeUniqueId}` | Get contents by ContentType. |
438
+ | `getContentByEntityId<T, P>(params)` | `GET` | `/api/core/contents/enable/{entityId}` | Get single published content item. |
439
+ | `getContentByUniqueId<T, P>(uniqueId)` | `GET` | `/api/core/contents/{uniqueId}/byContentUniqueId/enable` | Get content by uniqueId. |
440
+ | `searchContent<T, P>(params)` | `GET` | `/api/core/contents/search` | Search published contents by keyword. |
441
+ | `addContent<T, P>(params)` | `POST` | `/api/core/contents/{contentTypeUniqueId}/add-publish` | Add and publish content item. |
442
+ | `createContent<T, P>(params)` | `POST` | `/api/core/contents/{contentTypeUniqueId}` | Create unpublished/draft content. |
443
+ | `editContent<T, P>(entityId, params)` | `POST` | `/api/core/contents/{typeId}/{entityId}/edit-publish` | Update and publish content item. |
444
+ | `updateContent<T, P>(entityId, params)` | `POST` | `/api/core/contents/{typeId}/{entityId}` | Update content item. |
445
+ | `patchContent<T, P>(entityId, params)` | `PATCH` | `/api/core/contents/{typeId}/{entityId}` | Partial update content. |
446
+ | `publishContent(typeId, entityId)` | `POST` | `/api/core/contents/{typeId}/publish/{entityId}` | Publish single content item. |
447
+ | `unpublishContent(typeId, entityId)` | `POST` | `/api/core/contents/{typeId}/unpublish/{entityId}` | Unpublish single content item. |
448
+ | `batchPublish(params)` | `PUT` | `/api/core/contents/{typeId}/publish` | Batch publish multiple contents. |
449
+ | `batchUnpublish(params)` | `PUT` | `/api/core/contents/{typeId}/unpublish` | Batch unpublish multiple contents. |
450
+ | `archiveContent(params)` | `POST` | `/api/core/contents/{typeId}/archive` | Archive content items. |
451
+ | `unarchiveContent(params)` | `POST` | `/api/core/contents/{typeId}/unarchive` | Unarchive content items. |
452
+ | `getArchivedContents(params)` | `GET` | `/api/core/contents/archive` | Get archived contents list. |
453
+ | `getDrafts(params)` | `GET` | `/api/core/contents/draft` | Get drafts list. |
454
+ | `createDraft(params)` | `POST` | `/api/core/contents/{typeId}/draft` | Create a draft. |
455
+ | `getComments(typeId, entityId)` | `GET` | `/api/core/contents/{typeId}/comments/{entityId}` | Get comments on content. |
456
+ | `getLikes(uniqueId, entityId)` | `GET` | `/api/core/contents/{uniqueId}/like/{entityId}` | Get likes of content item. |
457
+ | `getCategories(params)` | `GET` | `/api/core/tags/root/tree/enable` | Get category/tag tree. |
458
+ | `timelineSearch(query)` | `GET` | `/api/core/contents/ai/timeline-search/enable` | Advanced AI timeline search. |
459
+
460
+ #### Product Methods (`podSdk.product` / `podSdk.cms.products`)
461
+
462
+ | Method | HTTP | Path | Description |
463
+ | :--- | :--- | :--- | :--- |
464
+ | `getProducts<T, P>(params, opts)` | `GET` | `/api/core/products/enable` | Get enabled published products with price/discount filters. |
465
+ | `getAllProducts<T, P>(params, opts)` | `GET` | `/api/core/products` | Get products with provider credentials (`Access-Token`). |
466
+ | `getManageProducts<T, P>(params, opts)`| `GET` | `/api/core/products/manage` | Get products for manager view. |
467
+ | `getProductsByType<T, P>(typeId, params)`| `GET` | `/api/core/products/{productTypeUniqueId}` | Get products by ProductType. |
468
+ | `getProductByEntityId<T, P>(params)` | `GET` | `/api/core/products/enable/{entityId}` | Get single product by entityId. |
469
+ | `getProductByUniqueId<T, P>(uniqueId)` | `GET` | `/api/core/products/{uniqueId}/byProductUniqueId/enable` | Get product by uniqueId. |
470
+ | `getProductByBarcode<T, P>(barcode)` | `GET` | `/api/core/products/barcode/enable` | Lookup products matching a barcode. |
471
+ | `searchProducts<T, P>(params)` | `GET` | `/api/core/products/enable/search` | Search published products. |
472
+ | `addProduct<T, P>(params)` | `POST` | `/api/core/products/{productTypeUniqueId}/add-publish` | Create and publish new product. |
473
+ | `createProduct<T, P>(params)` | `POST` | `/api/core/products/{productTypeUniqueId}` | Create draft/unpublished product. |
474
+ | `editProduct<T, P>(entityId, params)` | `POST` | `/api/core/products/{typeId}/{entityId}/edit-publish` | Edit and publish existing product. |
475
+ | `updateProduct<T, P>(entityId, params)` | `POST` | `/api/core/products/{typeId}/{entityId}` | Update existing product. |
476
+ | `patchProduct<T, P>(entityId, params)` | `PATCH` | `/api/core/products/{typeId}/{entityId}` | Partial update product. |
477
+ | `publishProduct(typeId, entityId)` | `POST` | `/api/core/products/{typeId}/publish/{entityId}` | Publish product. |
478
+ | `unpublishProduct(typeId, entityId)` | `POST` | `/api/core/products/{typeId}/unpublish/{entityId}` | Unpublish product. |
479
+ | `batchPublish(params)` | `PUT` | `/api/core/products/publish` | Batch publish multiple products. |
480
+ | `batchUnpublish(params)` | `PUT` | `/api/core/products/unpublish` | Batch unpublish multiple products. |
481
+ | `archiveProduct(params)` | `POST` | `/api/core/products/archive` | Archive products. |
482
+ | `unarchiveProduct(params)` | `POST` | `/api/core/products/unarchive` | Unarchive products. |
483
+ | `getArchivedProducts(params)` | `GET` | `/api/core/products/archive` | Get archived products list. |
484
+ | `timelineSearch(query)` | `GET` | `/api/core/products/ai/timeline-search/enable` | AI timeline search on products. |
474
485
 
475
- export default async function ProductsPage() {
476
- const products = await ProductsApi.getProducts();
486
+ ---
477
487
 
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
- }
493
- ```
488
+ ## Deep Dive 3: CustomPost & Generic Typed Repositories
494
489
 
495
- ---
490
+ `CustomPost` allows persisting arbitrary JSON schemas on the POD Platform. `@rezamirzapour/pod-sdk` provides two complementary approaches:
496
491
 
497
- #### Step 3: Mutate via Server Actions (`app/actions/products.ts`)
492
+ 1. **Direct Service Method Calls with Generics (`searchTimelineByMetadata<T>`, `getCustomPost<T>`, `addCustomPost<T>`)**
493
+ 2. **High-Level Typed CRUD Repository (`CustomPostCrudService<DataType>`)**
498
494
 
499
495
  ```typescript
500
- // app/actions/products.ts
501
- 'use server';
496
+ // lib/blog.ts
497
+ import { podSdk } from '@/lib/pod';
502
498
 
503
- import { ProductsApi } from '@/services/productsApi';
504
- import { revalidatePath } from 'next/cache';
499
+ export interface BlogPostMeta {
500
+ slug: string;
501
+ title: string;
502
+ summary: string;
503
+ views: number;
504
+ }
505
505
 
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;
506
+ // Search posts with typed metadata query
507
+ export async function getPublishedPosts() {
508
+ const response = await podSdk.customPost.searchTimelineByMetadata<BlogPostMeta>({
509
+ entityName: 'blog_post',
510
+ metadata: { views: 100 },
511
+ size: 20,
512
+ offset: 0,
513
+ });
514
+
515
+ return response.result.map((r) => r.item.metadata);
510
516
  }
511
517
  ```
512
518
 
@@ -514,7 +520,7 @@ export async function addProduct(data: { title: string; sku: string; price: numb
514
520
 
515
521
  ## Other POD Microservices
516
522
 
517
- ### 3. POD SSO (OTP Handshake with Web Crypto RSA Signature)
523
+ ### 4. POD SSO (OTP Handshake with Web Crypto RSA Signature)
518
524
 
519
525
  ```typescript
520
526
  // app/actions/auth.ts
@@ -524,7 +530,6 @@ import { podSdk } from '@/lib/pod';
524
530
 
525
531
  // Step 1: Handshake and send OTP
526
532
  export async function sendOtp(phoneNumber: string, clientIp: string) {
527
- // Handshake to obtain keyId
528
533
  const handshake = await podSdk.sso.handshake(`web-${Date.now()}`, clientIp);
529
534
  if (handshake.hasError || !handshake.result?.keyId) {
530
535
  throw new Error(handshake.message || 'SSO Handshake failed');
@@ -545,7 +550,6 @@ export async function verifyOtp(authorization: string, phoneNumber: string, code
545
550
  throw new Error(verify.message || 'OTP verification failed');
546
551
  }
547
552
 
548
- // Exchange authorization code for JWT tokens
549
553
  const tokens = await podSdk.sso.generateToken(verify.result.code);
550
554
  return tokens.result; // { access_token, refresh_token, expires_in }
551
555
  }
@@ -559,7 +563,7 @@ export async function getProfile(accessToken: string) {
559
563
 
560
564
  ---
561
565
 
562
- ### 4. Podspace (File Uploads & CDN URLs)
566
+ ### 5. Podspace (File Uploads & CDN URLs)
563
567
 
564
568
  ```typescript
565
569
  // app/actions/upload.ts
@@ -580,7 +584,7 @@ export async function uploadFile(formData: FormData) {
580
584
 
581
585
  ---
582
586
 
583
- ### 5. Notification (SMS Delivery)
587
+ ### 6. Notification (SMS Delivery)
584
588
 
585
589
  ```typescript
586
590
  // app/actions/notify.ts
@@ -598,7 +602,7 @@ export async function sendSms(phoneNumber: string, text: string) {
598
602
 
599
603
  ---
600
604
 
601
- ### 6. Social (Comments & Likes)
605
+ ### 7. Social (Comments & Likes)
602
606
 
603
607
  ```typescript
604
608
  // app/actions/social.ts
@@ -620,7 +624,7 @@ export async function likePost(postId: number) {
620
624
 
621
625
  ---
622
626
 
623
- ### 7. IUMS (Identity & University Management Service)
627
+ ### 8. IUMS (Identity & University Management Service)
624
628
 
625
629
  ```typescript
626
630
  // app/actions/iums.ts