@rezamirzapour/pod-sdk 1.0.3 → 1.0.5

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,656 +1,742 @@
1
- # @rezamirzapour/pod-sdk
2
-
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
-
5
- [![npm version](https://img.shields.io/npm/v/@rezamirzapour/pod-sdk.svg)](https://www.npmjs.com/package/@rezamirzapour/pod-sdk)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue?logo=typescript)](https://www.typescriptlang.org/)
8
-
9
- ---
10
-
11
- ## Features
12
-
13
- - 🌐 **Unified Multi-Service Architecture**: Single entry-point managing official POD microservices with official endpoints and configurations.
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 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.
18
- - 📦 **Generic CRUD Repository for CustomPost**: Instantiate typed data repositories on POD CustomPost in 2 lines with `create`, `getById`, `getAll`, `createAndBindEntityId`, and `archive`.
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).
20
- - 🚀 **Next.js App Router Ready**: Seamless support for Server Components, Server Actions, Route Handlers, and Client Components.
21
- - 🌳 **Treeshakeable & Dual ESM/CJS**: Ships clean ES modules (`.mjs`) and CommonJS (`.js`) with 100% complete TypeScript declarations (`.d.ts`).
22
-
23
- ---
24
-
25
- ## Microservices Included
26
-
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. |
38
-
39
- ---
40
-
41
- ## Installation
42
-
43
- ```bash
44
- npm install @rezamirzapour/pod-sdk @rezamirzapour/http @rezamirzapour/logger
45
- ```
46
-
47
- ---
48
-
49
- ## Quickstart
50
-
51
- ### 1. Initialize the Client
52
-
53
- ```typescript
54
- // lib/pod.ts
55
- import { createPodSdk } from '@rezamirzapour/pod-sdk';
56
-
57
- export const podSdk = createPodSdk({
58
- apiToken: process.env.POD_API_TOKEN,
59
- clientId: process.env.POD_CLIENT_ID,
60
- clientSecret: process.env.POD_CLIENT_SECRET,
61
- privateKeyPem: process.env.POD_PRIVATE_KEY_PEM, // RSA Private Key for SSO OTP signing
62
- revalidate: 3600, // Default ISR revalidation in seconds
63
- urls: {
64
- // Optional overrides (defaults to official POD endpoints)
65
- accounts: 'https://accounts.pod.ir',
66
- apiPod: 'https://api.pod.ir',
67
- cms: 'https://cms.pod.ir',
68
- podspace: 'https://podspace.pod.ir',
69
- podform: 'https://podform.pod.ir',
70
- notification: 'https://notification.pod.ir',
71
- iums: 'https://iums.pod.ir',
72
- },
73
- });
74
- ```
75
-
76
- ---
77
-
78
- ## Deep Dive 1: CMS Content & Data Formatting
79
-
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`).
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
-
85
- ### 🏗️ Clean Architecture Pattern: Dedicated Data Access Layer (`NewsApi`)
86
-
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`).
88
-
89
- #### Step 1: Create the Data Access Layer (`services/newsApi.ts`)
90
-
91
- ```typescript
92
- // services/newsApi.ts
93
- import { podSdk } from '@/lib/pod';
94
-
95
- // 1. Define the raw CMS Formatted Metadata schema (T)
96
- export interface NewsArticleFormatted {
97
- title: string;
98
- coverImage: string; // Auto-transformed from hash to full Podspace CDN URL!
99
- lead: string;
100
- body: string;
101
- author?: string;
102
- }
103
-
104
- // 2. Define computed / normalized domain attributes (P)
105
- export interface NewsArticleNormalized {
106
- summary: string;
107
- readTime: number;
108
- }
109
-
110
- // 3. Define the Clean Domain Model consumed by your React Components
111
- export interface NewsItem {
112
- id: number;
113
- entityId: number;
114
- title: string;
115
- coverImage: string;
116
- summary: string;
117
- body: string;
118
- readTime: number;
119
- publishedAt?: number;
120
- }
121
-
122
- export interface GetNewsListParams {
123
- page?: number;
124
- pageSize?: number;
125
- tagTrees?: string[];
126
- }
127
-
128
- export class NewsApi {
129
- private static readonly CONTENT_TYPE = 'company_news';
130
-
131
- /**
132
- * Fetches published news articles with Next.js App Router ISR Caching
133
- */
134
- public static async getNewsList({
135
- page = 1,
136
- pageSize = 10,
137
- tagTrees,
138
- }: GetNewsListParams = {}): Promise<{ items: NewsItem[]; total: number }> {
139
- const offset = (page - 1) * pageSize;
140
-
141
- // Call getContent with exact generics: <NewsArticleFormatted, NewsArticleNormalized>
142
- const response = await podSdk.cms.getContent<NewsArticleFormatted, NewsArticleNormalized>(
143
- {
144
- contentTypeUniqueId: this.CONTENT_TYPE,
145
- size: pageSize,
146
- offset,
147
- tagTrees,
148
- },
149
- {
150
- // Custom normalizer executed during data formatting
151
- normalizer: (item) => ({
152
- summary: item.formatted.lead || item.formatted.title,
153
- readTime: Math.max(1, Math.ceil((item.formatted.body?.length || 0) / 500)),
154
- }),
155
- revalidate: 1800, // Next.js ISR: cache for 30 minutes
156
- }
157
- );
158
-
159
- if (response.hasError || !Array.isArray(response.result)) {
160
- return { items: [], total: 0 };
161
- }
162
-
163
- const items: NewsItem[] = response.result.map((item) => ({
164
- id: item.id,
165
- entityId: item.entityId,
166
- title: item.formatted.title,
167
- coverImage: item.formatted.coverImage,
168
- summary: item.__normalized.summary,
169
- body: item.formatted.body,
170
- readTime: item.__normalized.readTime,
171
- publishedAt: item.timestamp,
172
- }));
173
-
174
- return {
175
- items,
176
- total: response.count || items.length,
177
- };
178
- }
179
-
180
- /**
181
- * Fetches a single news article by its entity ID
182
- */
183
- public static async getNewsById(entityId: number): Promise<NewsItem | null> {
184
- const response = await podSdk.cms.getContentByEntityId<NewsArticleFormatted, NewsArticleNormalized>({
185
- entityId,
186
- contentTypeUniqueId: this.CONTENT_TYPE,
187
- });
188
-
189
- const item = response.result?.[0];
190
- if (!item) return null;
191
-
192
- return {
193
- id: item.id,
194
- entityId: item.entityId,
195
- title: item.formatted.title,
196
- coverImage: item.formatted.coverImage,
197
- summary: item.formatted.lead || item.formatted.title,
198
- body: item.formatted.body,
199
- readTime: Math.max(1, Math.ceil((item.formatted.body?.length || 0) / 500)),
200
- publishedAt: item.timestamp,
201
- };
202
- }
203
-
204
- /**
205
- * Publishes a new article to CMS
206
- */
207
- public static async publishNews(data: {
208
- title: string;
209
- lead: string;
210
- body: string;
211
- imageHash: string;
212
- }) {
213
- return podSdk.cms.addContent({
214
- contentTypeUniqueId: this.CONTENT_TYPE,
215
- body: {
216
- name: data.title,
217
- fieldCode: ['title', 'lead', 'body', 'coverImage'],
218
- fieldValue: [data.title, data.lead, data.body, data.imageHash],
219
- canComment: true,
220
- canLike: true,
221
- enable: true,
222
- isSearchable: true,
223
- },
224
- });
225
- }
226
- }
227
- ```
228
-
229
- #### Step 2: Consume `NewsApi` in Next.js Server Components (`app/news/page.tsx`)
230
-
231
- ```tsx
232
- // app/news/page.tsx (Server Component)
233
- import { NewsApi } from '@/services/newsApi';
234
- import Link from 'next/link';
235
-
236
- interface NewsPageProps {
237
- searchParams: { page?: string };
238
- }
239
-
240
- export default async function NewsPage({ searchParams }: NewsPageProps) {
241
- const page = Number(searchParams.page) || 1;
242
- const { items, total } = await NewsApi.getNewsList({ page, pageSize: 12 });
243
-
244
- return (
245
- <main className="container mx-auto py-8 px-4">
246
- <h1 className="text-3xl font-bold mb-6">Company News</h1>
247
-
248
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
249
- {items.map((news) => (
250
- <article key={news.entityId} className="border rounded-xl overflow-hidden shadow-sm hover:shadow-md transition">
251
- {news.coverImage && (
252
- <img
253
- src={news.coverImage}
254
- alt={news.title}
255
- className="w-full h-48 object-cover"
256
- />
257
- )}
258
- <div className="p-4">
259
- <h2 className="text-xl font-semibold mb-2">
260
- <Link href={`/news/${news.entityId}`}>{news.title}</Link>
261
- </h2>
262
- <p className="text-gray-600 text-sm line-clamp-2">{news.summary}</p>
263
- <div className="mt-4 flex items-center justify-between text-xs text-gray-400">
264
- <span>{news.readTime} min read</span>
265
- <Link href={`/news/${news.entityId}`} className="text-blue-600 font-medium hover:underline">
266
- Read more
267
- </Link>
268
- </div>
269
- </div>
270
- </article>
271
- ))}
272
- </div>
273
- </main>
274
- );
275
- }
276
- ```
277
-
278
- ---
279
-
280
- ## Deep Dive 2: CMS Product Module (RAD API Swagger)
281
-
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.
283
-
284
- ### Accessing Products
285
-
286
- You can access product methods either via `podSdk.product` or `podSdk.cms.products`:
287
-
288
- ```typescript
289
- // Both point to the same CmsProductService instance
290
- podSdk.product.getProducts(...);
291
- podSdk.cms.products.getProducts(...);
292
- ```
293
-
294
- ### 🛍️ Clean Architecture Pattern: Product Data Access Layer (`ProductsApi`)
295
-
296
- ```typescript
297
- // services/productsApi.ts
298
- import { podSdk } from '@/lib/pod';
299
-
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
306
- }
307
-
308
- // 2. Computed / normalized presentation fields (P)
309
- export interface LaptopNormalized {
310
- finalPrice: number;
311
- discountBadge: string;
312
- }
313
-
314
- // 3. Clean Domain Product Model
315
- export interface ProductItemModel {
316
- entityId: number;
317
- name: string;
318
- price: number;
319
- finalPrice: number;
320
- discountBadge: string;
321
- photoUrl: string;
322
- specs: {
323
- cpu: string;
324
- ram: string;
325
- storage: string;
326
- };
327
- }
328
-
329
- export class ProductsApi {
330
- private static readonly PRODUCT_TYPE = 'laptops';
331
-
332
- /**
333
- * Fetches published products with price & discount filters
334
- */
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;
344
-
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
- }));
384
- }
385
-
386
- /**
387
- * Retrieves single product by entityId
388
- */
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;
395
- }
396
-
397
- /**
398
- * Look up product by barcode scanner
399
- */
400
- public static async getProductByBarcode(barcode: string) {
401
- return podSdk.product.getProductByBarcode<LaptopFormatted>(barcode);
402
- }
403
-
404
- /**
405
- * Adds and publishes a new product
406
- */
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
- });
424
- }
425
- }
426
- ```
427
-
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. |
485
-
486
- ---
487
-
488
- ## Deep Dive 3: CustomPost & Generic Typed Repositories
489
-
490
- `CustomPost` allows persisting arbitrary JSON schemas on the POD Platform. `@rezamirzapour/pod-sdk` provides two complementary approaches:
491
-
492
- 1. **Direct Service Method Calls with Generics (`searchTimelineByMetadata<T>`, `getCustomPost<T>`, `addCustomPost<T>`)**
493
- 2. **High-Level Typed CRUD Repository (`CustomPostCrudService<DataType>`)**
494
-
495
- ```typescript
496
- // lib/blog.ts
497
- import { podSdk } from '@/lib/pod';
498
-
499
- export interface BlogPostMeta {
500
- slug: string;
501
- title: string;
502
- summary: string;
503
- views: number;
504
- }
505
-
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);
516
- }
517
- ```
518
-
519
- ---
520
-
521
- ## Other POD Microservices
522
-
523
- ### 4. POD SSO (OTP Handshake with Web Crypto RSA Signature)
524
-
525
- ```typescript
526
- // app/actions/auth.ts
527
- 'use server';
528
-
529
- import { podSdk } from '@/lib/pod';
530
-
531
- // Step 1: Handshake and send OTP
532
- export async function sendOtp(phoneNumber: string, clientIp: string) {
533
- const handshake = await podSdk.sso.handshake(`web-${Date.now()}`, clientIp);
534
- if (handshake.hasError || !handshake.result?.keyId) {
535
- throw new Error(handshake.message || 'SSO Handshake failed');
536
- }
537
-
538
- // Automatic RSA-SHA256 signature using Web Crypto API
539
- const otpRes = await podSdk.sso.sendOtpCode(handshake.result.keyId, phoneNumber);
540
- return {
541
- success: !otpRes.hasError,
542
- authorization: otpRes.authorization,
543
- };
544
- }
545
-
546
- // Step 2: Verify OTP code & exchange for tokens
547
- export async function verifyOtp(authorization: string, phoneNumber: string, code: string) {
548
- const verify = await podSdk.sso.verifyOtpCode(authorization, phoneNumber, code);
549
- if (verify.hasError || !verify.result?.code) {
550
- throw new Error(verify.message || 'OTP verification failed');
551
- }
552
-
553
- const tokens = await podSdk.sso.generateToken(verify.result.code);
554
- return tokens.result; // { access_token, refresh_token, expires_in }
555
- }
556
-
557
- // Step 3: Fetch profile with access token
558
- export async function getProfile(accessToken: string) {
559
- const profile = await podSdk.sso.getUserProfile(accessToken);
560
- return profile.result;
561
- }
562
- ```
563
-
564
- ---
565
-
566
- ### 5. Podspace (File Uploads & CDN URLs)
567
-
568
- ```typescript
569
- // app/actions/upload.ts
570
- 'use server';
571
-
572
- import { podSdk } from '@/lib/pod';
573
-
574
- export async function uploadFile(formData: FormData) {
575
- const res = await podSdk.podspace.uploadFile(formData, '/uploads', true);
576
- if (res.hasError) {
577
- throw new Error(res.message || 'Upload failed');
578
- }
579
-
580
- const publicUrl = podSdk.podspace.getFileUrl(res.result.hash, true);
581
- return { url: publicUrl, hash: res.result.hash };
582
- }
583
- ```
584
-
585
- ---
586
-
587
- ### 6. Notification (SMS Delivery)
588
-
589
- ```typescript
590
- // app/actions/notify.ts
591
- 'use server';
592
-
593
- import { podSdk } from '@/lib/pod';
594
-
595
- export async function sendSms(phoneNumber: string, text: string) {
596
- return podSdk.notification.sendSms({
597
- receptor: phoneNumber,
598
- message: text,
599
- });
600
- }
601
- ```
602
-
603
- ---
604
-
605
- ### 7. Social (Comments & Likes)
606
-
607
- ```typescript
608
- // app/actions/social.ts
609
- 'use server';
610
-
611
- import { podSdk } from '@/lib/pod';
612
-
613
- export async function addComment(postId: number, content: string) {
614
- return podSdk.social.addComment({
615
- postId,
616
- text: content,
617
- });
618
- }
619
-
620
- export async function likePost(postId: number) {
621
- return podSdk.social.likePost(postId);
622
- }
623
- ```
624
-
625
- ---
626
-
627
- ### 8. IUMS (Identity & University Management Service)
628
-
629
- ```typescript
630
- // app/actions/iums.ts
631
- 'use server';
632
-
633
- import { podSdk } from '@/lib/pod';
634
-
635
- export async function getStudentByNationalCode(nationalCode: string) {
636
- return podSdk.iums.getStudentInformationByNationalCode(nationalCode);
637
- }
638
- ```
639
-
640
- ---
641
-
642
- ## Environment Variables Reference
643
-
644
- ```env
645
- # .env.local
646
- POD_API_TOKEN=your_api_token_here
647
- POD_CLIENT_ID=your_client_id_here
648
- POD_CLIENT_SECRET=your_client_secret_here
649
- POD_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
650
- ```
651
-
652
- ---
653
-
654
- ## License
655
-
656
- MIT © [Reza](https://github.com/rezamirzapour2)
1
+ # @rezamirzapour/pod-sdk
2
+
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
+
5
+ [![npm version](https://img.shields.io/npm/v/@rezamirzapour/pod-sdk.svg)](https://www.npmjs.com/package/@rezamirzapour/pod-sdk)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue?logo=typescript)](https://www.typescriptlang.org/)
8
+
9
+ ---
10
+
11
+ ## Features
12
+
13
+ - 🌐 **Unified Multi-Service Architecture**: Single entry-point managing official POD microservices with official endpoints and configurations.
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 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.
18
+ - 📦 **Generic CRUD Repository for CustomPost**: Instantiate typed data repositories on POD CustomPost in 2 lines with `create`, `getById`, `getAll`, `createAndBindEntityId`, and `archive`.
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).
20
+ - 🚀 **Next.js App Router Ready**: Seamless support for Server Components, Server Actions, Route Handlers, and Client Components.
21
+ - 🌳 **Treeshakeable & Dual ESM/CJS**: Ships clean ES modules (`.mjs`) and CommonJS (`.js`) with 100% complete TypeScript declarations (`.d.ts`).
22
+
23
+ ---
24
+
25
+ ## Microservices Included
26
+
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. |
39
+
40
+ ---
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ npm install @rezamirzapour/pod-sdk @rezamirzapour/http @rezamirzapour/logger
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Environment Variables & URL Configuration
51
+
52
+ The SDK natively discovers official POD Platform URLs from your Next.js / Node.js `.env` configuration, or falls back to production defaults automatically:
53
+
54
+ ```env
55
+ # POD Platform Production URLs
56
+ NEXT_PUBLIC_ACCOUNTS_URL=https://accounts.pod.ir
57
+ NEXT_PUBLIC_API_POD_URL=https://api.pod.ir
58
+ NEXT_PUBLIC_API_POD_SANDBOX_URL=https://api.sandpod.ir
59
+ NEXT_PUBLIC_CMS_URL=https://api.pod.ir/srv/cms-server
60
+ NEXT_PUBLIC_IUMS_URL=https://indra.khatam.ac.ir/srv
61
+ NEXT_PUBLIC_NOTIFICATION_URL=https://api.pod.ir/srv/notification
62
+ NEXT_PUBLIC_PODREPORT_URL=https://reporting.pod.ir
63
+ NEXT_PUBLIC_PODSPACE_URL=https://podspace.pod.ir
64
+
65
+ # Sandbox Endpoints (Development & Staging)
66
+ NEXT_PUBLIC_SANDBOX_CMS_URL=http://api.sandpod.ir/srv/cms-sandbox
67
+ NEXT_PUBLIC_SANDBOX_CMS_CLIENT_URL=https://api.sandpod.ir/srv/cms-sandbox
68
+ NEXT_PUBLIC_SANDBOX_PODSPACE_URL=http://podspace.sandpod.ir
69
+ NEXT_PUBLIC_SANDBOX_STREAM_URL=https://sandbox-offline-stream.sandpod.ir
70
+ ```
71
+
72
+ > [!TIP]
73
+ > If you have defined these `NEXT_PUBLIC_*` variables in your `.env` or `.env.local`, `@rezamirzapour/pod-sdk` will automatically detect and apply them without any manual URL mapping.
74
+
75
+ ---
76
+
77
+ ## Quickstart
78
+
79
+ ### 1. Initialize the Client
80
+
81
+ ```typescript
82
+ // lib/pod.ts
83
+ import { createPodSdk, DEFAULT_POD_URLS, SANDBOX_POD_URLS } from '@rezamirzapour/pod-sdk';
84
+
85
+ export const podSdk = createPodSdk({
86
+ apiToken: process.env.POD_API_TOKEN,
87
+ clientId: process.env.POD_CLIENT_ID,
88
+ clientSecret: process.env.POD_CLIENT_SECRET,
89
+ privateKeyPem: process.env.POD_PRIVATE_KEY_PEM, // RSA Private Key for SSO OTP signing
90
+ revalidate: 3600, // Default ISR revalidation in seconds
91
+ // sandbox: true, // Optional: automatically routes to POD Sandbox endpoints
92
+ urls: {
93
+ // Optional programmatic overrides (defaults to official POD endpoints & env vars)
94
+ accounts: 'https://accounts.pod.ir',
95
+ apiPod: 'https://api.pod.ir',
96
+ cms: 'https://api.pod.ir/srv/cms-server',
97
+ iums: 'https://indra.khatam.ac.ir/srv',
98
+ notification: 'https://api.pod.ir/srv/notification',
99
+ podreport: 'https://reporting.pod.ir',
100
+ podspace: 'https://podspace.pod.ir',
101
+ podform: 'https://podform.pod.ir',
102
+ },
103
+ });
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Deep Dive 1: CMS Content & Data Formatting
109
+
110
+ 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`).
111
+
112
+ > [!NOTE]
113
+ > `getContent2` has been removed. For authenticated/management content retrieval (using `Access-Token`), call `podSdk.cms.getAllContents()` or pass headers to `getContent()`.
114
+
115
+ ### 🏗️ Clean Architecture Pattern: Dedicated Data Access Layer (`NewsApi`)
116
+
117
+ 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`).
118
+
119
+ #### Step 1: Create the Data Access Layer (`services/newsApi.ts`)
120
+
121
+ ```typescript
122
+ // services/newsApi.ts
123
+ import { podSdk } from '@/lib/pod';
124
+
125
+ // 1. Define the raw CMS Formatted Metadata schema (T)
126
+ export interface NewsArticleFormatted {
127
+ title: string;
128
+ coverImage: string; // Auto-transformed from hash to full Podspace CDN URL!
129
+ lead: string;
130
+ body: string;
131
+ author?: string;
132
+ }
133
+
134
+ // 2. Define computed / normalized domain attributes (P)
135
+ export interface NewsArticleNormalized {
136
+ summary: string;
137
+ readTime: number;
138
+ }
139
+
140
+ // 3. Define the Clean Domain Model consumed by your React Components
141
+ export interface NewsItem {
142
+ id: number;
143
+ entityId: number;
144
+ title: string;
145
+ coverImage: string;
146
+ summary: string;
147
+ body: string;
148
+ readTime: number;
149
+ publishedAt?: number;
150
+ }
151
+
152
+ export interface GetNewsListParams {
153
+ page?: number;
154
+ pageSize?: number;
155
+ tagTrees?: string[];
156
+ }
157
+
158
+ export class NewsApi {
159
+ private static readonly CONTENT_TYPE = 'company_news';
160
+
161
+ /**
162
+ * Fetches published news articles with Next.js App Router ISR Caching
163
+ */
164
+ public static async getNewsList({
165
+ page = 1,
166
+ pageSize = 10,
167
+ tagTrees,
168
+ }: GetNewsListParams = {}): Promise<{ items: NewsItem[]; total: number }> {
169
+ const offset = (page - 1) * pageSize;
170
+
171
+ // Call getContent with exact generics: <NewsArticleFormatted, NewsArticleNormalized>
172
+ const response = await podSdk.cms.getContent<NewsArticleFormatted, NewsArticleNormalized>(
173
+ {
174
+ contentTypeUniqueId: this.CONTENT_TYPE,
175
+ size: pageSize,
176
+ offset,
177
+ tagTrees,
178
+ },
179
+ {
180
+ // Custom normalizer executed during data formatting
181
+ normalizer: (item) => ({
182
+ summary: item.formatted.lead || item.formatted.title,
183
+ readTime: Math.max(1, Math.ceil((item.formatted.body?.length || 0) / 500)),
184
+ }),
185
+ revalidate: 1800, // Next.js ISR: cache for 30 minutes
186
+ }
187
+ );
188
+
189
+ if (response.hasError || !Array.isArray(response.result)) {
190
+ return { items: [], total: 0 };
191
+ }
192
+
193
+ const items: NewsItem[] = response.result.map((item) => ({
194
+ id: item.id,
195
+ entityId: item.entityId,
196
+ title: item.formatted.title,
197
+ coverImage: item.formatted.coverImage,
198
+ summary: item.__normalized.summary,
199
+ body: item.formatted.body,
200
+ readTime: item.__normalized.readTime,
201
+ publishedAt: item.timestamp,
202
+ }));
203
+
204
+ return {
205
+ items,
206
+ total: response.count || items.length,
207
+ };
208
+ }
209
+
210
+ /**
211
+ * Fetches a single news article by its entity ID
212
+ */
213
+ public static async getNewsById(entityId: number): Promise<NewsItem | null> {
214
+ const response = await podSdk.cms.getContentByEntityId<NewsArticleFormatted, NewsArticleNormalized>({
215
+ entityId,
216
+ contentTypeUniqueId: this.CONTENT_TYPE,
217
+ });
218
+
219
+ const item = response.result?.[0];
220
+ if (!item) return null;
221
+
222
+ return {
223
+ id: item.id,
224
+ entityId: item.entityId,
225
+ title: item.formatted.title,
226
+ coverImage: item.formatted.coverImage,
227
+ summary: item.formatted.lead || item.formatted.title,
228
+ body: item.formatted.body,
229
+ readTime: Math.max(1, Math.ceil((item.formatted.body?.length || 0) / 500)),
230
+ publishedAt: item.timestamp,
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Publishes a new article to CMS
236
+ */
237
+ public static async publishNews(data: {
238
+ title: string;
239
+ lead: string;
240
+ body: string;
241
+ imageHash: string;
242
+ }) {
243
+ return podSdk.cms.addContent({
244
+ contentTypeUniqueId: this.CONTENT_TYPE,
245
+ body: {
246
+ name: data.title,
247
+ fieldCode: ['title', 'lead', 'body', 'coverImage'],
248
+ fieldValue: [data.title, data.lead, data.body, data.imageHash],
249
+ canComment: true,
250
+ canLike: true,
251
+ enable: true,
252
+ isSearchable: true,
253
+ },
254
+ });
255
+ }
256
+ }
257
+ ```
258
+
259
+ #### Step 2: Consume `NewsApi` in Next.js Server Components (`app/news/page.tsx`)
260
+
261
+ ```tsx
262
+ // app/news/page.tsx (Server Component)
263
+ import { NewsApi } from '@/services/newsApi';
264
+ import Link from 'next/link';
265
+
266
+ interface NewsPageProps {
267
+ searchParams: { page?: string };
268
+ }
269
+
270
+ export default async function NewsPage({ searchParams }: NewsPageProps) {
271
+ const page = Number(searchParams.page) || 1;
272
+ const { items, total } = await NewsApi.getNewsList({ page, pageSize: 12 });
273
+
274
+ return (
275
+ <main className="container mx-auto py-8 px-4">
276
+ <h1 className="text-3xl font-bold mb-6">Company News</h1>
277
+
278
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
279
+ {items.map((news) => (
280
+ <article key={news.entityId} className="border rounded-xl overflow-hidden shadow-sm hover:shadow-md transition">
281
+ {news.coverImage && (
282
+ <img
283
+ src={news.coverImage}
284
+ alt={news.title}
285
+ className="w-full h-48 object-cover"
286
+ />
287
+ )}
288
+ <div className="p-4">
289
+ <h2 className="text-xl font-semibold mb-2">
290
+ <Link href={`/news/${news.entityId}`}>{news.title}</Link>
291
+ </h2>
292
+ <p className="text-gray-600 text-sm line-clamp-2">{news.summary}</p>
293
+ <div className="mt-4 flex items-center justify-between text-xs text-gray-400">
294
+ <span>{news.readTime} min read</span>
295
+ <Link href={`/news/${news.entityId}`} className="text-blue-600 font-medium hover:underline">
296
+ Read more →
297
+ </Link>
298
+ </div>
299
+ </div>
300
+ </article>
301
+ ))}
302
+ </div>
303
+ </main>
304
+ );
305
+ }
306
+ ```
307
+
308
+ ---
309
+
310
+ ## Deep Dive 2: CMS Product Module (RAD API Swagger)
311
+
312
+ 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
+
314
+ ### Accessing Products
315
+
316
+ You can access product methods either via `podSdk.product` or `podSdk.cms.products`:
317
+
318
+ ```typescript
319
+ // Both point to the same CmsProductService instance
320
+ podSdk.product.getProducts(...);
321
+ podSdk.cms.products.getProducts(...);
322
+ ```
323
+
324
+ ### 🛍️ Clean Architecture Pattern: Product Data Access Layer (`ProductsApi`)
325
+
326
+ ```typescript
327
+ // services/productsApi.ts
328
+ import { podSdk } from '@/lib/pod';
329
+
330
+ // 1. Raw formatted product metadata (T)
331
+ export interface LaptopFormatted {
332
+ cpu: string;
333
+ ram: string;
334
+ storage: string;
335
+ displayPhoto: string; // Auto-transformed from hash to Podspace CDN URL
336
+ }
337
+
338
+ // 2. Computed / normalized presentation fields (P)
339
+ export interface LaptopNormalized {
340
+ finalPrice: number;
341
+ discountBadge: string;
342
+ }
343
+
344
+ // 3. Clean Domain Product Model
345
+ export interface ProductItemModel {
346
+ entityId: number;
347
+ name: string;
348
+ price: number;
349
+ finalPrice: number;
350
+ discountBadge: string;
351
+ photoUrl: string;
352
+ specs: {
353
+ cpu: string;
354
+ ram: string;
355
+ storage: string;
356
+ };
357
+ }
358
+
359
+ export class ProductsApi {
360
+ private static readonly PRODUCT_TYPE = 'laptops';
361
+
362
+ /**
363
+ * Fetches published products with price & discount filters
364
+ */
365
+ public static async getProducts(params: {
366
+ fromPrice?: number;
367
+ toPrice?: number;
368
+ barcode?: string;
369
+ page?: number;
370
+ pageSize?: number;
371
+ } = {}): Promise<ProductItemModel[]> {
372
+ const { fromPrice, toPrice, barcode, page = 1, pageSize = 20 } = params;
373
+ const offset = (page - 1) * pageSize;
374
+
375
+ const response = await podSdk.product.getProducts<LaptopFormatted, LaptopNormalized>(
376
+ {
377
+ productTypeUniqueId: this.PRODUCT_TYPE,
378
+ fromPrice,
379
+ toPrice,
380
+ barcode,
381
+ size: pageSize,
382
+ offset,
383
+ },
384
+ {
385
+ normalizer: (item) => {
386
+ const discount = item.discount || 0;
387
+ const finalPrice = item.price ? item.price * (1 - discount / 100) : 0;
388
+ return {
389
+ finalPrice,
390
+ discountBadge: discount > 0 ? `${discount}% OFF` : '',
391
+ };
392
+ },
393
+ revalidate: 3600, // Next.js ISR: cache for 1 hour
394
+ }
395
+ );
396
+
397
+ if (response.hasError || !Array.isArray(response.result)) {
398
+ return [];
399
+ }
400
+
401
+ return response.result.map((item) => ({
402
+ entityId: item.entityId,
403
+ name: item.name || '',
404
+ price: item.price || 0,
405
+ finalPrice: item.__normalized.finalPrice,
406
+ discountBadge: item.__normalized.discountBadge,
407
+ photoUrl: item.formatted.displayPhoto,
408
+ specs: {
409
+ cpu: item.formatted.cpu,
410
+ ram: item.formatted.ram,
411
+ storage: item.formatted.storage,
412
+ },
413
+ }));
414
+ }
415
+
416
+ /**
417
+ * Retrieves single product by entityId
418
+ */
419
+ public static async getProductById(entityId: number) {
420
+ const res = await podSdk.product.getProductByEntityId<LaptopFormatted, LaptopNormalized>({
421
+ entityId,
422
+ productTypeUniqueId: this.PRODUCT_TYPE,
423
+ });
424
+ return res.result?.[0] || null;
425
+ }
426
+
427
+ /**
428
+ * Look up product by barcode scanner
429
+ */
430
+ public static async getProductByBarcode(barcode: string) {
431
+ return podSdk.product.getProductByBarcode<LaptopFormatted>(barcode);
432
+ }
433
+
434
+ /**
435
+ * Adds and publishes a new product
436
+ */
437
+ public static async createProduct(data: {
438
+ name: string;
439
+ price: number;
440
+ cpu: string;
441
+ ram: string;
442
+ storage: string;
443
+ photoHash: string;
444
+ }) {
445
+ return podSdk.product.addProduct({
446
+ productTypeUniqueId: this.PRODUCT_TYPE,
447
+ body: {
448
+ name: data.name,
449
+ enable: true,
450
+ fieldCode: ['cpu', 'ram', 'storage', 'displayPhoto', 'price'],
451
+ fieldValue: [data.cpu, data.ram, data.storage, data.photoHash, data.price],
452
+ },
453
+ });
454
+ }
455
+ }
456
+ ```
457
+
458
+ ### CMS Methods Reference
459
+
460
+ #### Content Methods (`podSdk.cms`)
461
+
462
+ | Method | HTTP | Path | Description |
463
+ | :--- | :--- | :--- | :--- |
464
+ | `getContent<T, P>(params, opts)` | `GET` | `/api/core/contents/enable` | Get published enabled contents with generic typing. |
465
+ | `getAllContents<T, P>(params, opts)` | `GET` | `/api/core/contents` | Get contents with managing credentials (`Access-Token`). |
466
+ | `getMyContents<T, P>(params, opts)` | `GET` | `/api/core/contents/my` | Get current user/client contents. |
467
+ | `getContentsByType<T, P>(typeId, params)` | `GET` | `/api/core/contents/{contentTypeUniqueId}` | Get contents by ContentType. |
468
+ | `getContentByEntityId<T, P>(params)` | `GET` | `/api/core/contents/enable/{entityId}` | Get single published content item. |
469
+ | `getContentByUniqueId<T, P>(uniqueId)` | `GET` | `/api/core/contents/{uniqueId}/byContentUniqueId/enable` | Get content by uniqueId. |
470
+ | `searchContent<T, P>(params)` | `GET` | `/api/core/contents/search` | Search published contents by keyword. |
471
+ | `addContent<T, P>(params)` | `POST` | `/api/core/contents/{contentTypeUniqueId}/add-publish` | Add and publish content item. |
472
+ | `createContent<T, P>(params)` | `POST` | `/api/core/contents/{contentTypeUniqueId}` | Create unpublished/draft content. |
473
+ | `editContent<T, P>(entityId, params)` | `POST` | `/api/core/contents/{typeId}/{entityId}/edit-publish` | Update and publish content item. |
474
+ | `updateContent<T, P>(entityId, params)` | `POST` | `/api/core/contents/{typeId}/{entityId}` | Update content item. |
475
+ | `patchContent<T, P>(entityId, params)` | `PATCH` | `/api/core/contents/{typeId}/{entityId}` | Partial update content. |
476
+ | `publishContent(typeId, entityId)` | `POST` | `/api/core/contents/{typeId}/publish/{entityId}` | Publish single content item. |
477
+ | `unpublishContent(typeId, entityId)` | `POST` | `/api/core/contents/{typeId}/unpublish/{entityId}` | Unpublish single content item. |
478
+ | `batchPublish(params)` | `PUT` | `/api/core/contents/{typeId}/publish` | Batch publish multiple contents. |
479
+ | `batchUnpublish(params)` | `PUT` | `/api/core/contents/{typeId}/unpublish` | Batch unpublish multiple contents. |
480
+ | `archiveContent(params)` | `POST` | `/api/core/contents/{typeId}/archive` | Archive content items. |
481
+ | `unarchiveContent(params)` | `POST` | `/api/core/contents/{typeId}/unarchive` | Unarchive content items. |
482
+ | `getArchivedContents(params)` | `GET` | `/api/core/contents/archive` | Get archived contents list. |
483
+ | `getDrafts(params)` | `GET` | `/api/core/contents/draft` | Get drafts list. |
484
+ | `createDraft(params)` | `POST` | `/api/core/contents/{typeId}/draft` | Create a draft. |
485
+ | `getComments(typeId, entityId)` | `GET` | `/api/core/contents/{typeId}/comments/{entityId}` | Get comments on content. |
486
+ | `getLikes(uniqueId, entityId)` | `GET` | `/api/core/contents/{uniqueId}/like/{entityId}` | Get likes of content item. |
487
+ | `getCategories(params)` | `GET` | `/api/core/tags/root/tree/enable` | Get category/tag tree. |
488
+ | `timelineSearch(query)` | `GET` | `/api/core/contents/ai/timeline-search/enable` | Advanced AI timeline search. |
489
+
490
+ #### Product Methods (`podSdk.product` / `podSdk.cms.products`)
491
+
492
+ | Method | HTTP | Path | Description |
493
+ | :--- | :--- | :--- | :--- |
494
+ | `getProducts<T, P>(params, opts)` | `GET` | `/api/core/products/enable` | Get enabled published products with price/discount filters. |
495
+ | `getAllProducts<T, P>(params, opts)` | `GET` | `/api/core/products` | Get products with provider credentials (`Access-Token`). |
496
+ | `getManageProducts<T, P>(params, opts)`| `GET` | `/api/core/products/manage` | Get products for manager view. |
497
+ | `getProductsByType<T, P>(typeId, params)`| `GET` | `/api/core/products/{productTypeUniqueId}` | Get products by ProductType. |
498
+ | `getProductByEntityId<T, P>(params)` | `GET` | `/api/core/products/enable/{entityId}` | Get single product by entityId. |
499
+ | `getProductByUniqueId<T, P>(uniqueId)` | `GET` | `/api/core/products/{uniqueId}/byProductUniqueId/enable` | Get product by uniqueId. |
500
+ | `getProductByBarcode<T, P>(barcode)` | `GET` | `/api/core/products/barcode/enable` | Lookup products matching a barcode. |
501
+ | `searchProducts<T, P>(params)` | `GET` | `/api/core/products/enable/search` | Search published products. |
502
+ | `addProduct<T, P>(params)` | `POST` | `/api/core/products/{productTypeUniqueId}/add-publish` | Create and publish new product. |
503
+ | `createProduct<T, P>(params)` | `POST` | `/api/core/products/{productTypeUniqueId}` | Create draft/unpublished product. |
504
+ | `editProduct<T, P>(entityId, params)` | `POST` | `/api/core/products/{typeId}/{entityId}/edit-publish` | Edit and publish existing product. |
505
+ | `updateProduct<T, P>(entityId, params)` | `POST` | `/api/core/products/{typeId}/{entityId}` | Update existing product. |
506
+ | `patchProduct<T, P>(entityId, params)` | `PATCH` | `/api/core/products/{typeId}/{entityId}` | Partial update product. |
507
+ | `publishProduct(typeId, entityId)` | `POST` | `/api/core/products/{typeId}/publish/{entityId}` | Publish product. |
508
+ | `unpublishProduct(typeId, entityId)` | `POST` | `/api/core/products/{typeId}/unpublish/{entityId}` | Unpublish product. |
509
+ | `batchPublish(params)` | `PUT` | `/api/core/products/publish` | Batch publish multiple products. |
510
+ | `batchUnpublish(params)` | `PUT` | `/api/core/products/unpublish` | Batch unpublish multiple products. |
511
+ | `archiveProduct(params)` | `POST` | `/api/core/products/archive` | Archive products. |
512
+ | `unarchiveProduct(params)` | `POST` | `/api/core/products/unarchive` | Unarchive products. |
513
+ | `getArchivedProducts(params)` | `GET` | `/api/core/products/archive` | Get archived products list. |
514
+ | `timelineSearch(query)` | `GET` | `/api/core/products/ai/timeline-search/enable` | AI timeline search on products. |
515
+
516
+ ---
517
+
518
+ ## Deep Dive 3: CMS Tags & Tag Categories (RAD API Swagger: tag=tags)
519
+
520
+ Manage tag categories and multi-level hierarchical tag trees ([Swagger documentation](https://rad-sandbox.sandpod.ir/api/documentation?tag=tags)).
521
+
522
+ ### Accessing Tag Services
523
+
524
+ Access via `podSdk.tags` or `podSdk.cms.tags`:
525
+
526
+ ```typescript
527
+ // Category Management (with Access-Token)
528
+ const categories = await podSdk.tags.getTagCategories({ size: 20 });
529
+ await podSdk.tags.createTagCategory({ name: 'Technology', desc: 'Tech articles' });
530
+ await podSdk.tags.publishTagCategory(categoryId);
531
+
532
+ // Public Hierarchical Tag Tree (without token, defaults to 'root')
533
+ const rootTree = await podSdk.tags.getTagTree('root', { levelCount: 3 });
534
+
535
+ // Backward-compatible shortcut
536
+ const categoriesTree = await podSdk.cms.getCategories({ levelCount: 3 });
537
+
538
+ // Tree Node Management (create, update parent, code, ancestors)
539
+ await podSdk.tags.createTagTreeItem('technology', {
540
+ name: 'Next.js',
541
+ code: 'NEXTJS',
542
+ parentId: 10,
543
+ });
544
+ await podSdk.tags.updateTagTreeParent('technology', nodeId, newParentId);
545
+ await podSdk.tags.updateTagTreeCode('technology', nodeId, 'NEW_CODE');
546
+ const ancestors = await podSdk.tags.getTagTreeAncestors('technology', nodeId);
547
+ ```
548
+
549
+ ### Tag Methods Reference (`podSdk.tags` / `podSdk.cms.tags`)
550
+
551
+ | Method | HTTP | Path | Description |
552
+ | :--- | :--- | :--- | :--- |
553
+ | `getTagCategories(params)` | `GET` | `/api/core/tags/category` | List tag categories (manage). |
554
+ | `createTagCategory(params)` | `POST` | `/api/core/tags/category` | Create new tag category. |
555
+ | `getTagCategory(id)` | `GET` | `/api/core/tags/category/{id}` | Show single tag category. |
556
+ | `updateTagCategory(id, params)` | `PUT` | `/api/core/tags/category/{id}` | Update tag category. |
557
+ | `publishTagCategory(id)` | `PUT` | `/api/core/tags/category/{id}/publish` | Publish tag category. |
558
+ | `unpublishTagCategory(id)` | `PUT` | `/api/core/tags/category/{id}/unpublish` | Unpublish tag category. |
559
+ | `getTagTree(categoryId, params)` | `GET` | `/api/core/tags/{categoryId}/tree/enable` | Get enabled tag tree (public). |
560
+ | `getManageTagTree(categoryId, params)` | `GET` | `/api/core/tags/{categoryId}/tree/manage` | Get tag tree (manage with token). |
561
+ | `createTagTreeItem(categoryId, params)` | `POST` | `/api/core/tags/{categoryId}/tree` | Create tag tree node for category. |
562
+ | `getTagTreeItem(categoryId, id)` | `GET` | `/api/core/tags/{categoryId}/tree/{id}/enable` | Show enabled tag tree node. |
563
+ | `getManageTagTreeItem(categoryId, id)` | `GET` | `/api/core/tags/{categoryId}/tree/{id}/manage` | Show tag tree node (manage). |
564
+ | `updateTagTreeItem(categoryId, id, params)` | `PUT` | `/api/core/tags/{categoryId}/tree/{id}` | Update tag tree node. |
565
+ | `publishTagTreeItem(categoryId, id)` | `PUT` | `/api/core/tags/{categoryId}/tree/publish/{id}` | Publish tag tree node. |
566
+ | `unpublishTagTreeItem(categoryId, id)` | `PUT` | `/api/core/tags/{categoryId}/tree/unpublish/{id}` | Unpublish tag tree node. |
567
+ | `updateTagTreeParent(categoryId, id, parentId)` | `PUT` | `/api/core/tags/{categoryId}/tree/parent/{id}` | Update parent of tag tree node. |
568
+ | `getTagTreeAncestors(categoryId, id, params)` | `GET` | `/api/core/tags/{categoryId}/tree/parent/{id}/enable` | Get node ancestors (public). |
569
+ | `getManageTagTreeAncestors(categoryId, id, params)`| `GET` | `/api/core/tags/{categoryId}/tree/parent/{id}/manage` | Get node ancestors (manage). |
570
+ | `updateTagTreeCode(categoryId, id, code)` | `PUT` | `/api/core/tags/{categoryId}/tree/code/{id}` | Update code of tag tree node. |
571
+
572
+ ---
573
+
574
+ ## Deep Dive 4: CustomPost & Generic Typed Repositories
575
+
576
+ `CustomPost` allows persisting arbitrary JSON schemas on the POD Platform. `@rezamirzapour/pod-sdk` provides two complementary approaches:
577
+
578
+ 1. **Direct Service Method Calls with Generics (`searchTimelineByMetadata<T>`, `getCustomPost<T>`, `addCustomPost<T>`)**
579
+ 2. **High-Level Typed CRUD Repository (`CustomPostCrudService<DataType>`)**
580
+
581
+ ```typescript
582
+ // lib/blog.ts
583
+ import { podSdk } from '@/lib/pod';
584
+
585
+ export interface BlogPostMeta {
586
+ slug: string;
587
+ title: string;
588
+ summary: string;
589
+ views: number;
590
+ }
591
+
592
+ // Search posts with typed metadata query
593
+ export async function getPublishedPosts() {
594
+ const response = await podSdk.customPost.searchTimelineByMetadata<BlogPostMeta>({
595
+ entityName: 'blog_post',
596
+ metadata: { views: 100 },
597
+ size: 20,
598
+ offset: 0,
599
+ });
600
+
601
+ return response.result.map((r) => r.item.metadata);
602
+ }
603
+ ```
604
+
605
+ ---
606
+
607
+ ## Other POD Microservices
608
+
609
+ ### 4. POD SSO (OTP Handshake with Web Crypto RSA Signature)
610
+
611
+ ```typescript
612
+ // app/actions/auth.ts
613
+ 'use server';
614
+
615
+ import { podSdk } from '@/lib/pod';
616
+
617
+ // Step 1: Handshake and send OTP
618
+ export async function sendOtp(phoneNumber: string, clientIp: string) {
619
+ const handshake = await podSdk.sso.handshake(`web-${Date.now()}`, clientIp);
620
+ if (handshake.hasError || !handshake.result?.keyId) {
621
+ throw new Error(handshake.message || 'SSO Handshake failed');
622
+ }
623
+
624
+ // Automatic RSA-SHA256 signature using Web Crypto API
625
+ const otpRes = await podSdk.sso.sendOtpCode(handshake.result.keyId, phoneNumber);
626
+ return {
627
+ success: !otpRes.hasError,
628
+ authorization: otpRes.authorization,
629
+ };
630
+ }
631
+
632
+ // Step 2: Verify OTP code & exchange for tokens
633
+ export async function verifyOtp(authorization: string, phoneNumber: string, code: string) {
634
+ const verify = await podSdk.sso.verifyOtpCode(authorization, phoneNumber, code);
635
+ if (verify.hasError || !verify.result?.code) {
636
+ throw new Error(verify.message || 'OTP verification failed');
637
+ }
638
+
639
+ const tokens = await podSdk.sso.generateToken(verify.result.code);
640
+ return tokens.result; // { access_token, refresh_token, expires_in }
641
+ }
642
+
643
+ // Step 3: Fetch profile with access token
644
+ export async function getProfile(accessToken: string) {
645
+ const profile = await podSdk.sso.getUserProfile(accessToken);
646
+ return profile.result;
647
+ }
648
+ ```
649
+
650
+ ---
651
+
652
+ ### 5. Podspace (File Uploads & CDN URLs)
653
+
654
+ ```typescript
655
+ // app/actions/upload.ts
656
+ 'use server';
657
+
658
+ import { podSdk } from '@/lib/pod';
659
+
660
+ export async function uploadFile(formData: FormData) {
661
+ const res = await podSdk.podspace.uploadFile(formData, '/uploads', true);
662
+ if (res.hasError) {
663
+ throw new Error(res.message || 'Upload failed');
664
+ }
665
+
666
+ const publicUrl = podSdk.podspace.getFileUrl(res.result.hash, true);
667
+ return { url: publicUrl, hash: res.result.hash };
668
+ }
669
+ ```
670
+
671
+ ---
672
+
673
+ ### 6. Notification (SMS Delivery)
674
+
675
+ ```typescript
676
+ // app/actions/notify.ts
677
+ 'use server';
678
+
679
+ import { podSdk } from '@/lib/pod';
680
+
681
+ export async function sendSms(phoneNumber: string, text: string) {
682
+ return podSdk.notification.sendSms({
683
+ receptor: phoneNumber,
684
+ message: text,
685
+ });
686
+ }
687
+ ```
688
+
689
+ ---
690
+
691
+ ### 7. Social (Comments & Likes)
692
+
693
+ ```typescript
694
+ // app/actions/social.ts
695
+ 'use server';
696
+
697
+ import { podSdk } from '@/lib/pod';
698
+
699
+ export async function addComment(postId: number, content: string) {
700
+ return podSdk.social.addComment({
701
+ postId,
702
+ text: content,
703
+ });
704
+ }
705
+
706
+ export async function likePost(postId: number) {
707
+ return podSdk.social.likePost(postId);
708
+ }
709
+ ```
710
+
711
+ ---
712
+
713
+ ### 8. IUMS (Identity & University Management Service)
714
+
715
+ ```typescript
716
+ // app/actions/iums.ts
717
+ 'use server';
718
+
719
+ import { podSdk } from '@/lib/pod';
720
+
721
+ export async function getStudentByNationalCode(nationalCode: string) {
722
+ return podSdk.iums.getStudentInformationByNationalCode(nationalCode);
723
+ }
724
+ ```
725
+
726
+ ---
727
+
728
+ ## Environment Variables Reference
729
+
730
+ ```env
731
+ # .env.local
732
+ POD_API_TOKEN=your_api_token_here
733
+ POD_CLIENT_ID=your_client_id_here
734
+ POD_CLIENT_SECRET=your_client_secret_here
735
+ POD_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
736
+ ```
737
+
738
+ ---
739
+
740
+ ## License
741
+
742
+ MIT © [Reza](https://github.com/rezamirzapour2)