@rezamirzapour/pod-sdk 1.0.4 → 1.0.7

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,713 +1,971 @@
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
- ## Quickstart
51
-
52
- ### 1. Initialize the Client
53
-
54
- ```typescript
55
- // lib/pod.ts
56
- import { createPodSdk } from '@rezamirzapour/pod-sdk';
57
-
58
- export const podSdk = createPodSdk({
59
- apiToken: process.env.POD_API_TOKEN,
60
- clientId: process.env.POD_CLIENT_ID,
61
- clientSecret: process.env.POD_CLIENT_SECRET,
62
- privateKeyPem: process.env.POD_PRIVATE_KEY_PEM, // RSA Private Key for SSO OTP signing
63
- revalidate: 3600, // Default ISR revalidation in seconds
64
- urls: {
65
- // Optional overrides (defaults to official POD endpoints)
66
- accounts: 'https://accounts.pod.ir',
67
- apiPod: 'https://api.pod.ir',
68
- cms: 'https://cms.pod.ir',
69
- podspace: 'https://podspace.pod.ir',
70
- podform: 'https://podform.pod.ir',
71
- notification: 'https://notification.pod.ir',
72
- iums: 'https://iums.pod.ir',
73
- },
74
- });
75
- ```
76
-
77
- ---
78
-
79
- ## Deep Dive 1: CMS Content & Data Formatting
80
-
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`).
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
-
86
- ### 🏗️ Clean Architecture Pattern: Dedicated Data Access Layer (`NewsApi`)
87
-
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`).
89
-
90
- #### Step 1: Create the Data Access Layer (`services/newsApi.ts`)
91
-
92
- ```typescript
93
- // services/newsApi.ts
94
- import { podSdk } from '@/lib/pod';
95
-
96
- // 1. Define the raw CMS Formatted Metadata schema (T)
97
- export interface NewsArticleFormatted {
98
- title: string;
99
- coverImage: string; // Auto-transformed from hash to full Podspace CDN URL!
100
- lead: string;
101
- body: string;
102
- author?: string;
103
- }
104
-
105
- // 2. Define computed / normalized domain attributes (P)
106
- export interface NewsArticleNormalized {
107
- summary: string;
108
- readTime: number;
109
- }
110
-
111
- // 3. Define the Clean Domain Model consumed by your React Components
112
- export interface NewsItem {
113
- id: number;
114
- entityId: number;
115
- title: string;
116
- coverImage: string;
117
- summary: string;
118
- body: string;
119
- readTime: number;
120
- publishedAt?: number;
121
- }
122
-
123
- export interface GetNewsListParams {
124
- page?: number;
125
- pageSize?: number;
126
- tagTrees?: string[];
127
- }
128
-
129
- export class NewsApi {
130
- private static readonly CONTENT_TYPE = 'company_news';
131
-
132
- /**
133
- * Fetches published news articles with Next.js App Router ISR Caching
134
- */
135
- public static async getNewsList({
136
- page = 1,
137
- pageSize = 10,
138
- tagTrees,
139
- }: GetNewsListParams = {}): Promise<{ items: NewsItem[]; total: number }> {
140
- const offset = (page - 1) * pageSize;
141
-
142
- // Call getContent with exact generics: <NewsArticleFormatted, NewsArticleNormalized>
143
- const response = await podSdk.cms.getContent<NewsArticleFormatted, NewsArticleNormalized>(
144
- {
145
- contentTypeUniqueId: this.CONTENT_TYPE,
146
- size: pageSize,
147
- offset,
148
- tagTrees,
149
- },
150
- {
151
- // Custom normalizer executed during data formatting
152
- normalizer: (item) => ({
153
- summary: item.formatted.lead || item.formatted.title,
154
- readTime: Math.max(1, Math.ceil((item.formatted.body?.length || 0) / 500)),
155
- }),
156
- revalidate: 1800, // Next.js ISR: cache for 30 minutes
157
- }
158
- );
159
-
160
- if (response.hasError || !Array.isArray(response.result)) {
161
- return { items: [], total: 0 };
162
- }
163
-
164
- const items: NewsItem[] = response.result.map((item) => ({
165
- id: item.id,
166
- entityId: item.entityId,
167
- title: item.formatted.title,
168
- coverImage: item.formatted.coverImage,
169
- summary: item.__normalized.summary,
170
- body: item.formatted.body,
171
- readTime: item.__normalized.readTime,
172
- publishedAt: item.timestamp,
173
- }));
174
-
175
- return {
176
- items,
177
- total: response.count || items.length,
178
- };
179
- }
180
-
181
- /**
182
- * Fetches a single news article by its entity ID
183
- */
184
- public static async getNewsById(entityId: number): Promise<NewsItem | null> {
185
- const response = await podSdk.cms.getContentByEntityId<NewsArticleFormatted, NewsArticleNormalized>({
186
- entityId,
187
- contentTypeUniqueId: this.CONTENT_TYPE,
188
- });
189
-
190
- const item = response.result?.[0];
191
- if (!item) return null;
192
-
193
- return {
194
- id: item.id,
195
- entityId: item.entityId,
196
- title: item.formatted.title,
197
- coverImage: item.formatted.coverImage,
198
- summary: item.formatted.lead || item.formatted.title,
199
- body: item.formatted.body,
200
- readTime: Math.max(1, Math.ceil((item.formatted.body?.length || 0) / 500)),
201
- publishedAt: item.timestamp,
202
- };
203
- }
204
-
205
- /**
206
- * Publishes a new article to CMS
207
- */
208
- public static async publishNews(data: {
209
- title: string;
210
- lead: string;
211
- body: string;
212
- imageHash: string;
213
- }) {
214
- return podSdk.cms.addContent({
215
- contentTypeUniqueId: this.CONTENT_TYPE,
216
- body: {
217
- name: data.title,
218
- fieldCode: ['title', 'lead', 'body', 'coverImage'],
219
- fieldValue: [data.title, data.lead, data.body, data.imageHash],
220
- canComment: true,
221
- canLike: true,
222
- enable: true,
223
- isSearchable: true,
224
- },
225
- });
226
- }
227
- }
228
- ```
229
-
230
- #### Step 2: Consume `NewsApi` in Next.js Server Components (`app/news/page.tsx`)
231
-
232
- ```tsx
233
- // app/news/page.tsx (Server Component)
234
- import { NewsApi } from '@/services/newsApi';
235
- import Link from 'next/link';
236
-
237
- interface NewsPageProps {
238
- searchParams: { page?: string };
239
- }
240
-
241
- export default async function NewsPage({ searchParams }: NewsPageProps) {
242
- const page = Number(searchParams.page) || 1;
243
- const { items, total } = await NewsApi.getNewsList({ page, pageSize: 12 });
244
-
245
- return (
246
- <main className="container mx-auto py-8 px-4">
247
- <h1 className="text-3xl font-bold mb-6">Company News</h1>
248
-
249
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
250
- {items.map((news) => (
251
- <article key={news.entityId} className="border rounded-xl overflow-hidden shadow-sm hover:shadow-md transition">
252
- {news.coverImage && (
253
- <img
254
- src={news.coverImage}
255
- alt={news.title}
256
- className="w-full h-48 object-cover"
257
- />
258
- )}
259
- <div className="p-4">
260
- <h2 className="text-xl font-semibold mb-2">
261
- <Link href={`/news/${news.entityId}`}>{news.title}</Link>
262
- </h2>
263
- <p className="text-gray-600 text-sm line-clamp-2">{news.summary}</p>
264
- <div className="mt-4 flex items-center justify-between text-xs text-gray-400">
265
- <span>{news.readTime} min read</span>
266
- <Link href={`/news/${news.entityId}`} className="text-blue-600 font-medium hover:underline">
267
- Read more
268
- </Link>
269
- </div>
270
- </div>
271
- </article>
272
- ))}
273
- </div>
274
- </main>
275
- );
276
- }
277
- ```
278
-
279
- ---
280
-
281
- ## Deep Dive 2: CMS Product Module (RAD API Swagger)
282
-
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.
284
-
285
- ### Accessing Products
286
-
287
- You can access product methods either via `podSdk.product` or `podSdk.cms.products`:
288
-
289
- ```typescript
290
- // Both point to the same CmsProductService instance
291
- podSdk.product.getProducts(...);
292
- podSdk.cms.products.getProducts(...);
293
- ```
294
-
295
- ### 🛍️ Clean Architecture Pattern: Product Data Access Layer (`ProductsApi`)
296
-
297
- ```typescript
298
- // services/productsApi.ts
299
- import { podSdk } from '@/lib/pod';
300
-
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
307
- }
308
-
309
- // 2. Computed / normalized presentation fields (P)
310
- export interface LaptopNormalized {
311
- finalPrice: number;
312
- discountBadge: string;
313
- }
314
-
315
- // 3. Clean Domain Product Model
316
- export interface ProductItemModel {
317
- entityId: number;
318
- name: string;
319
- price: number;
320
- finalPrice: number;
321
- discountBadge: string;
322
- photoUrl: string;
323
- specs: {
324
- cpu: string;
325
- ram: string;
326
- storage: string;
327
- };
328
- }
329
-
330
- export class ProductsApi {
331
- private static readonly PRODUCT_TYPE = 'laptops';
332
-
333
- /**
334
- * Fetches published products with price & discount filters
335
- */
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;
345
-
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
- }));
385
- }
386
-
387
- /**
388
- * Retrieves single product by entityId
389
- */
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;
396
- }
397
-
398
- /**
399
- * Look up product by barcode scanner
400
- */
401
- public static async getProductByBarcode(barcode: string) {
402
- return podSdk.product.getProductByBarcode<LaptopFormatted>(barcode);
403
- }
404
-
405
- /**
406
- * Adds and publishes a new product
407
- */
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
- });
425
- }
426
- }
427
- ```
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
-
487
- ---
488
-
489
- ## Deep Dive 3: CMS Tags & Tag Categories (RAD API Swagger: tag=tags)
490
-
491
- Manage tag categories and multi-level hierarchical tag trees ([Swagger documentation](https://rad-sandbox.sandpod.ir/api/documentation?tag=tags)).
492
-
493
- ### Accessing Tag Services
494
-
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);
518
- ```
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
-
543
- ---
544
-
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>`)**
551
-
552
- ```typescript
553
- // lib/blog.ts
554
- import { podSdk } from '@/lib/pod';
555
-
556
- export interface BlogPostMeta {
557
- slug: string;
558
- title: string;
559
- summary: string;
560
- views: number;
561
- }
562
-
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);
573
- }
574
- ```
575
-
576
- ---
577
-
578
- ## Other POD Microservices
579
-
580
- ### 4. POD SSO (OTP Handshake with Web Crypto RSA Signature)
581
-
582
- ```typescript
583
- // app/actions/auth.ts
584
- 'use server';
585
-
586
- import { podSdk } from '@/lib/pod';
587
-
588
- // Step 1: Handshake and send OTP
589
- export async function sendOtp(phoneNumber: string, clientIp: string) {
590
- const handshake = await podSdk.sso.handshake(`web-${Date.now()}`, clientIp);
591
- if (handshake.hasError || !handshake.result?.keyId) {
592
- throw new Error(handshake.message || 'SSO Handshake failed');
593
- }
594
-
595
- // Automatic RSA-SHA256 signature using Web Crypto API
596
- const otpRes = await podSdk.sso.sendOtpCode(handshake.result.keyId, phoneNumber);
597
- return {
598
- success: !otpRes.hasError,
599
- authorization: otpRes.authorization,
600
- };
601
- }
602
-
603
- // Step 2: Verify OTP code & exchange for tokens
604
- export async function verifyOtp(authorization: string, phoneNumber: string, code: string) {
605
- const verify = await podSdk.sso.verifyOtpCode(authorization, phoneNumber, code);
606
- if (verify.hasError || !verify.result?.code) {
607
- throw new Error(verify.message || 'OTP verification failed');
608
- }
609
-
610
- const tokens = await podSdk.sso.generateToken(verify.result.code);
611
- return tokens.result; // { access_token, refresh_token, expires_in }
612
- }
613
-
614
- // Step 3: Fetch profile with access token
615
- export async function getProfile(accessToken: string) {
616
- const profile = await podSdk.sso.getUserProfile(accessToken);
617
- return profile.result;
618
- }
619
- ```
620
-
621
- ---
622
-
623
- ### 5. Podspace (File Uploads & CDN URLs)
624
-
625
- ```typescript
626
- // app/actions/upload.ts
627
- 'use server';
628
-
629
- import { podSdk } from '@/lib/pod';
630
-
631
- export async function uploadFile(formData: FormData) {
632
- const res = await podSdk.podspace.uploadFile(formData, '/uploads', true);
633
- if (res.hasError) {
634
- throw new Error(res.message || 'Upload failed');
635
- }
636
-
637
- const publicUrl = podSdk.podspace.getFileUrl(res.result.hash, true);
638
- return { url: publicUrl, hash: res.result.hash };
639
- }
640
- ```
641
-
642
- ---
643
-
644
- ### 6. Notification (SMS Delivery)
645
-
646
- ```typescript
647
- // app/actions/notify.ts
648
- 'use server';
649
-
650
- import { podSdk } from '@/lib/pod';
651
-
652
- export async function sendSms(phoneNumber: string, text: string) {
653
- return podSdk.notification.sendSms({
654
- receptor: phoneNumber,
655
- message: text,
656
- });
657
- }
658
- ```
659
-
660
- ---
661
-
662
- ### 7. Social (Comments & Likes)
663
-
664
- ```typescript
665
- // app/actions/social.ts
666
- 'use server';
667
-
668
- import { podSdk } from '@/lib/pod';
669
-
670
- export async function addComment(postId: number, content: string) {
671
- return podSdk.social.addComment({
672
- postId,
673
- text: content,
674
- });
675
- }
676
-
677
- export async function likePost(postId: number) {
678
- return podSdk.social.likePost(postId);
679
- }
680
- ```
681
-
682
- ---
683
-
684
- ### 8. IUMS (Identity & University Management Service)
685
-
686
- ```typescript
687
- // app/actions/iums.ts
688
- 'use server';
689
-
690
- import { podSdk } from '@/lib/pod';
691
-
692
- export async function getStudentByNationalCode(nationalCode: string) {
693
- return podSdk.iums.getStudentInformationByNationalCode(nationalCode);
694
- }
695
- ```
696
-
697
- ---
698
-
699
- ## Environment Variables Reference
700
-
701
- ```env
702
- # .env.local
703
- POD_API_TOKEN=your_api_token_here
704
- POD_CLIENT_ID=your_client_id_here
705
- POD_CLIENT_SECRET=your_client_secret_here
706
- POD_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
707
- ```
708
-
709
- ---
710
-
711
- ## License
712
-
713
- 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` | `public-apis` | Complete Cloud Storage: file/image uploads, resumable chunked uploads, folders, versions, share links, workspaces, trash, tags, & metadata. |
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
+ ## Deep Dive 5: Podspace Cloud Storage (Official Swagger API)
608
+
609
+ The **Podspace** module provides complete enterprise integration with the official POD Podspace Cloud Storage service ([Swagger documentation: `podspace.sandpod.ir/api/docs`](http://podspace.sandpod.ir/api/docs)). It supports standard and resumable uploads, public/private files, dynamic CDN image transformations, folder hierarchy, file versioning, public share links, trash recovery, team workspaces, user groups, and custom metadata.
610
+
611
+ ### Sub-Services Architecture
612
+
613
+ All Podspace operations are accessible via specialized, modular sub-services under `podSdk.podspace`:
614
+
615
+ ```typescript
616
+ podSdk.podspace.files // File details, rename, move, copy, search, versions, zip
617
+ podSdk.podspace.folders // Folder creation, nested paths, children, actions
618
+ podSdk.podspace.resumable // Chunked/large file upload (tus protocol: create, append, finalize)
619
+ podSdk.podspace.links // Public/password-protected shareable download links
620
+ podSdk.podspace.shares // Sharing files/folders with specific users & permissions
621
+ podSdk.podspace.trash // Recycle bin: list, restore, empty, auto-cleanup
622
+ podSdk.podspace.bookmarks // Starred / favorite files and folders
623
+ podSdk.podspace.tags // File & folder tagging
624
+ podSdk.podspace.metadata // Custom key-value metadata & descriptions
625
+ podSdk.podspace.userGroups // Group storage, member uploads & quota usage
626
+ podSdk.podspace.workspaces // Team workspaces & member roles
627
+ podSdk.podspace.me // User storage usage, plans, personal/chat folders
628
+ ```
629
+
630
+ > [!TIP]
631
+ > Commonly used methods (`uploadFile`, `downloadFile`, `getFileUrl`, `getImageUrl`, `getThumbnailUrl`, `createFolder`, `searchFiles`) are also directly available on `podSdk.podspace` for maximum developer convenience.
632
+
633
+ ---
634
+
635
+ ### Key Usage Examples
636
+
637
+ #### 1. File Upload & CDN URLs (100% Backward Compatible)
638
+
639
+ ```typescript
640
+ // app/actions/upload.ts
641
+ 'use server';
642
+
643
+ import { podSdk } from '@/lib/pod';
644
+
645
+ export async function uploadDocument(formData: FormData) {
646
+ // Supports both positional args (formData, path, isPublic) and options object
647
+ const res = await podSdk.podspace.uploadFile(formData, {
648
+ path: '/documents/invoices',
649
+ isPublic: true,
650
+ });
651
+
652
+ if (res.hasError) {
653
+ throw new Error(res.message || 'Upload failed');
654
+ }
655
+
656
+ const file = res.result;
657
+
658
+ // Resolve direct download URL
659
+ const downloadUrl = podSdk.podspace.getFileUrl(file.hash, true);
660
+
661
+ return {
662
+ hash: file.hash,
663
+ name: file.name,
664
+ size: file.size,
665
+ url: downloadUrl,
666
+ };
667
+ }
668
+ ```
669
+
670
+ #### 2. Image & Thumbnail CDN URL Helper
671
+
672
+ ```typescript
673
+ // Helper functions generate instant CDN URLs without network calls:
674
+ const fullImageUrl = podSdk.podspace.getImageUrl(imageHash, {
675
+ isPublic: true,
676
+ size: '1200x800',
677
+ quality: 85,
678
+ crop: true,
679
+ });
680
+
681
+ const thumbUrl = podSdk.podspace.getThumbnailUrl(imageHash, {
682
+ isPublic: true,
683
+ size: '150x150',
684
+ });
685
+ ```
686
+
687
+ #### 3. Base64 Image Upload
688
+
689
+ ```typescript
690
+ // Upload image directly from a Base64 data URL
691
+ const uploadRes = await podSdk.podspace.uploadImageBase64({
692
+ base64: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAE...',
693
+ filename: 'avatar.png',
694
+ path: '/avatars',
695
+ isPublic: true,
696
+ });
697
+ ```
698
+
699
+ #### 4. Resumable Chunked Upload (Large Files)
700
+
701
+ For reliable multi-gigabyte uploads with pause/resume support:
702
+
703
+ ```typescript
704
+ // Step 1: Initialize resumable session
705
+ const session = await podSdk.podspace.resumable.create({
706
+ filename: 'presentation.mp4',
707
+ fileSize: totalBytes,
708
+ path: '/videos',
709
+ isPublic: false,
710
+ });
711
+
712
+ const uploadUrl = session.uploadUrl;
713
+
714
+ // Step 2: Check current server offset if resuming
715
+ const offset = await podSdk.podspace.resumable.getStatus(uploadUrl);
716
+
717
+ // Step 3: Append chunks
718
+ await podSdk.podspace.resumable.append({
719
+ uploadUrl,
720
+ offset,
721
+ chunk: chunkBuffer,
722
+ });
723
+
724
+ // Step 4: Finalize when all bytes are uploaded
725
+ const completedFile = await podSdk.podspace.resumable.finalizeUpload(uploadUrl);
726
+ ```
727
+
728
+ #### 5. Folder Hierarchy & Listing
729
+
730
+ ```typescript
731
+ // Create a folder
732
+ await podSdk.podspace.folders.createFolder({
733
+ name: 'Reports-2026',
734
+ path: '/finance',
735
+ });
736
+
737
+ // Recursively create nested directory paths
738
+ await podSdk.podspace.folders.createDirectories('/finance/2026/Q1/receipts');
739
+
740
+ // List folder contents with pagination
741
+ const contents = await podSdk.podspace.folders.getFolderChildren({
742
+ path: '/finance/Reports-2026',
743
+ offset: 0,
744
+ size: 50,
745
+ });
746
+ ```
747
+
748
+ #### 6. Shareable Public / Password-Protected Links
749
+
750
+ ```typescript
751
+ // Create an expiring, password-protected download link
752
+ const linkRes = await podSdk.podspace.links.createLink(fileHash, {
753
+ type: 'DOWNLOAD',
754
+ password: 'SecurePassword123!',
755
+ expiresAt: Date.now() + 7 * 24 * 3600 * 1000, // 7 days
756
+ });
757
+
758
+ // Share with a specific user
759
+ await podSdk.podspace.shares.shareWithUser(fileHash, {
760
+ username: 'john_doe',
761
+ permission: 'READ',
762
+ });
763
+ ```
764
+
765
+ #### 7. File Versions & Rollback
766
+
767
+ ```typescript
768
+ // List previous versions of a file
769
+ const versions = await podSdk.podspace.files.getFileVersions(fileHash);
770
+
771
+ // Rollback to a specific historical version
772
+ await podSdk.podspace.files.rollbackFileVersion(fileHash, versionId);
773
+ ```
774
+
775
+ #### 8. Trash & Recycling
776
+
777
+ ```typescript
778
+ // Move file or folder to trash
779
+ await podSdk.podspace.files.trashEntity(fileHash);
780
+
781
+ // List recycle bin
782
+ const trashItems = await podSdk.podspace.trash.getTrashList({ size: 20 });
783
+
784
+ // Restore or permanently delete
785
+ await podSdk.podspace.trash.restore(fileHash);
786
+ await podSdk.podspace.trash.deletePermanently(fileHash);
787
+ ```
788
+
789
+ ---
790
+
791
+ ### Podspace Methods Reference
792
+
793
+ | Sub-Service | Method | HTTP | Path | Description |
794
+ | :--- | :--- | :--- | :--- | :--- |
795
+ | **Upload / Download** | `uploadFile(formData, params)` | `POST` | `/api/files` | Upload single file with metadata. |
796
+ | | `uploadMultipleFiles(formData, params)` | `POST` | `/api/files/batch` | Upload multiple files simultaneously. |
797
+ | | `uploadImageBase64(params)` | `POST` | `/api/images/base64` | Upload base64 encoded image. |
798
+ | | `replaceFile(hash, formData)` | `PUT` | `/api/files/{hash}` | Overwrite existing file content. |
799
+ | | `uploadByLink(params)` | `POST` | `/api/files/link` | Download remote file into Podspace. |
800
+ | | `downloadFile(hash, opts)` | `GET` | `/api/files/{hash}` | Download raw binary file. |
801
+ | | `downloadImage(hash, opts)` | `GET` | `/api/images/{hash}` | Download transformed image. |
802
+ | | `downloadThumbnail(hash, opts)` | `GET` | `/api/images/thumbnails/{hash}` | Download image thumbnail. |
803
+ | | `downloadFilesAsZip(hashes)` | `POST` | `/api/files/zip` | Batch download files as a ZIP archive. |
804
+ | | `getFileUrl(hash, isPublic)` | - | Client Helper | Generates direct file CDN link. |
805
+ | | `getImageUrl(hash, opts)` | - | Client Helper | Generates transformed image CDN link. |
806
+ | | `getThumbnailUrl(hash, opts)` | - | Client Helper | Generates thumbnail CDN link. |
807
+ | **Files** | `getFileDetail(hash)` | `GET` | `/api/files/{hash}/metadata` | Fetch full metadata for file. |
808
+ | | `getFileDetailByPath(path)` | `GET` | `/api/files/path` | Fetch metadata by absolute path. |
809
+ | | `checkEntityExist(hash)` | `GET` | `/api/entities/{hash}/exist` | Check existence of file/folder. |
810
+ | | `renameEntity(hash, newName)` | `PUT` | `/api/entities/{hash}/rename` | Rename file or folder. |
811
+ | | `moveEntity(hash, targetPath)` | `PUT` | `/api/entities/{hash}/move` | Move file or folder. |
812
+ | | `copyEntity(hash, targetPath)` | `POST` | `/api/entities/{hash}/copy` | Duplicate file or folder. |
813
+ | | `trashEntity(hash)` | `DELETE` | `/api/entities/{hash}` | Move file/folder to trash. |
814
+ | | `searchFiles(params)` | `GET` | `/api/files/search` | Search files by name, type, date, or tags. |
815
+ | | `getFileVersions(hash)` | `GET` | `/api/files/{hash}/versions` | List all historical versions. |
816
+ | | `rollbackFileVersion(hash, id)` | `POST` | `/api/files/{hash}/versions/{id}/rollback` | Rollback to specific version. |
817
+ | | `compressToZip(params)` | `POST` | `/api/files/compress` | Compress files into a ZIP archive. |
818
+ | | `extractZip(params)` | `POST` | `/api/files/extract` | Extract ZIP archive in Podspace. |
819
+ | **Folders** | `createFolder(params)` | `POST` | `/api/folders` | Create a new folder. |
820
+ | | `createDirectories(path)` | `POST` | `/api/folders/directories` | Create nested directory tree. |
821
+ | | `getFolderChildren(params)` | `GET` | `/api/folders/{hash}/children` | List folder contents. |
822
+ | | `getRecentFolders(params)` | `GET` | `/api/folders/recent` | List recently accessed folders. |
823
+ | **Resumable** | `create(params)` | `POST` | `/api/files/resumable` | Initialize resumable upload. |
824
+ | | `append(params)` | `PATCH` | `{uploadUrl}` | Upload chunk to offset. |
825
+ | | `getStatus(uploadUrl)` | `HEAD` | `{uploadUrl}` | Get current uploaded byte offset. |
826
+ | | `finalizeUpload(uploadUrl)` | `POST` | `{uploadUrl}/finalize` | Complete resumable upload. |
827
+ | **Links** | `createLink(hash, params)` | `POST` | `/api/entities/{hash}/links` | Create shareable download link. |
828
+ | | `getUserLinks(params)` | `GET` | `/api/links` | List all created links. |
829
+ | | `revokeLink(linkHash)` | `DELETE` | `/api/links/{linkHash}` | Deactivate share link. |
830
+ | **Shares** | `shareWithUser(hash, params)` | `POST` | `/api/entities/{hash}/shares` | Share with another user. |
831
+ | | `makePublic(hash, isPublic)` | `PUT` | `/api/entities/{hash}/public` | Toggle public/private access. |
832
+ | | `getSharedWithMe(params)` | `GET` | `/api/shares/shared-with-me` | List items shared with user. |
833
+ | **Trash** | `getTrashList(params)` | `GET` | `/api/trash` | List items in recycle bin. |
834
+ | | `restore(hash)` | `POST` | `/api/trash/{hash}/restore` | Restore file/folder. |
835
+ | | `deletePermanently(hash)` | `DELETE` | `/api/trash/{hash}` | Permanently destroy file/folder. |
836
+ | | `emptyTrash()` | `DELETE` | `/api/trash` | Empty entire recycle bin. |
837
+ | **Bookmarks** | `getBookmarks(params)` | `GET` | `/api/bookmarks` | List starred files/folders. |
838
+ | | `addBookmark(hash)` | `POST` | `/api/bookmarks/{hash}` | Star a file or folder. |
839
+ | | `removeBookmark(hash)` | `DELETE` | `/api/bookmarks/{hash}` | Unstar a file or folder. |
840
+ | **Tags** | `addTags(hash, tags)` | `POST` | `/api/entities/{hash}/tags` | Add tags to entity. |
841
+ | | `getEntityTags(hash)` | `GET` | `/api/entities/{hash}/tags` | List entity tags. |
842
+ | | `getEntitiesByTag(tag)` | `GET` | `/api/tags/{tag}/entities` | Search entities by tag. |
843
+ | **Metadata** | `setMetadata(hash, metadata)` | `PUT` | `/api/entities/{hash}/metadata` | Set custom key-value metadata. |
844
+ | | `setDescription(hash, desc)` | `PUT` | `/api/entities/{hash}/description` | Set entity description. |
845
+ | **User Groups** | `createUserGroup(params)` | `POST` | `/api/user-groups` | Create user group. |
846
+ | | `uploadToUserGroup(group, fd)`| `POST` | `/api/user-groups/{group}/files` | Upload file to user group. |
847
+ | | `getUserGroupUsage(group)` | `GET` | `/api/user-groups/{group}/usage` | Get storage quota & usage. |
848
+ | **Workspaces** | `createWorkspace(params)` | `POST` | `/api/workspaces` | Create team workspace. |
849
+ | | `getWorkspaceMembers(id)` | `GET` | `/api/workspaces/{id}/members` | List workspace members. |
850
+ | **Me** | `getUser()` | `GET` | `/api/users/me` | Current user profile. |
851
+ | | `getUserUsageReport()` | `GET` | `/api/users/me/usage` | Total storage usage report. |
852
+ | | `getPersonalFolder()` | `GET` | `/api/users/me/personal-folder` | Get or create root personal folder. |
853
+ | | `getOrCreateChatFolder()` | `GET` | `/api/users/me/chat-folder` | Get or create chat attachments folder. |
854
+
855
+ ---
856
+
857
+ ## Other POD Microservices
858
+
859
+ ### 1. POD SSO (OTP Handshake with Web Crypto RSA Signature)
860
+
861
+ ```typescript
862
+ // app/actions/auth.ts
863
+ 'use server';
864
+
865
+ import { podSdk } from '@/lib/pod';
866
+
867
+ // Step 1: Handshake and send OTP
868
+ export async function sendOtp(phoneNumber: string, clientIp: string) {
869
+ const handshake = await podSdk.sso.handshake(`web-${Date.now()}`, clientIp);
870
+ if (handshake.hasError || !handshake.result?.keyId) {
871
+ throw new Error(handshake.message || 'SSO Handshake failed');
872
+ }
873
+
874
+ // Automatic RSA-SHA256 signature using Web Crypto API
875
+ const otpRes = await podSdk.sso.sendOtpCode(handshake.result.keyId, phoneNumber);
876
+ return {
877
+ success: !otpRes.hasError,
878
+ authorization: otpRes.authorization,
879
+ };
880
+ }
881
+
882
+ // Step 2: Verify OTP code & exchange for tokens
883
+ export async function verifyOtp(authorization: string, phoneNumber: string, code: string) {
884
+ const verify = await podSdk.sso.verifyOtpCode(authorization, phoneNumber, code);
885
+ if (verify.hasError || !verify.result?.code) {
886
+ throw new Error(verify.message || 'OTP verification failed');
887
+ }
888
+
889
+ const tokens = await podSdk.sso.generateToken(verify.result.code);
890
+ return tokens.result; // { access_token, refresh_token, expires_in }
891
+ }
892
+
893
+ // Step 3: Fetch profile with access token
894
+ export async function getProfile(accessToken: string) {
895
+ const profile = await podSdk.sso.getUserProfile(accessToken);
896
+ return profile.result;
897
+ }
898
+ ```
899
+
900
+ ---
901
+
902
+ ### 2. Notification (SMS Delivery)
903
+
904
+ ```typescript
905
+ // app/actions/notify.ts
906
+ 'use server';
907
+
908
+ import { podSdk } from '@/lib/pod';
909
+
910
+ export async function sendSms(phoneNumber: string, text: string) {
911
+ return podSdk.notification.sendSms({
912
+ receptor: phoneNumber,
913
+ message: text,
914
+ });
915
+ }
916
+ ```
917
+
918
+ ---
919
+
920
+ ### 3. Social (Comments & Likes)
921
+
922
+ ```typescript
923
+ // app/actions/social.ts
924
+ 'use server';
925
+
926
+ import { podSdk } from '@/lib/pod';
927
+
928
+ export async function addComment(postId: number, content: string) {
929
+ return podSdk.social.addComment({
930
+ postId,
931
+ text: content,
932
+ });
933
+ }
934
+
935
+ export async function likePost(postId: number) {
936
+ return podSdk.social.likePost(postId);
937
+ }
938
+ ```
939
+
940
+ ---
941
+
942
+ ### 4. IUMS (Identity & University Management Service)
943
+
944
+ ```typescript
945
+ // app/actions/iums.ts
946
+ 'use server';
947
+
948
+ import { podSdk } from '@/lib/pod';
949
+
950
+ export async function getStudentByNationalCode(nationalCode: string) {
951
+ return podSdk.iums.getStudentInformationByNationalCode(nationalCode);
952
+ }
953
+ ```
954
+
955
+ ---
956
+
957
+ ## Environment Variables Reference
958
+
959
+ ```env
960
+ # .env.local
961
+ POD_API_TOKEN=your_api_token_here
962
+ POD_CLIENT_ID=your_client_id_here
963
+ POD_CLIENT_SECRET=your_client_secret_here
964
+ POD_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
965
+ ```
966
+
967
+ ---
968
+
969
+ ## License
970
+
971
+ MIT © [Reza](https://github.com/rezamirzapour2)