malawi-curriculum-api 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -7
- package/package.json +1 -1
- package/src/MalawiCurriculumClient.js +135 -14
package/README.md
CHANGED
|
@@ -15,20 +15,89 @@ The official SDK for accessing the Malawi Curriculum API.
|
|
|
15
15
|
npm install malawi-curriculum-api
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
### Authentication
|
|
19
|
+
|
|
20
|
+
Initialize the client with your API Key and optional Firebase ID Token for user-specific access (subscriptions/purchases).
|
|
19
21
|
|
|
20
22
|
```javascript
|
|
21
23
|
import { MalawiCurriculumClient } from 'malawi-curriculum-api';
|
|
22
24
|
|
|
23
|
-
const client = new MalawiCurriculumClient({
|
|
24
|
-
apiKey: 'YOUR_API_KEY'
|
|
25
|
+
const client = new MalawiCurriculumClient({
|
|
26
|
+
apiKey: 'YOUR_API_KEY',
|
|
27
|
+
firebaseToken: 'USER_FIREBASE_ID_TOKEN' // Optional: for accessing paid resources
|
|
25
28
|
});
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
// Update token later (e.g., on user login/refresh)
|
|
31
|
+
client.setFirebaseToken('NEW_TOKEN');
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Search Resources
|
|
35
|
+
|
|
36
|
+
Search with powerful filtering options. Access to filters depends on your API Key plan tier.
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
const results = await client.search({
|
|
40
|
+
q: 'mathematics',
|
|
41
|
+
level: 'MSCE', // Basic+ Plan
|
|
42
|
+
year: 2023, // Pro+ Plan
|
|
43
|
+
type: 'past_paper', // Pro+ Plan
|
|
44
|
+
limit: 10
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Secure Downloads
|
|
49
|
+
|
|
50
|
+
Request a secure, short-lived download URL. This flow enforces entitlements (subscriptions/purchases).
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
try {
|
|
54
|
+
// 1. Get signed URL (handles payment checks automatically)
|
|
55
|
+
const url = await client.download(123, 'download');
|
|
56
|
+
|
|
57
|
+
// 2. Open or download
|
|
58
|
+
window.location.href = url;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error.message.includes('402')) {
|
|
61
|
+
console.error('Payment Required');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Payments
|
|
67
|
+
|
|
68
|
+
Initiate mobile money payments for subscriptions or resource purchases.
|
|
69
|
+
|
|
70
|
+
#### Developer Subscription (Pay Platform)
|
|
71
|
+
```javascript
|
|
72
|
+
const response = await client.initiatePayment({
|
|
73
|
+
type: 'subscription',
|
|
74
|
+
tier: 'pro',
|
|
75
|
+
mobile: '26599123456',
|
|
76
|
+
operator: 'AIRTEL', // or 'TNM'
|
|
77
|
+
email: 'dev@example.com'
|
|
31
78
|
});
|
|
79
|
+
console.log('Charge ID:', response.charge_id);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### Purchase Resource (User pays Developer)
|
|
83
|
+
```javascript
|
|
84
|
+
const response = await client.initiatePayment({
|
|
85
|
+
type: 'purchase',
|
|
86
|
+
developerId: 'DEV_UUID',
|
|
87
|
+
resourceId: 123,
|
|
88
|
+
amount: 500,
|
|
89
|
+
mobile: '26588123456',
|
|
90
|
+
operator: 'TNM',
|
|
91
|
+
email: 'user@example.com'
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### Verify Payment
|
|
96
|
+
```javascript
|
|
97
|
+
const status = await client.verifyPayment(chargeId);
|
|
98
|
+
if (status.data.status === 'successful') {
|
|
99
|
+
console.log('Payment Successful');
|
|
100
|
+
}
|
|
32
101
|
```
|
|
33
102
|
|
|
34
103
|
## API Overview
|
|
@@ -328,6 +397,107 @@ When a resource has been priced (not free), download requests return `402 Paymen
|
|
|
328
397
|
|
|
329
398
|
---
|
|
330
399
|
|
|
400
|
+
### Related Resources
|
|
401
|
+
|
|
402
|
+
Get resources similar to a given resource (same subject/level), prioritising the same year.
|
|
403
|
+
|
|
404
|
+
```javascript
|
|
405
|
+
const related = await client.getRelatedResources(101);
|
|
406
|
+
console.log(related);
|
|
407
|
+
// Up to 5 resources from the same subject and level
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**Request:**
|
|
411
|
+
- Method: `GET`
|
|
412
|
+
- Endpoint: `/resources/{id}/related`
|
|
413
|
+
- Headers: `Authorization: Bearer API_KEY`
|
|
414
|
+
|
|
415
|
+
**Response:**
|
|
416
|
+
```json
|
|
417
|
+
{
|
|
418
|
+
"success": true,
|
|
419
|
+
"data": [
|
|
420
|
+
{
|
|
421
|
+
"id": 102,
|
|
422
|
+
"title": "MSCE Mathematics Paper 2 2023",
|
|
423
|
+
"type": "past_paper",
|
|
424
|
+
"year": 2023,
|
|
425
|
+
"subject": "Mathematics",
|
|
426
|
+
"level": "MSCE",
|
|
427
|
+
"price_mwk": 500,
|
|
428
|
+
"is_free": false
|
|
429
|
+
}
|
|
430
|
+
]
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
### Bookmarks
|
|
437
|
+
|
|
438
|
+
Save resources for quick access later.
|
|
439
|
+
|
|
440
|
+
#### List Bookmarks
|
|
441
|
+
|
|
442
|
+
```javascript
|
|
443
|
+
const bookmarks = await client.getBookmarks({ limit: 20 });
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
**Request:**
|
|
447
|
+
- Method: `GET`
|
|
448
|
+
- Endpoint: `/bookmarks`
|
|
449
|
+
- Headers: `Authorization: Bearer API_KEY`
|
|
450
|
+
- Query Parameters:
|
|
451
|
+
- `limit` (optional): Max results (1-100, default: 50)
|
|
452
|
+
- `offset` (optional): Pagination offset
|
|
453
|
+
|
|
454
|
+
**Response:**
|
|
455
|
+
```json
|
|
456
|
+
{
|
|
457
|
+
"success": true,
|
|
458
|
+
"count": 1,
|
|
459
|
+
"data": [
|
|
460
|
+
{
|
|
461
|
+
"id": 1,
|
|
462
|
+
"resource_id": 101,
|
|
463
|
+
"title": "MSCE Mathematics Paper 1 2023",
|
|
464
|
+
"type": "past_paper",
|
|
465
|
+
"year": 2023,
|
|
466
|
+
"subject": "Mathematics",
|
|
467
|
+
"level": "MSCE",
|
|
468
|
+
"price_mwk": 500,
|
|
469
|
+
"is_free": false,
|
|
470
|
+
"bookmarked_at": "2026-02-14T00:00:00.000Z"
|
|
471
|
+
}
|
|
472
|
+
]
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
#### Add Bookmark
|
|
477
|
+
|
|
478
|
+
```javascript
|
|
479
|
+
const bookmark = await client.addBookmark(101);
|
|
480
|
+
console.log(bookmark);
|
|
481
|
+
// { id: 1, resource_id: 101, created_at: "..." }
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
**Request:**
|
|
485
|
+
- Method: `POST`
|
|
486
|
+
- Endpoint: `/bookmarks`
|
|
487
|
+
- Body: `{ "resource_id": 101 }`
|
|
488
|
+
|
|
489
|
+
#### Remove Bookmark
|
|
490
|
+
|
|
491
|
+
```javascript
|
|
492
|
+
await client.removeBookmark(101);
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
**Request:**
|
|
496
|
+
- Method: `DELETE`
|
|
497
|
+
- Endpoint: `/bookmarks/{resourceId}`
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
331
501
|
### Search Resources
|
|
332
502
|
|
|
333
503
|
Search across all curriculum resources. Results and filter capabilities depend on your plan tier.
|
package/package.json
CHANGED
|
@@ -26,13 +26,23 @@ export class MalawiCurriculumClient {
|
|
|
26
26
|
/**
|
|
27
27
|
* @param {Object} config
|
|
28
28
|
* @param {string} config.apiKey - Your API Key
|
|
29
|
-
* @param {string} [config.baseUrl] - API Base URL
|
|
29
|
+
* @param {string} [config.baseUrl] - API Base URL
|
|
30
|
+
* @param {string} [config.firebaseToken] - Firebase ID Token (for paid resource access)
|
|
30
31
|
*/
|
|
31
|
-
constructor({ apiKey, baseUrl }) {
|
|
32
|
+
constructor({ apiKey, baseUrl, firebaseToken }) {
|
|
32
33
|
if (!apiKey) throw new Error('API Key is required');
|
|
33
34
|
|
|
34
35
|
this.apiKey = apiKey;
|
|
35
36
|
this.baseUrl = baseUrl || 'https://malawi-curricular-api-production.up.railway.app/api/v1';
|
|
37
|
+
this.firebaseToken = firebaseToken;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Set or update the Firebase ID Token
|
|
42
|
+
* @param {string} token
|
|
43
|
+
*/
|
|
44
|
+
setFirebaseToken(token) {
|
|
45
|
+
this.firebaseToken = token;
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
/**
|
|
@@ -49,14 +59,17 @@ export class MalawiCurriculumClient {
|
|
|
49
59
|
});
|
|
50
60
|
|
|
51
61
|
const url = `${this.baseUrl}${endpoint}?${params.toString()}`;
|
|
62
|
+
const headers = {
|
|
63
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
64
|
+
'Content-Type': 'application/json'
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (this.firebaseToken) {
|
|
68
|
+
headers['X-Firebase-Token'] = this.firebaseToken;
|
|
69
|
+
}
|
|
52
70
|
|
|
53
71
|
try {
|
|
54
|
-
const response = await fetch(url, {
|
|
55
|
-
headers: {
|
|
56
|
-
'Authorization': `Bearer ${this.apiKey}`,
|
|
57
|
-
'Content-Type': 'application/json'
|
|
58
|
-
}
|
|
59
|
-
});
|
|
72
|
+
const response = await fetch(url, { headers });
|
|
60
73
|
|
|
61
74
|
if (!response.ok) {
|
|
62
75
|
const errorText = await response.text();
|
|
@@ -77,14 +90,19 @@ export class MalawiCurriculumClient {
|
|
|
77
90
|
*/
|
|
78
91
|
async _post(endpoint, body = {}) {
|
|
79
92
|
const url = `${this.baseUrl}${endpoint}`;
|
|
93
|
+
const headers = {
|
|
94
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
95
|
+
'Content-Type': 'application/json'
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (this.firebaseToken) {
|
|
99
|
+
headers['X-Firebase-Token'] = this.firebaseToken;
|
|
100
|
+
}
|
|
80
101
|
|
|
81
102
|
try {
|
|
82
103
|
const response = await fetch(url, {
|
|
83
104
|
method: 'POST',
|
|
84
|
-
headers
|
|
85
|
-
'Authorization': `Bearer ${this.apiKey}`,
|
|
86
|
-
'Content-Type': 'application/json'
|
|
87
|
-
},
|
|
105
|
+
headers,
|
|
88
106
|
body: JSON.stringify(body)
|
|
89
107
|
});
|
|
90
108
|
|
|
@@ -184,8 +202,11 @@ export class MalawiCurriculumClient {
|
|
|
184
202
|
*/
|
|
185
203
|
async downloadResource(id) {
|
|
186
204
|
console.warn('[DEPRECATED] downloadResource() is deprecated. Use download() for the secure token flow.');
|
|
187
|
-
|
|
188
|
-
|
|
205
|
+
// This old endpoint is gone/legacy, but for SDK compat we redirect to new flow if possible or just fail?
|
|
206
|
+
// Since strict mode is on, we should probably advise new flow.
|
|
207
|
+
// But let's try to map it to new flow if possible?
|
|
208
|
+
// "downloadResource(id)" implies purpose=download.
|
|
209
|
+
return this.download(id, 'download');
|
|
189
210
|
}
|
|
190
211
|
|
|
191
212
|
/**
|
|
@@ -210,4 +231,104 @@ export class MalawiCurriculumClient {
|
|
|
210
231
|
const response = await this._request(`/pricing/${resourceId}`);
|
|
211
232
|
return response.data;
|
|
212
233
|
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Helper to make authenticated DELETE requests
|
|
237
|
+
* @private
|
|
238
|
+
*/
|
|
239
|
+
async _delete(endpoint) {
|
|
240
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
241
|
+
const headers = {
|
|
242
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
243
|
+
'Content-Type': 'application/json'
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
if (this.firebaseToken) {
|
|
247
|
+
headers['X-Firebase-Token'] = this.firebaseToken;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const response = await fetch(url, {
|
|
252
|
+
method: 'DELETE',
|
|
253
|
+
headers
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (!response.ok) {
|
|
257
|
+
const errorText = await response.text();
|
|
258
|
+
throw new Error(`API Error ${response.status}: ${errorText}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return await response.json();
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (error.cause) console.error('Network Error Cause:', error.cause);
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Initiate a payment (Subscription or Purchase).
|
|
270
|
+
* @param {Object} data - Payment details
|
|
271
|
+
* @param {'subscription'|'purchase'} data.type - Payment type
|
|
272
|
+
* @param {string} data.mobile - Mobile number
|
|
273
|
+
* @param {'TNM'|'AIRTEL'} data.operator - Mobile operator
|
|
274
|
+
* @param {string} data.email - Email address
|
|
275
|
+
* @param {string} [data.tier] - Subscription tier (for type='subscription')
|
|
276
|
+
* @param {string} [data.developerId] - Developer ID (for type='purchase')
|
|
277
|
+
* @param {number} [data.resourceId] - Resource ID (for type='purchase')
|
|
278
|
+
* @param {number} [data.amount] - Amount (for type='purchase')
|
|
279
|
+
* @returns {Promise<Object>} - { success, charge_id, status, message }
|
|
280
|
+
*/
|
|
281
|
+
async initiatePayment(data) {
|
|
282
|
+
return this._post('/payments/initiate', data);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Verify a payment status.
|
|
287
|
+
* @param {string} chargeId - The charge ID returned from initiatePayment
|
|
288
|
+
* @returns {Promise<Object>} - { success, status, data }
|
|
289
|
+
*/
|
|
290
|
+
async verifyPayment(chargeId) {
|
|
291
|
+
return this._get(`/payments/verify/${chargeId}`, {}, (data) => data);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get related resources for a given resource
|
|
296
|
+
* @param {number} resourceId - ID of the source resource
|
|
297
|
+
* @returns {Promise<Resource[]>} Up to 5 related resources
|
|
298
|
+
*/
|
|
299
|
+
async getRelatedResources(resourceId) {
|
|
300
|
+
const response = await this._request(`/resources/${resourceId}/related`);
|
|
301
|
+
return response.data || [];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* List bookmarked resources
|
|
306
|
+
* @param {Object} [options]
|
|
307
|
+
* @param {number} [options.limit] - Max results (1-100)
|
|
308
|
+
* @param {number} [options.offset] - Pagination offset
|
|
309
|
+
* @returns {Promise<Object[]>} Bookmarked resources with metadata
|
|
310
|
+
*/
|
|
311
|
+
async getBookmarks(options = {}) {
|
|
312
|
+
const response = await this._request('/bookmarks', options);
|
|
313
|
+
return response.data || [];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Bookmark a resource
|
|
318
|
+
* @param {number} resourceId - ID of the resource to bookmark
|
|
319
|
+
* @returns {Promise<Object>} Bookmark record
|
|
320
|
+
*/
|
|
321
|
+
async addBookmark(resourceId) {
|
|
322
|
+
const response = await this._post('/bookmarks', { resource_id: resourceId });
|
|
323
|
+
return response.data;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Remove a bookmark
|
|
328
|
+
* @param {number} resourceId - ID of the resource to un-bookmark
|
|
329
|
+
* @returns {Promise<Object>} Confirmation
|
|
330
|
+
*/
|
|
331
|
+
async removeBookmark(resourceId) {
|
|
332
|
+
return await this._delete(`/bookmarks/${resourceId}`);
|
|
333
|
+
}
|
|
213
334
|
}
|