malawi-curriculum-api 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 +424 -0
- package/package.json +22 -0
- package/src/MalawiCurriculumClient.js +209 -0
- package/test-sdk.js +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# Malawi Curriculum API - JavaScript SDK
|
|
2
|
+
|
|
3
|
+
A simple, typed client for accessing the Malawi Curriculum API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install malawi-curriculum-api
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
import { MalawiCurriculumClient } from 'malawi-curriculum-api';
|
|
15
|
+
|
|
16
|
+
const client = new MalawiCurriculumClient({
|
|
17
|
+
apiKey: 'YOUR_API_KEY'
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const resources = await client.getResources({
|
|
21
|
+
level: 'MSCE',
|
|
22
|
+
subject: 'Mathematics',
|
|
23
|
+
type: 'past_paper'
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## API Overview
|
|
28
|
+
|
|
29
|
+
The Malawi Curriculum API provides access to educational resources across various academic levels in Malawi. All endpoints require authentication via an API key.
|
|
30
|
+
|
|
31
|
+
### Base URL
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
https://malawi-curricular-api-production.up.railway.app/api/v1
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Authentication
|
|
38
|
+
|
|
39
|
+
Include your API key in the `Authorization` header:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Authorization: Bearer YOUR_API_KEY
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## API Reference
|
|
46
|
+
|
|
47
|
+
### Get Levels
|
|
48
|
+
|
|
49
|
+
Retrieves all academic levels available in the Malawi curriculum.
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
const levels = await client.getLevels();
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Request:**
|
|
56
|
+
- Method: `GET`
|
|
57
|
+
- Endpoint: `/levels`
|
|
58
|
+
- Headers: `Authorization: Bearer API_KEY`
|
|
59
|
+
|
|
60
|
+
**Response:**
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"success": true,
|
|
64
|
+
"count": 3,
|
|
65
|
+
"data": [
|
|
66
|
+
{ "id": 1, "name": "JCE" },
|
|
67
|
+
{ "id": 2, "name": "MSCE" },
|
|
68
|
+
{ "id": 3, "name": "Primary" }
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### Get Subjects
|
|
76
|
+
|
|
77
|
+
Retrieves subjects, optionally filtered by academic level.
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
const subjects = await client.getSubjects('MSCE');
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Request:**
|
|
84
|
+
- Method: `GET`
|
|
85
|
+
- Endpoint: `/subjects`
|
|
86
|
+
- Headers: `Authorization: Bearer API_KEY`
|
|
87
|
+
- Query Parameters:
|
|
88
|
+
- `level` (optional): Filter by level name (e.g., "MSCE", "JCE")
|
|
89
|
+
|
|
90
|
+
**Response:**
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"success": true,
|
|
94
|
+
"count": 12,
|
|
95
|
+
"data": [
|
|
96
|
+
{ "id": 1, "name": "Mathematics", "level": "MSCE" },
|
|
97
|
+
{ "id": 2, "name": "Physics", "level": "MSCE" },
|
|
98
|
+
{ "id": 3, "name": "Chemistry", "level": "MSCE" }
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### Get Resources
|
|
106
|
+
|
|
107
|
+
Retrieves curriculum resources with filtering options.
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
const resources = await client.getResources({
|
|
111
|
+
level: 'MSCE',
|
|
112
|
+
subject: 'Mathematics',
|
|
113
|
+
type: 'past_paper',
|
|
114
|
+
year: 2023,
|
|
115
|
+
limit: 10
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Request:**
|
|
120
|
+
- Method: `GET`
|
|
121
|
+
- Endpoint: `/resources`
|
|
122
|
+
- Headers: `Authorization: Bearer API_KEY`
|
|
123
|
+
- Query Parameters:
|
|
124
|
+
- `level` (required): Academic level (e.g., "MSCE", "JCE")
|
|
125
|
+
- `subject` (required): Subject name (e.g., "Mathematics")
|
|
126
|
+
- `type` (optional): Resource type
|
|
127
|
+
- `past_paper`
|
|
128
|
+
- `marking_scheme`
|
|
129
|
+
- `textbook`
|
|
130
|
+
- `teacher_notes`
|
|
131
|
+
- `student_notes`
|
|
132
|
+
- `scheme_of_work`
|
|
133
|
+
- `year` (optional): Year of the resource (integer)
|
|
134
|
+
- `limit` (optional): Number of results (1-100, default: 50)
|
|
135
|
+
- `offset` (optional): Pagination offset (default: 0)
|
|
136
|
+
|
|
137
|
+
**Response:**
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"success": true,
|
|
141
|
+
"count": 5,
|
|
142
|
+
"total_count": 47,
|
|
143
|
+
"data": [
|
|
144
|
+
{
|
|
145
|
+
"id": 101,
|
|
146
|
+
"title": "MSCE Mathematics Paper 1 2023",
|
|
147
|
+
"type": "past_paper",
|
|
148
|
+
"year": 2023,
|
|
149
|
+
"description": "Main examination paper",
|
|
150
|
+
"subject": "Mathematics",
|
|
151
|
+
"level": "MSCE"
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
### Download Resource (Secure Token Flow)
|
|
160
|
+
|
|
161
|
+
Downloads use a two-step token flow for security. Requires a paid subscription (Basic, Pro, or Enterprise).
|
|
162
|
+
|
|
163
|
+
#### Request a Download Token
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
const tokenData = await client.requestDownload(101);
|
|
167
|
+
console.log('Token:', tokenData.token);
|
|
168
|
+
console.log('Expires in:', tokenData.expires_in_seconds, 'seconds');
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Request:**
|
|
172
|
+
- Method: `POST`
|
|
173
|
+
- Endpoint: `/downloads/request`
|
|
174
|
+
- Headers: `Authorization: Bearer API_KEY`
|
|
175
|
+
- Body: `{ "resourceId": 101 }`
|
|
176
|
+
|
|
177
|
+
**Response:**
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"success": true,
|
|
181
|
+
"token": "a1b2c3d4e5f6...",
|
|
182
|
+
"expires_in_seconds": 900,
|
|
183
|
+
"download_url": "/api/v1/downloads/a1b2c3d4e5f6..."
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
#### Redeem the Token
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
const download = await client.redeemDownload(tokenData.token);
|
|
191
|
+
console.log('File URL:', download.download_url);
|
|
192
|
+
// URL is valid for 5 minutes, max 2 attempts per token
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Request:**
|
|
196
|
+
- Method: `GET`
|
|
197
|
+
- Endpoint: `/downloads/{token}`
|
|
198
|
+
- No API key required (token is the authentication)
|
|
199
|
+
|
|
200
|
+
**Response:**
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"success": true,
|
|
204
|
+
"download_url": "https://storage.googleapis.com/...",
|
|
205
|
+
"expires_in_seconds": 300,
|
|
206
|
+
"attempts_remaining": 1
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Error Responses:**
|
|
211
|
+
```json
|
|
212
|
+
// Free Tier
|
|
213
|
+
{ "error": "Free tier cannot download files.", "code": "PLAN_INSUFFICIENT" }
|
|
214
|
+
|
|
215
|
+
// Limit Exceeded
|
|
216
|
+
{ "error": "Daily download limit exceeded (100/day).", "code": "DOWNLOAD_LIMIT_EXCEEDED" }
|
|
217
|
+
|
|
218
|
+
// Token Expired
|
|
219
|
+
{ "error": "Download token has expired.", "code": "TOKEN_EXPIRED" }
|
|
220
|
+
|
|
221
|
+
// Max Attempts
|
|
222
|
+
{ "error": "Maximum download attempts reached (2).", "code": "TOKEN_MAX_ATTEMPTS" }
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
> **Security:** Tokens expire after 15 minutes, allow max 2 download attempts, and are stored as SHA-256 hashes in the database. The signed download URL is only valid for 5 minutes.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
### Resource Pricing
|
|
230
|
+
|
|
231
|
+
Set custom prices on resources or mark them as free. Each developer's pricing is independent, identified by their API key.
|
|
232
|
+
|
|
233
|
+
#### Set a Price
|
|
234
|
+
|
|
235
|
+
```javascript
|
|
236
|
+
// Mark a resource as paid (500 MWK)
|
|
237
|
+
const pricing = await client.setPrice({
|
|
238
|
+
resourceId: 101,
|
|
239
|
+
price: 500,
|
|
240
|
+
isFree: false
|
|
241
|
+
});
|
|
242
|
+
console.log(pricing);
|
|
243
|
+
// { resource_id: 101, price_mwk: 500, is_free: false }
|
|
244
|
+
|
|
245
|
+
// Mark a resource as free
|
|
246
|
+
await client.setPrice({ resourceId: 101, isFree: true });
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Request:**
|
|
250
|
+
- Method: `POST`
|
|
251
|
+
- Endpoint: `/pricing/set`
|
|
252
|
+
- Headers: `Authorization: Bearer API_KEY`
|
|
253
|
+
- Body:
|
|
254
|
+
- `resourceId` (required): Resource ID
|
|
255
|
+
- `price` (optional): Price in MWK (ignored if `isFree` is true)
|
|
256
|
+
- `isFree` (optional): Set to `true` for free access
|
|
257
|
+
|
|
258
|
+
**Response:**
|
|
259
|
+
```json
|
|
260
|
+
{
|
|
261
|
+
"success": true,
|
|
262
|
+
"data": {
|
|
263
|
+
"resource_id": 101,
|
|
264
|
+
"price_mwk": 500,
|
|
265
|
+
"is_free": false
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
#### Get a Price
|
|
271
|
+
|
|
272
|
+
```javascript
|
|
273
|
+
const pricing = await client.getPrice(101);
|
|
274
|
+
console.log(pricing.is_free); // true or false
|
|
275
|
+
console.log(pricing.price_mwk); // price in MWK
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Request:**
|
|
279
|
+
- Method: `GET`
|
|
280
|
+
- Endpoint: `/pricing/{resourceId}`
|
|
281
|
+
- Headers: `Authorization: Bearer API_KEY`
|
|
282
|
+
|
|
283
|
+
**Response:**
|
|
284
|
+
```json
|
|
285
|
+
{
|
|
286
|
+
"success": true,
|
|
287
|
+
"data": {
|
|
288
|
+
"resource_id": 101,
|
|
289
|
+
"price_mwk": 0,
|
|
290
|
+
"is_free": true
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
If no price has been set, the resource defaults to free.
|
|
296
|
+
|
|
297
|
+
**Download Enforcement:**
|
|
298
|
+
|
|
299
|
+
When a resource has been priced (not free), download requests return `402 Payment Required`:
|
|
300
|
+
|
|
301
|
+
```json
|
|
302
|
+
{
|
|
303
|
+
"error": "This resource requires purchase before downloading.",
|
|
304
|
+
"code": "PAYMENT_REQUIRED",
|
|
305
|
+
"price_mwk": 500
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
### Search Resources
|
|
312
|
+
|
|
313
|
+
Search across all curriculum resources. Results and filter capabilities depend on your plan tier.
|
|
314
|
+
|
|
315
|
+
```javascript
|
|
316
|
+
const results = await client.search({
|
|
317
|
+
q: 'biology past paper',
|
|
318
|
+
level: 'MSCE', // Basic+ plans
|
|
319
|
+
type: 'past_paper', // Pro+ plans
|
|
320
|
+
year: 2024 // Pro+ plans
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**Request:**
|
|
325
|
+
- Method: `GET`
|
|
326
|
+
- Endpoint: `/search`
|
|
327
|
+
- Headers: `Authorization: Bearer API_KEY`
|
|
328
|
+
- Query Parameters:
|
|
329
|
+
- `q` (required): Search query (min 2 characters)
|
|
330
|
+
- `level` (optional): Filter by level -- Basic+ plans only
|
|
331
|
+
- `subject` (optional): Filter by subject -- Basic+ plans only
|
|
332
|
+
- `type` (optional): Filter by resource type -- Pro+ plans only
|
|
333
|
+
- `year` (optional): Filter by year -- Pro+ plans only
|
|
334
|
+
- `limit` (optional): Max results (capped by plan tier)
|
|
335
|
+
- `offset` (optional): Pagination offset
|
|
336
|
+
- `sort` (optional): Sort order -- Enterprise only (`relevance`, `newest`, `oldest`, `title`)
|
|
337
|
+
|
|
338
|
+
**Response:**
|
|
339
|
+
```json
|
|
340
|
+
{
|
|
341
|
+
"success": true,
|
|
342
|
+
"count": 5,
|
|
343
|
+
"total_count": 23,
|
|
344
|
+
"tier": "basic",
|
|
345
|
+
"tier_info": "Title & description search with basic filters.",
|
|
346
|
+
"data": [
|
|
347
|
+
{
|
|
348
|
+
"id": 101,
|
|
349
|
+
"title": "MSCE Biology Paper 1 2024",
|
|
350
|
+
"type": "past_paper",
|
|
351
|
+
"year": 2024,
|
|
352
|
+
"subject": "Biology",
|
|
353
|
+
"level": "MSCE",
|
|
354
|
+
"relevance": 0.8721
|
|
355
|
+
}
|
|
356
|
+
]
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**Search Tier Limits:**
|
|
361
|
+
|
|
362
|
+
| Feature | Free | Basic | Pro | Enterprise |
|
|
363
|
+
|---|---|---|---|---|
|
|
364
|
+
| Search fields | Title only | Title + Description | Title + Description | Title + Description |
|
|
365
|
+
| Max results | 10 | 50 | 100 | 500 |
|
|
366
|
+
| Filters | None | Level, Subject | All | All + Sorting |
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## Error Handling
|
|
370
|
+
|
|
371
|
+
All methods may throw errors for various reasons:
|
|
372
|
+
|
|
373
|
+
```javascript
|
|
374
|
+
try {
|
|
375
|
+
const resources = await client.getResources({ level: 'MSCE', subject: 'Math' });
|
|
376
|
+
} catch (error) {
|
|
377
|
+
if (error.response?.status === 401) {
|
|
378
|
+
console.error('Invalid API key');
|
|
379
|
+
} else if (error.response?.status === 402) {
|
|
380
|
+
console.error('Payment required for this resource');
|
|
381
|
+
} else if (error.response?.status === 429) {
|
|
382
|
+
console.error('Rate limit exceeded');
|
|
383
|
+
} else {
|
|
384
|
+
console.error('Error:', error.message);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Common HTTP Status Codes
|
|
390
|
+
|
|
391
|
+
- `200` - Success
|
|
392
|
+
- `401` - Unauthorized (invalid or missing API key)
|
|
393
|
+
- `402` - Payment Required (resource has a price set)
|
|
394
|
+
- `403` - Forbidden (subscription expired or insufficient permissions)
|
|
395
|
+
- `404` - Resource not found
|
|
396
|
+
- `429` - Rate limit exceeded
|
|
397
|
+
|
|
398
|
+
## Rate Limits & Plans
|
|
399
|
+
|
|
400
|
+
Different subscription tiers provide varying levels of access. Visit the [developer portal](https://malawi-curricular-api-production.up.railway.app) for current pricing.
|
|
401
|
+
|
|
402
|
+
### Free Plan
|
|
403
|
+
- 100 requests/day
|
|
404
|
+
- Metadata access only
|
|
405
|
+
- No file downloads
|
|
406
|
+
|
|
407
|
+
### Basic Plan
|
|
408
|
+
- 1,000 requests/day
|
|
409
|
+
- 5 downloads/day
|
|
410
|
+
- Full API access
|
|
411
|
+
|
|
412
|
+
### Pro Plan
|
|
413
|
+
- 10,000 requests/day
|
|
414
|
+
- 100 downloads/day
|
|
415
|
+
- Priority support
|
|
416
|
+
|
|
417
|
+
### Enterprise Plan
|
|
418
|
+
- 100,000 requests/day
|
|
419
|
+
- 1,000 downloads/day
|
|
420
|
+
- 24/7 support
|
|
421
|
+
|
|
422
|
+
## License
|
|
423
|
+
|
|
424
|
+
ISC
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "malawi-curriculum-api",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "Official JavaScript SDK for the Malawi Curriculum API",
|
|
5
|
+
"main": "src/MalawiCurriculumClient.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node test-sdk.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"malawi",
|
|
12
|
+
"curriculum",
|
|
13
|
+
"education",
|
|
14
|
+
"api",
|
|
15
|
+
"sdk"
|
|
16
|
+
],
|
|
17
|
+
"author": "CodingCodeMW",
|
|
18
|
+
"license": "ISC",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"node-fetch": "^3.3.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {Object} ResourceFilter
|
|
4
|
+
* @property {string} [level] - Education level (e.g., 'MSCE', 'JCE', 'PRIMARY')
|
|
5
|
+
* @property {string} [subject] - Subject name (e.g., 'Mathematics')
|
|
6
|
+
* @property {string} [type] - Resource type (e.g., 'past_paper', 'textbook')
|
|
7
|
+
* @property {number} [year] - Year of the resource
|
|
8
|
+
* @property {number} [limit] - Max number of results (1-100)
|
|
9
|
+
* @property {number} [offset] - Pagination offset
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} Resource
|
|
14
|
+
* @property {number} id
|
|
15
|
+
* @property {string} title
|
|
16
|
+
* @property {string} type
|
|
17
|
+
* @property {string} subject
|
|
18
|
+
* @property {string} level
|
|
19
|
+
* @property {number|null} year
|
|
20
|
+
* @property {string|null} description
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export class MalawiCurriculumClient {
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object} config
|
|
26
|
+
* @param {string} config.apiKey - Your API Key
|
|
27
|
+
* @param {string} [config.baseUrl] - API Base URL (default: https://malawi-curricular-api-production.up.railway.app/api/v1)
|
|
28
|
+
*/
|
|
29
|
+
constructor({ apiKey, baseUrl }) {
|
|
30
|
+
if (!apiKey) throw new Error('API Key is required');
|
|
31
|
+
|
|
32
|
+
this.apiKey = apiKey;
|
|
33
|
+
this.baseUrl = baseUrl || 'https://malawi-curricular-api-production.up.railway.app/api/v1';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Helper to make authenticated requests
|
|
38
|
+
* @private
|
|
39
|
+
*/
|
|
40
|
+
async _request(endpoint, query = {}) {
|
|
41
|
+
// Filter out undefined query params
|
|
42
|
+
const params = new URLSearchParams();
|
|
43
|
+
Object.entries(query).forEach(([key, value]) => {
|
|
44
|
+
if (value !== undefined && value !== null) {
|
|
45
|
+
params.append(key, String(value));
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const url = `${this.baseUrl}${endpoint}?${params.toString()}`;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch(url, {
|
|
53
|
+
headers: {
|
|
54
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
55
|
+
'Content-Type': 'application/json'
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
const errorText = await response.text();
|
|
61
|
+
throw new Error(`API Error ${response.status}: ${errorText}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const json = await response.json();
|
|
65
|
+
return json;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error.cause) console.error('Network Error Cause:', error.cause);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Helper to make authenticated POST requests
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
76
|
+
async _post(endpoint, body = {}) {
|
|
77
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const response = await fetch(url, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: {
|
|
83
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
84
|
+
'Content-Type': 'application/json'
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify(body)
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
const errorText = await response.text();
|
|
91
|
+
throw new Error(`API Error ${response.status}: ${errorText}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return await response.json();
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error.cause) console.error('Network Error Cause:', error.cause);
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get filtered resources
|
|
103
|
+
* @param {ResourceFilter} filter
|
|
104
|
+
* @returns {Promise<Resource[]>}
|
|
105
|
+
*/
|
|
106
|
+
async getResources(filter = {}) {
|
|
107
|
+
const response = await this._request('/resources', filter);
|
|
108
|
+
return response.data || [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get all subjects, optionally filtered by level
|
|
113
|
+
* @param {string} [level]
|
|
114
|
+
* @returns {Promise<Object[]>}
|
|
115
|
+
*/
|
|
116
|
+
async getSubjects(level) {
|
|
117
|
+
const response = await this._request('/subjects', { level });
|
|
118
|
+
return response.data || [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get all education levels
|
|
123
|
+
* @returns {Promise<Object[]>}
|
|
124
|
+
*/
|
|
125
|
+
async getLevels() {
|
|
126
|
+
const response = await this._request('/levels');
|
|
127
|
+
return response.data || [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Search resources with plan-tiered filtering
|
|
132
|
+
* @param {Object} options
|
|
133
|
+
* @param {string} options.q - Search query (required, min 2 chars)
|
|
134
|
+
* @param {string} [options.level] - Filter by level (Basic+ plans)
|
|
135
|
+
* @param {string} [options.subject] - Filter by subject (Basic+ plans)
|
|
136
|
+
* @param {string} [options.type] - Filter by type (Pro+ plans)
|
|
137
|
+
* @param {number} [options.year] - Filter by year (Pro+ plans)
|
|
138
|
+
* @param {number} [options.limit] - Max results (capped by plan)
|
|
139
|
+
* @param {number} [options.offset] - Pagination offset
|
|
140
|
+
* @param {string} [options.sort] - Sort order (Enterprise only: 'relevance', 'newest', 'oldest', 'title')
|
|
141
|
+
* @returns {Promise<Object>} Search results with tier info
|
|
142
|
+
*/
|
|
143
|
+
async search(options = {}) {
|
|
144
|
+
return await this._request('/search', options);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Request a secure download token (paid plans only)
|
|
149
|
+
* @param {number} resourceId - ID of the resource to download
|
|
150
|
+
* @returns {Promise<{token: string, expires_in_seconds: number, download_url: string}>}
|
|
151
|
+
*/
|
|
152
|
+
async requestDownload(resourceId) {
|
|
153
|
+
return await this._post('/downloads/request', { resourceId });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Redeem a download token to get the file URL
|
|
158
|
+
* @param {string} token - Token from requestDownload()
|
|
159
|
+
* @returns {Promise<{download_url: string, expires_in_seconds: number, attempts_remaining: number}>}
|
|
160
|
+
*/
|
|
161
|
+
async redeemDownload(token) {
|
|
162
|
+
return await this._request(`/downloads/${token}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Convenience: Request token and redeem in one call
|
|
167
|
+
* @param {number} resourceId
|
|
168
|
+
* @returns {Promise<string>} Signed download URL
|
|
169
|
+
*/
|
|
170
|
+
async download(resourceId) {
|
|
171
|
+
const { token } = await this.requestDownload(resourceId);
|
|
172
|
+
const { download_url } = await this.redeemDownload(token);
|
|
173
|
+
return download_url;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @deprecated Use requestDownload() + redeemDownload() or download() instead
|
|
178
|
+
* @param {number} id
|
|
179
|
+
* @returns {Promise<string>} Signed download URL
|
|
180
|
+
*/
|
|
181
|
+
async downloadResource(id) {
|
|
182
|
+
console.warn('[DEPRECATED] downloadResource() is deprecated. Use download() for the secure token flow.');
|
|
183
|
+
const response = await this._request(`/resources/${id}/download`);
|
|
184
|
+
return response.download_url;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Set or update pricing for a resource
|
|
189
|
+
* @param {Object} options
|
|
190
|
+
* @param {number} options.resourceId - ID of the resource to price
|
|
191
|
+
* @param {number} [options.price] - Price in MWK (ignored if isFree is true)
|
|
192
|
+
* @param {boolean} [options.isFree] - Set to true to make the resource free
|
|
193
|
+
* @returns {Promise<{resource_id: number, price_mwk: number, is_free: boolean}>}
|
|
194
|
+
*/
|
|
195
|
+
async setPrice({ resourceId, price, isFree }) {
|
|
196
|
+
const response = await this._post('/pricing/set', { resourceId, price, isFree });
|
|
197
|
+
return response.data;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get the price set for a specific resource
|
|
202
|
+
* @param {number} resourceId - ID of the resource
|
|
203
|
+
* @returns {Promise<{resource_id: number, price_mwk: number, is_free: boolean}>}
|
|
204
|
+
*/
|
|
205
|
+
async getPrice(resourceId) {
|
|
206
|
+
const response = await this._request(`/pricing/${resourceId}`);
|
|
207
|
+
return response.data;
|
|
208
|
+
}
|
|
209
|
+
}
|
package/test-sdk.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
|
|
2
|
+
import { MalawiCurriculumClient } from './src/MalawiCurriculumClient.js';
|
|
3
|
+
|
|
4
|
+
// Use the known test key or replace with yours
|
|
5
|
+
const API_KEY = 'YOUR_API_KEY'; // Replace with your actual key
|
|
6
|
+
const BASE_URL = 'https://malawi-curricular-api-production.up.railway.app/api/v1';
|
|
7
|
+
|
|
8
|
+
async function testSDK() {
|
|
9
|
+
console.log('Testing Malawi Curriculum SDK...');
|
|
10
|
+
|
|
11
|
+
const client = new MalawiCurriculumClient({
|
|
12
|
+
apiKey: API_KEY,
|
|
13
|
+
baseUrl: BASE_URL
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
console.log('\nFetching Subjects...');
|
|
18
|
+
const subjects = await client.getSubjects();
|
|
19
|
+
console.log(`Success! Found ${subjects.length} subjects.`);
|
|
20
|
+
if (subjects.length > 0) console.log(' Sample:', subjects[0]);
|
|
21
|
+
|
|
22
|
+
console.log('\nFetching Levels...');
|
|
23
|
+
const levels = await client.getLevels();
|
|
24
|
+
console.log(`Success! Found ${levels.length} levels.`);
|
|
25
|
+
if (levels.length > 0) console.log(' Sample:', levels[0]);
|
|
26
|
+
|
|
27
|
+
console.log('\nFetching Resources (MSCE Mathematics)...');
|
|
28
|
+
const resources = await client.getResources({
|
|
29
|
+
level: 'MSCE',
|
|
30
|
+
subject: 'Mathematics',
|
|
31
|
+
limit: 5
|
|
32
|
+
});
|
|
33
|
+
console.log(`Success! Found ${resources.length} resources.`);
|
|
34
|
+
if (resources.length > 0) console.log(' Sample:', resources[0]);
|
|
35
|
+
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('SDK Test Failed:', error.message);
|
|
38
|
+
if (error.cause) console.error(' Cause:', error.cause);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
testSDK();
|