@rezamirzapour/pod-sdk 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Reza
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,470 @@
1
+ # @rezamirzapour/pod-sdk
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**.
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 8 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 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.
17
+ - 📦 **Generic CRUD Repository for CustomPost**: Instantiate typed data repositories on POD CustomPost in 2 lines with `create`, `getById`, `getAll`, `createAndBindEntityId`, and `archive`.
18
+ - 🔐 **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
+ - 🚀 **Next.js App Router Ready**: Seamless support for Server Components, Server Actions, Route Handlers, and Client Components.
20
+ - 🌳 **Treeshakeable & Dual ESM/CJS**: Ships clean ES modules (`.mjs`) and CommonJS (`.js`) with 100% complete TypeScript declarations (`.d.ts`).
21
+
22
+ ---
23
+
24
+ ## Microservices Included
25
+
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. |
36
+
37
+ ---
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ npm install @rezamirzapour/pod-sdk @rezamirzapour/http @rezamirzapour/logger
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Quickstart
48
+
49
+ ### 1. Initialize the Client
50
+
51
+ ```typescript
52
+ // lib/pod.ts
53
+ import { createPodSdk } from '@rezamirzapour/pod-sdk';
54
+
55
+ export const podSdk = createPodSdk({
56
+ apiToken: process.env.POD_API_TOKEN,
57
+ clientId: process.env.POD_CLIENT_ID,
58
+ clientSecret: process.env.POD_CLIENT_SECRET,
59
+ privateKeyPem: process.env.POD_PRIVATE_KEY_PEM, // RSA Private Key for SSO OTP signing
60
+ revalidate: 3600, // Default ISR revalidation in seconds
61
+ urls: {
62
+ // Optional overrides (defaults to official POD endpoints)
63
+ accounts: 'https://accounts.pod.ir',
64
+ apiPod: 'https://api.pod.ir',
65
+ cms: 'https://cms.pod.ir',
66
+ podspace: 'https://podspace.pod.ir',
67
+ podform: 'https://podform.pod.ir',
68
+ notification: 'https://notification.pod.ir',
69
+ iums: 'https://iums.pod.ir',
70
+ },
71
+ });
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Deep Dive: CMS & Generic Data Formatting
77
+
78
+ 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
+
80
+ ### 1. Defining Types and Fetching CMS Articles with `getContent<T, P>`
81
+
82
+ ```tsx
83
+ // app/news/page.tsx (Server Component)
84
+ import { podSdk } from '@/lib/pod';
85
+
86
+ // 1. Define the formatted structure of your CMS content type
87
+ interface NewsArticle {
88
+ title: string;
89
+ coverImage: string; // Auto-transformed from hash to full Podspace CDN URL!
90
+ lead: string;
91
+ body: string;
92
+ publishDate: string;
93
+ }
94
+
95
+ // 2. Optional: Define normalized helper fields computed on the fly
96
+ interface NewsNormalized {
97
+ summary: string;
98
+ readTimeEstimate: number;
99
+ }
100
+
101
+ export default async function NewsPage() {
102
+ // Call getContent with exact generic parameters: <T, P>
103
+ const response = await podSdk.cms.getContent<NewsArticle, NewsNormalized>(
104
+ {
105
+ contentTypeUniqueId: 'company_news',
106
+ size: 10,
107
+ offset: 0,
108
+ },
109
+ {
110
+ // Optional: compute custom normalized fields
111
+ normalizer: (item) => ({
112
+ summary: item.formatted.lead || item.formatted.title,
113
+ readTimeEstimate: Math.ceil((item.formatted.body?.length || 0) / 500),
114
+ }),
115
+ // Next.js ISR options
116
+ revalidate: 1800,
117
+ }
118
+ );
119
+
120
+ return (
121
+ <div className="container mx-auto p-6">
122
+ <h1 className="text-2xl font-bold mb-4">Latest News</h1>
123
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
124
+ {response.result.map((item) => (
125
+ <article key={item.id} className="border rounded-lg p-4 shadow">
126
+ {/* item.formatted is 100% typed as NewsArticle */}
127
+ {item.formatted.coverImage && (
128
+ <img
129
+ src={item.formatted.coverImage}
130
+ alt={item.formatted.title}
131
+ className="w-full h-48 object-cover rounded"
132
+ />
133
+ )}
134
+ <h2 className="text-xl font-semibold mt-2">{item.formatted.title}</h2>
135
+ <p className="text-gray-600 mt-1">{item.__normalized.summary}</p>
136
+ <span className="text-xs text-gray-400">
137
+ Estimated read: {item.__normalized.readTimeEstimate} min
138
+ </span>
139
+ </article>
140
+ ))}
141
+ </div>
142
+ </div>
143
+ );
144
+ }
145
+ ```
146
+
147
+ ### 2. Fetching Single Content by Entity ID: `getContentByEntityId<T>`
148
+
149
+ ```typescript
150
+ // app/news/[id]/page.tsx
151
+ import { podSdk } from '@/lib/pod';
152
+ import { notFound } from 'next/navigation';
153
+
154
+ interface ArticleDetail {
155
+ title: string;
156
+ content: string;
157
+ author: string;
158
+ coverImage: string;
159
+ }
160
+
161
+ export default async function NewsDetailPage({ params }: { params: { id: string } }) {
162
+ const res = await podSdk.cms.getContentByEntityId<ArticleDetail>({
163
+ entityId: Number(params.id),
164
+ contentTypeUniqueId: 'company_news',
165
+ });
166
+
167
+ const article = res.result?.[0];
168
+ if (!article) notFound();
169
+
170
+ return (
171
+ <main>
172
+ <h1>{article.formatted.title}</h1>
173
+ <p>By {article.formatted.author}</p>
174
+ <div dangerouslySetInnerHTML={{ __html: article.formatted.content }} />
175
+ </main>
176
+ );
177
+ }
178
+ ```
179
+
180
+ ### 3. Adding and Editing CMS Content: `addContent<T>` & `editContent<T>`
181
+
182
+ ```typescript
183
+ // app/actions/cms.ts
184
+ 'use server';
185
+
186
+ import { podSdk } from '@/lib/pod';
187
+
188
+ export async function createNewsArticle(data: {
189
+ title: string;
190
+ body: string;
191
+ imageHash: string;
192
+ }) {
193
+ return podSdk.cms.addContent({
194
+ contentTypeUniqueId: 'company_news',
195
+ body: {
196
+ name: data.title,
197
+ fieldCode: ['title', 'body', 'coverImage'],
198
+ fieldValue: [data.title, data.body, data.imageHash],
199
+ canComment: true,
200
+ canLike: true,
201
+ enable: true,
202
+ isSearchable: true,
203
+ },
204
+ });
205
+ }
206
+ ```
207
+
208
+ ---
209
+
210
+ ## Deep Dive: CustomPost & Generic Typed Repositories
211
+
212
+ `CustomPost` allows persisting arbitrary JSON schemas on the POD Platform. `@rezamirzapour/pod-sdk` provides two complementary approaches:
213
+
214
+ 1. **Direct Service Method Calls with Generics (`searchTimelineByMetadata<T>`, `getCustomPost<T>`, `addCustomPost<T>`)**
215
+ 2. **High-Level Typed CRUD Repository (`CustomPostCrudService<DataType>`)**
216
+
217
+ ### 1. Direct Generic Method Usage
218
+
219
+ ```typescript
220
+ // lib/blog.ts
221
+ import { podSdk } from '@/lib/pod';
222
+
223
+ export interface BlogPostMeta {
224
+ slug: string;
225
+ title: string;
226
+ summary: string;
227
+ views: number;
228
+ publishedAt: string;
229
+ }
230
+
231
+ // 1. Search posts with typed metadata query
232
+ export async function getPublishedPosts() {
233
+ const response = await podSdk.customPost.searchTimelineByMetadata<BlogPostMeta>({
234
+ entityName: 'blog_post',
235
+ metadata: { views: 100 }, // Partial match or query
236
+ size: 20,
237
+ offset: 0,
238
+ });
239
+
240
+ // response.result[i].item.metadata is strongly typed as BlogPostMeta
241
+ return response.result.map((r) => r.item.metadata);
242
+ }
243
+
244
+ // 2. Add a new custom post with typed metadata
245
+ export async function createBlogPost(meta: BlogPostMeta) {
246
+ return podSdk.customPost.addCustomPost<BlogPostMeta>({
247
+ name: 'blog_post',
248
+ content: meta.title,
249
+ metadata: meta, // Strongly typed
250
+ enable: true,
251
+ });
252
+ }
253
+ ```
254
+
255
+ ### 2. High-Level Generic CRUD Repository (`createCrud<DataType>`)
256
+
257
+ Create an ORM-like typed repository for any entity type in seconds:
258
+
259
+ ```typescript
260
+ // lib/products.ts
261
+ import { podSdk } from '@/lib/pod';
262
+
263
+ export interface ProductData {
264
+ title: string;
265
+ sku: string;
266
+ price: number;
267
+ inStock: boolean;
268
+ category: string;
269
+ entityId?: number;
270
+ }
271
+
272
+ // Instantiate typed CRUD service for "product_item"
273
+ export const productCrud = podSdk.createCrud<ProductData>({
274
+ name: 'product_entity',
275
+ type: 'commerce',
276
+ detailedType: 'gadgets',
277
+ });
278
+ ```
279
+
280
+ All methods on `productCrud` are fully generic:
281
+
282
+ ```typescript
283
+ // app/actions/products.ts
284
+ 'use server';
285
+
286
+ import { productCrud } from '@/lib/products';
287
+ import { revalidatePath } from 'next/cache';
288
+
289
+ // 1. Fetch all products
290
+ export async function getProducts() {
291
+ const result = await productCrud.getAll();
292
+ return result.result.map((r) => r.item.metadata.data);
293
+ }
294
+
295
+ // 2. Get single product by numerical ID
296
+ export async function getProductById(id: number) {
297
+ const res = await productCrud.getById(id);
298
+ return res.result?.[0]?.metadata.data;
299
+ }
300
+
301
+ // 3. Create product and automatically bind entityId
302
+ export async function saveNewProduct(data: { title: string; sku: string; price: number }) {
303
+ const res = await productCrud.createAndBindEntityId({
304
+ title: data.title,
305
+ sku: data.sku,
306
+ price: data.price,
307
+ inStock: true,
308
+ category: 'electronics',
309
+ });
310
+
311
+ revalidatePath('/products');
312
+ return res;
313
+ }
314
+
315
+ // 4. Update product
316
+ export async function updateProduct(entityId: number, data: ProductData) {
317
+ return productCrud.update(entityId, data);
318
+ }
319
+
320
+ // 5. Archive (soft-disable without deleting)
321
+ export async function archiveProduct(entityId: number, data: ProductData) {
322
+ return productCrud.archive(entityId, data);
323
+ }
324
+
325
+ // 6. Delete permanently
326
+ export async function deleteProduct(entityId: number) {
327
+ return productCrud.delete(entityId);
328
+ }
329
+ ```
330
+
331
+ ---
332
+
333
+ ## Other POD Microservices
334
+
335
+ ### 3. POD SSO (OTP Handshake with Web Crypto RSA Signature)
336
+
337
+ ```typescript
338
+ // app/actions/auth.ts
339
+ 'use server';
340
+
341
+ import { podSdk } from '@/lib/pod';
342
+
343
+ // Step 1: Handshake and send OTP
344
+ export async function sendOtp(phoneNumber: string, clientIp: string) {
345
+ // Handshake to obtain keyId
346
+ const handshake = await podSdk.sso.handshake(`web-${Date.now()}`, clientIp);
347
+ if (handshake.hasError || !handshake.result?.keyId) {
348
+ throw new Error(handshake.message || 'SSO Handshake failed');
349
+ }
350
+
351
+ // Automatic RSA-SHA256 signature using Web Crypto API
352
+ const otpRes = await podSdk.sso.sendOtpCode(handshake.result.keyId, phoneNumber);
353
+ return {
354
+ success: !otpRes.hasError,
355
+ authorization: otpRes.authorization,
356
+ };
357
+ }
358
+
359
+ // Step 2: Verify OTP code & exchange for tokens
360
+ export async function verifyOtp(authorization: string, phoneNumber: string, code: string) {
361
+ const verify = await podSdk.sso.verifyOtpCode(authorization, phoneNumber, code);
362
+ if (verify.hasError || !verify.result?.code) {
363
+ throw new Error(verify.message || 'OTP verification failed');
364
+ }
365
+
366
+ // Exchange authorization code for JWT tokens
367
+ const tokens = await podSdk.sso.generateToken(verify.result.code);
368
+ return tokens.result; // { access_token, refresh_token, expires_in }
369
+ }
370
+
371
+ // Step 3: Fetch profile with access token
372
+ export async function getProfile(accessToken: string) {
373
+ const profile = await podSdk.sso.getUserProfile(accessToken);
374
+ return profile.result;
375
+ }
376
+ ```
377
+
378
+ ---
379
+
380
+ ### 4. Podspace (File Uploads & CDN URLs)
381
+
382
+ ```typescript
383
+ // app/actions/upload.ts
384
+ 'use server';
385
+
386
+ import { podSdk } from '@/lib/pod';
387
+
388
+ export async function uploadFile(formData: FormData) {
389
+ const res = await podSdk.podspace.uploadFile(formData, '/uploads', true);
390
+ if (res.hasError) {
391
+ throw new Error(res.message || 'Upload failed');
392
+ }
393
+
394
+ const publicUrl = podSdk.podspace.getFileUrl(res.result.hash, true);
395
+ return { url: publicUrl, hash: res.result.hash };
396
+ }
397
+ ```
398
+
399
+ ---
400
+
401
+ ### 5. Notification (SMS Delivery)
402
+
403
+ ```typescript
404
+ // app/actions/notify.ts
405
+ 'use server';
406
+
407
+ import { podSdk } from '@/lib/pod';
408
+
409
+ export async function sendSms(phoneNumber: string, text: string) {
410
+ return podSdk.notification.sendSms({
411
+ receptor: phoneNumber,
412
+ message: text,
413
+ });
414
+ }
415
+ ```
416
+
417
+ ---
418
+
419
+ ### 6. Social (Comments & Likes)
420
+
421
+ ```typescript
422
+ // app/actions/social.ts
423
+ 'use server';
424
+
425
+ import { podSdk } from '@/lib/pod';
426
+
427
+ export async function addComment(postId: number, content: string) {
428
+ return podSdk.social.addComment({
429
+ postId,
430
+ text: content,
431
+ });
432
+ }
433
+
434
+ export async function likePost(postId: number) {
435
+ return podSdk.social.likePost(postId);
436
+ }
437
+ ```
438
+
439
+ ---
440
+
441
+ ### 7. IUMS (Identity & University Management Service)
442
+
443
+ ```typescript
444
+ // app/actions/iums.ts
445
+ 'use server';
446
+
447
+ import { podSdk } from '@/lib/pod';
448
+
449
+ export async function getStudentByNationalCode(nationalCode: string) {
450
+ return podSdk.iums.getStudentInformationByNationalCode(nationalCode);
451
+ }
452
+ ```
453
+
454
+ ---
455
+
456
+ ## Environment Variables Reference
457
+
458
+ ```env
459
+ # .env.local
460
+ POD_API_TOKEN=your_api_token_here
461
+ POD_CLIENT_ID=your_client_id_here
462
+ POD_CLIENT_SECRET=your_client_secret_here
463
+ POD_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
464
+ ```
465
+
466
+ ---
467
+
468
+ ## License
469
+
470
+ MIT © [Reza](https://github.com/rezamirzapour2)