@rezamirzapour/pod-sdk 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @rezamirzapour/pod-sdk
2
2
 
3
- > Unified, type-safe, enterprise-grade SDK for **POD Platform** microservices (SSO, CustomPost, Podspace, Podform, Notification, Social, CMS, IUMS), engineered specifically for **Next.js App Router**, **Server Components**, and **Server Actions**.
3
+ > Unified, type-safe, enterprise-grade SDK for **POD Platform** microservices (CMS Content, CMS Product, SSO, CustomPost, Podspace, Podform, Notification, Social, IUMS), engineered specifically for **Next.js App Router**, **Server Components**, and **Server Actions**.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@rezamirzapour/pod-sdk.svg)](https://www.npmjs.com/package/@rezamirzapour/pod-sdk)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
@@ -10,10 +10,11 @@
10
10
 
11
11
  ## Features
12
12
 
13
- - 🌐 **Unified Multi-Service Architecture**: Single entry-point managing 8 POD microservices with official endpoints and configurations.
13
+ - 🌐 **Unified Multi-Service Architecture**: Single entry-point managing official POD microservices with official endpoints and configurations.
14
14
  - ⚡ **Powered by `@rezamirzapour/http`**: Automatic exponential backoff retries, Next.js incremental static regeneration (ISR) caching (`next.revalidate`, tags), and isomorphic execution.
15
- - 🧬 **First-Class TypeScript Generics (`<T, P>`)**: CMS and CustomPost endpoints allow you to define and receive strongly typed metadata structures according to your business schemas.
16
- - 🎨 **Automatic CMS Data Formatter**: Automatically translates POD CMS nested `metadata.content` arrays into flat, strongly typed objects (`formatted[fieldCode]`) and resolves image hashes to CDN URLs.
15
+ - 🧬 **First-Class TypeScript Generics (`<T, P>`)**: CMS Content, CMS Product, and CustomPost endpoints allow you to define and receive strongly typed metadata structures according to your business schemas.
16
+ - 🎨 **Automatic CMS Data Formatter**: Automatically translates nested `metadata.content` and `metadata.product` arrays into flat, strongly typed objects (`formatted[fieldCode]`) and resolves image hashes to Podspace CDN URLs.
17
+ - 🛍️ **Full CMS Product Module (RAD API Swagger)**: Complete support for published products, price/discount filters, barcodes, batch publish/unpublish, archiving, and AI timeline search.
17
18
  - 📦 **Generic CRUD Repository for CustomPost**: Instantiate typed data repositories on POD CustomPost in 2 lines with `create`, `getById`, `getAll`, `createAndBindEntityId`, and `archive`.
18
19
  - 🔐 **Zero-Dependency Web Crypto RSA Signing**: Built-in RSA-SHA256 signature calculation for POD SSO OTP handshakes using standard Web Crypto API (Node.js, Edge Runtime, Browsers).
19
20
  - 🚀 **Next.js App Router Ready**: Seamless support for Server Components, Server Actions, Route Handlers, and Client Components.
@@ -23,16 +24,17 @@
23
24
 
24
25
  ## Microservices Included
25
26
 
26
- | Service | Property | Description |
27
- | :--- | :--- | :--- |
28
- | **CMS** | `sdk.cms` | Fetch, publish, edit, and categorize CMS articles with generic metadata `<T, P>` and automatic image formatting. |
29
- | **CustomPost** | `sdk.customPost` | Search timeline by metadata `<T>`, custom post CRUD, and high-level typed repository. |
30
- | **SSO** | `sdk.sso` | OAuth2 handshake, OTP dispatch with digital RSA signature, OTP verify, token generation, and user profile. |
31
- | **Podspace** | `sdk.podspace` | File upload (FormData) and public/private download URL resolution. |
32
- | **Podform** | `sdk.podform` | Survey & form response submission, question/form structure retrieval. |
33
- | **Notification** | `sdk.notification` | SMS delivery and bulk messaging with tracking. |
34
- | **Social** | `sdk.social` | User comments, reactions (likes/dislikes), rates, and social post interactions. |
35
- | **IUMS** | `sdk.iums` | Student and user identity inspection by national code or student ID. |
27
+ | Service | Accessor | Swagger Tag | Description |
28
+ | :--- | :--- | :--- | :--- |
29
+ | **CMS Content** | `sdk.cms` | `content` | Fetch, publish, edit, draft, archive, and categorize CMS articles with generic metadata `<T, P>`. |
30
+ | **CMS Product** | `sdk.product` / `sdk.cms.products` | `product` | Product catalog, price & discount range filters, barcode lookup, batch publish, and AI search. |
31
+ | **CustomPost** | `sdk.customPost` | - | Search timeline by metadata `<T>`, custom post CRUD, and high-level typed repository. |
32
+ | **SSO** | `sdk.sso` | - | OAuth2 handshake, OTP dispatch with digital RSA signature, OTP verify, token generation, and user profile. |
33
+ | **Podspace** | `sdk.podspace` | - | File upload (FormData) and public/private download URL resolution. |
34
+ | **Podform** | `sdk.podform` | - | Survey & form response submission, question/form structure retrieval. |
35
+ | **Notification** | `sdk.notification` | - | SMS delivery and bulk messaging with tracking. |
36
+ | **Social** | `sdk.social` | - | User comments, reactions (likes/dislikes), rates, and social post interactions. |
37
+ | **IUMS** | `sdk.iums` | - | Student and user identity inspection by national code or student ID. |
36
38
 
37
39
  ---
38
40
 
@@ -73,10 +75,13 @@ export const podSdk = createPodSdk({
73
75
 
74
76
  ---
75
77
 
76
- ## Deep Dive: CMS & Generic Data Formatting
78
+ ## Deep Dive 1: CMS Content & Data Formatting
77
79
 
78
80
  POD CMS stores dynamic fields inside a `metadata.content` array of `{ code, value, type }` objects. `@rezamirzapour/pod-sdk` automatically formats these into clean typed key-value pairs (`item.formatted`) and allows an optional custom `normalizer` function (`item.__normalized`).
79
81
 
82
+ > [!NOTE]
83
+ > `getContent2` has been removed. For authenticated/management content retrieval (using `Access-Token`), call `podSdk.cms.getAllContents()` or pass headers to `getContent()`.
84
+
80
85
  ### 🏗️ Clean Architecture Pattern: Dedicated Data Access Layer (`NewsApi`)
81
86
 
82
87
  In enterprise Next.js applications, avoid calling SDK clients directly inside UI components. Instead, encapsulate CMS queries, generics, normalizers, and caching logic inside a dedicated Data Access Layer (`services/newsApi.ts`).
@@ -221,12 +226,8 @@ export class NewsApi {
221
226
  }
222
227
  ```
223
228
 
224
- ---
225
-
226
229
  #### Step 2: Consume `NewsApi` in Next.js Server Components (`app/news/page.tsx`)
227
230
 
228
- Notice how clean the UI layer becomes! No low-level SDK parameters, headers, or raw hashes leaked into your views:
229
-
230
231
  ```tsx
231
232
  // app/news/page.tsx (Server Component)
232
233
  import { NewsApi } from '@/services/newsApi';
@@ -276,81 +277,221 @@ export default async function NewsPage({ searchParams }: NewsPageProps) {
276
277
 
277
278
  ---
278
279
 
279
- #### Step 3: Detail Page with `NewsApi.getNewsById` (`app/news/[id]/page.tsx`)
280
+ ## Deep Dive 2: CMS Product Module (RAD API Swagger)
280
281
 
281
- ```tsx
282
- // app/news/[id]/page.tsx (Server Component)
283
- import { NewsApi } from '@/services/newsApi';
284
- import { notFound } from 'next/navigation';
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.
285
283
 
286
- interface NewsDetailPageProps {
287
- params: { id: string };
288
- }
284
+ ### Accessing Products
289
285
 
290
- export default async function NewsDetailPage({ params }: NewsDetailPageProps) {
291
- const news = await NewsApi.getNewsById(Number(params.id));
286
+ You can access product methods either via `podSdk.product` or `podSdk.cms.products`:
292
287
 
293
- if (!news) {
294
- notFound();
295
- }
288
+ ```typescript
289
+ // Both point to the same CmsProductService instance
290
+ podSdk.product.getProducts(...);
291
+ podSdk.cms.products.getProducts(...);
292
+ ```
296
293
 
297
- return (
298
- <article className="max-w-3xl mx-auto py-10 px-4">
299
- {news.coverImage && (
300
- <img
301
- src={news.coverImage}
302
- alt={news.title}
303
- className="w-full h-72 object-cover rounded-xl mb-6"
304
- />
305
- )}
306
- <h1 className="text-3xl font-bold mb-3">{news.title}</h1>
307
- <p className="text-sm text-gray-500 mb-6">{news.readTime} min read</p>
308
- <div className="prose lg:prose-lg" dangerouslySetInnerHTML={{ __html: news.body }} />
309
- </article>
310
- );
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
311
306
  }
312
- ```
313
307
 
314
- ---
308
+ // 2. Computed / normalized presentation fields (P)
309
+ export interface LaptopNormalized {
310
+ finalPrice: number;
311
+ discountBadge: string;
312
+ }
315
313
 
316
- #### Step 4: Server Action for CMS Publishing (`app/actions/news.ts`)
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
+ }
317
328
 
318
- ```typescript
319
- // app/actions/news.ts
320
- 'use server';
329
+ export class ProductsApi {
330
+ private static readonly PRODUCT_TYPE = 'laptops';
321
331
 
322
- import { NewsApi } from '@/services/newsApi';
323
- import { revalidatePath } from 'next/cache';
324
-
325
- export async function publishArticle(formData: FormData) {
326
- const title = formData.get('title') as string;
327
- const lead = formData.get('lead') as string;
328
- const body = formData.get('body') as string;
329
- const imageHash = formData.get('imageHash') as string;
330
-
331
- const result = await NewsApi.publishNews({
332
- title,
333
- lead,
334
- body,
335
- imageHash,
336
- });
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;
337
344
 
338
- revalidatePath('/news');
339
- return { success: !result.hasError };
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
+ }
340
425
  }
341
426
  ```
342
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
+
343
486
  ---
344
487
 
345
- ## Deep Dive: CustomPost & Generic Typed Repositories
488
+ ## Deep Dive 3: CustomPost & Generic Typed Repositories
346
489
 
347
490
  `CustomPost` allows persisting arbitrary JSON schemas on the POD Platform. `@rezamirzapour/pod-sdk` provides two complementary approaches:
348
491
 
349
492
  1. **Direct Service Method Calls with Generics (`searchTimelineByMetadata<T>`, `getCustomPost<T>`, `addCustomPost<T>`)**
350
493
  2. **High-Level Typed CRUD Repository (`CustomPostCrudService<DataType>`)**
351
494
 
352
- ### 1. Direct Generic Method Usage
353
-
354
495
  ```typescript
355
496
  // lib/blog.ts
356
497
  import { podSdk } from '@/lib/pod';
@@ -360,114 +501,26 @@ export interface BlogPostMeta {
360
501
  title: string;
361
502
  summary: string;
362
503
  views: number;
363
- publishedAt: string;
364
504
  }
365
505
 
366
- // 1. Search posts with typed metadata query
506
+ // Search posts with typed metadata query
367
507
  export async function getPublishedPosts() {
368
508
  const response = await podSdk.customPost.searchTimelineByMetadata<BlogPostMeta>({
369
509
  entityName: 'blog_post',
370
- metadata: { views: 100 }, // Partial match or query
510
+ metadata: { views: 100 },
371
511
  size: 20,
372
512
  offset: 0,
373
513
  });
374
514
 
375
- // response.result[i].item.metadata is strongly typed as BlogPostMeta
376
515
  return response.result.map((r) => r.item.metadata);
377
516
  }
378
-
379
- // 2. Add a new custom post with typed metadata
380
- export async function createBlogPost(meta: BlogPostMeta) {
381
- return podSdk.customPost.addCustomPost<BlogPostMeta>({
382
- name: 'blog_post',
383
- content: meta.title,
384
- metadata: meta, // Strongly typed
385
- enable: true,
386
- });
387
- }
388
- ```
389
-
390
- ### 2. High-Level Generic CRUD Repository (`createCrud<DataType>`)
391
-
392
- Create an ORM-like typed repository for any entity type in seconds:
393
-
394
- ```typescript
395
- // lib/products.ts
396
- import { podSdk } from '@/lib/pod';
397
-
398
- export interface ProductData {
399
- title: string;
400
- sku: string;
401
- price: number;
402
- inStock: boolean;
403
- category: string;
404
- entityId?: number;
405
- }
406
-
407
- // Instantiate typed CRUD service for "product_item"
408
- export const productCrud = podSdk.createCrud<ProductData>({
409
- name: 'product_entity',
410
- type: 'commerce',
411
- detailedType: 'gadgets',
412
- });
413
- ```
414
-
415
- All methods on `productCrud` are fully generic:
416
-
417
- ```typescript
418
- // app/actions/products.ts
419
- 'use server';
420
-
421
- import { productCrud } from '@/lib/products';
422
- import { revalidatePath } from 'next/cache';
423
-
424
- // 1. Fetch all products
425
- export async function getProducts() {
426
- const result = await productCrud.getAll();
427
- return result.result.map((r) => r.item.metadata.data);
428
- }
429
-
430
- // 2. Get single product by numerical ID
431
- export async function getProductById(id: number) {
432
- const res = await productCrud.getById(id);
433
- return res.result?.[0]?.metadata.data;
434
- }
435
-
436
- // 3. Create product and automatically bind entityId
437
- export async function saveNewProduct(data: { title: string; sku: string; price: number }) {
438
- const res = await productCrud.createAndBindEntityId({
439
- title: data.title,
440
- sku: data.sku,
441
- price: data.price,
442
- inStock: true,
443
- category: 'electronics',
444
- });
445
-
446
- revalidatePath('/products');
447
- return res;
448
- }
449
-
450
- // 4. Update product
451
- export async function updateProduct(entityId: number, data: ProductData) {
452
- return productCrud.update(entityId, data);
453
- }
454
-
455
- // 5. Archive (soft-disable without deleting)
456
- export async function archiveProduct(entityId: number, data: ProductData) {
457
- return productCrud.archive(entityId, data);
458
- }
459
-
460
- // 6. Delete permanently
461
- export async function deleteProduct(entityId: number) {
462
- return productCrud.delete(entityId);
463
- }
464
517
  ```
465
518
 
466
519
  ---
467
520
 
468
521
  ## Other POD Microservices
469
522
 
470
- ### 3. POD SSO (OTP Handshake with Web Crypto RSA Signature)
523
+ ### 4. POD SSO (OTP Handshake with Web Crypto RSA Signature)
471
524
 
472
525
  ```typescript
473
526
  // app/actions/auth.ts
@@ -477,7 +530,6 @@ import { podSdk } from '@/lib/pod';
477
530
 
478
531
  // Step 1: Handshake and send OTP
479
532
  export async function sendOtp(phoneNumber: string, clientIp: string) {
480
- // Handshake to obtain keyId
481
533
  const handshake = await podSdk.sso.handshake(`web-${Date.now()}`, clientIp);
482
534
  if (handshake.hasError || !handshake.result?.keyId) {
483
535
  throw new Error(handshake.message || 'SSO Handshake failed');
@@ -498,7 +550,6 @@ export async function verifyOtp(authorization: string, phoneNumber: string, code
498
550
  throw new Error(verify.message || 'OTP verification failed');
499
551
  }
500
552
 
501
- // Exchange authorization code for JWT tokens
502
553
  const tokens = await podSdk.sso.generateToken(verify.result.code);
503
554
  return tokens.result; // { access_token, refresh_token, expires_in }
504
555
  }
@@ -512,7 +563,7 @@ export async function getProfile(accessToken: string) {
512
563
 
513
564
  ---
514
565
 
515
- ### 4. Podspace (File Uploads & CDN URLs)
566
+ ### 5. Podspace (File Uploads & CDN URLs)
516
567
 
517
568
  ```typescript
518
569
  // app/actions/upload.ts
@@ -533,7 +584,7 @@ export async function uploadFile(formData: FormData) {
533
584
 
534
585
  ---
535
586
 
536
- ### 5. Notification (SMS Delivery)
587
+ ### 6. Notification (SMS Delivery)
537
588
 
538
589
  ```typescript
539
590
  // app/actions/notify.ts
@@ -551,7 +602,7 @@ export async function sendSms(phoneNumber: string, text: string) {
551
602
 
552
603
  ---
553
604
 
554
- ### 6. Social (Comments & Likes)
605
+ ### 7. Social (Comments & Likes)
555
606
 
556
607
  ```typescript
557
608
  // app/actions/social.ts
@@ -573,7 +624,7 @@ export async function likePost(postId: number) {
573
624
 
574
625
  ---
575
626
 
576
- ### 7. IUMS (Identity & University Management Service)
627
+ ### 8. IUMS (Identity & University Management Service)
577
628
 
578
629
  ```typescript
579
630
  // app/actions/iums.ts