@insignia-education/api-sdk-js 0.9.25 → 0.9.28
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/AGENTS.md +29 -1
- package/package.json +1 -1
- package/src/Client.js +18 -0
- package/src/api/v1/Files.js +17 -2
- package/src/api/v1/Users.js +14 -2
- package/tests/integration/api/v1/files.test.js +15 -1
package/AGENTS.md
CHANGED
|
@@ -15,6 +15,18 @@ npm run lint # ESLint
|
|
|
15
15
|
A zero-dependency JavaScript SDK that wraps the Insignia Education API (`/api/v1`).
|
|
16
16
|
Consumed by `insignia-education/front` via `@insignia-education/api-sdk-js`.
|
|
17
17
|
|
|
18
|
+
## API versioning
|
|
19
|
+
The SDK is versioned to match the API:
|
|
20
|
+
|
|
21
|
+
- `src/index.js` — base client
|
|
22
|
+
- `src/api/index.js` — appends `/api` to the base URL
|
|
23
|
+
- `src/api/v1/index.js` — appends `/v1` → all requests land at `<host>/api/v1/...`
|
|
24
|
+
- **v1 is being finalized. Once stable it is permanently frozen.**
|
|
25
|
+
- A future v2 API will live in `src/api/v2/index.js` (new class, new resource modules).
|
|
26
|
+
- Never modify the URL construction logic in `v1/` to point at a different version.
|
|
27
|
+
- Tests for each version live in `tests/integration/api/v1/` — mirror this structure for v2+.
|
|
28
|
+
- The `upload(path, formData)` method on `Client` sends multipart — use `api.files.upload(fd)` for any file upload, never raw `fetch()`.
|
|
29
|
+
|
|
18
30
|
## Structure
|
|
19
31
|
```
|
|
20
32
|
src/
|
|
@@ -46,9 +58,25 @@ api.users.cashReceivers();
|
|
|
46
58
|
## Adding a new resource
|
|
47
59
|
1. Create `src/api/v1/ResourceName.js` with a class that receives the client
|
|
48
60
|
2. Register it in `src/api/v1/index.js`
|
|
49
|
-
3. Write tests in `
|
|
61
|
+
3. Write integration tests in `tests/integration/api/v1/resource-name.test.js`
|
|
62
|
+
|
|
63
|
+
## Internationalisation (i18n)
|
|
64
|
+
The SDK is language-neutral — it must never contain human-readable strings.
|
|
65
|
+
|
|
66
|
+
- Do not include hardcoded error messages or labels in SDK source.
|
|
67
|
+
- Error objects thrown by the SDK must expose a machine-readable `status` (HTTP code) and `data` (raw API body). The consuming app handles translation.
|
|
68
|
+
- Do not add locale/language logic to the SDK — that belongs to the frontend.
|
|
69
|
+
|
|
70
|
+
## API ↔ SDK sync rule
|
|
71
|
+
|
|
72
|
+
**This SDK must stay in sync with [`insignia-education/api`](../api) at all times.** Any endpoint added, renamed, or removed in the API must be reflected here in the same task/commit.
|
|
73
|
+
|
|
74
|
+
- New endpoint in `api` → new method in the correct `src/api/v1/*.js` class
|
|
75
|
+
- Removed endpoint → remove or deprecate the corresponding SDK method
|
|
76
|
+
- Never leave the SDK behind the API; the `front` repo relies solely on this SDK
|
|
50
77
|
|
|
51
78
|
## Never do
|
|
52
79
|
- Don't add runtime dependencies
|
|
53
80
|
- Don't change the constructor signature of `InsigniaApiV1`
|
|
54
81
|
- Don't hardcode API base URLs — always receive from constructor
|
|
82
|
+
- Don't let the SDK lag behind `api` — update both in the same task
|
package/package.json
CHANGED
package/src/Client.js
CHANGED
|
@@ -86,6 +86,24 @@ export default class InsigniaClient {
|
|
|
86
86
|
return data?.success ? data.response : data;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
async upload(path, formData) {
|
|
90
|
+
const headers = { 'Accept': 'application/json' };
|
|
91
|
+
const cookie = this.#cookieHeader();
|
|
92
|
+
if (cookie) headers.Cookie = cookie;
|
|
93
|
+
const response = await fetch(`${this.#baseUrl}${path}`, {
|
|
94
|
+
method: 'PUT', headers, credentials: 'include', body: formData,
|
|
95
|
+
});
|
|
96
|
+
this.#storeCookies(response);
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
const err = new Error(`HTTP ${response.status}`);
|
|
99
|
+
err.status = response.status;
|
|
100
|
+
try { err.data = await this.#parseResponse(response); } catch (_) {}
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
const data = await this.#parseResponse(response);
|
|
104
|
+
return data?.success ? data.response : data;
|
|
105
|
+
}
|
|
106
|
+
|
|
89
107
|
get(path) { return this.#request('GET', path); }
|
|
90
108
|
post(path, body = null) { return this.#request('POST', path, body); }
|
|
91
109
|
put(path, body = null) { return this.#request('PUT', path, body); }
|
package/src/api/v1/Files.js
CHANGED
|
@@ -5,6 +5,21 @@ export default class Files {
|
|
|
5
5
|
this.#client = client;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/** List files in a directory. params: { organization_id, directory, per_page } */
|
|
9
|
+
list(params = {}) { return this.#client.get('/files', params); }
|
|
10
|
+
|
|
11
|
+
/** Get a single file record by ID. */
|
|
12
|
+
get(id) { return this.#client.get(`/files/${id}`); }
|
|
13
|
+
|
|
14
|
+
/** Upload a file (FormData). Supports: file, folder, organization_id, filename, generate_webp, … */
|
|
15
|
+
upload(formData) { return this.#client.upload('/files', formData); }
|
|
16
|
+
|
|
17
|
+
/** Create a logical folder. body: { organization_id, path } */
|
|
18
|
+
createFolder(body) { return this.#client.upload('/files/folder', body); }
|
|
19
|
+
|
|
20
|
+
/** Update file description. body: { description } */
|
|
21
|
+
update(id, body) { return this.#client.patch(`/files/${id}`, body); }
|
|
22
|
+
|
|
23
|
+
/** Soft-delete a file. Pass s3=true to also remove from S3. */
|
|
24
|
+
delete(id, s3 = false) { return this.#client.del(`/files/${id}${s3 ? '?s3=1' : ''}`); }
|
|
10
25
|
}
|
package/src/api/v1/Users.js
CHANGED
|
@@ -5,8 +5,9 @@ export default class Users {
|
|
|
5
5
|
this.#client = client;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
get(id = null)
|
|
9
|
-
|
|
8
|
+
get(id = null) { return id ? this.#client.get(`/users/${id}`) : this.#client.get('/users'); }
|
|
9
|
+
getByUsername(username) { return this.#client.get(`/users/username/${username}`); }
|
|
10
|
+
cashReceivers() { return this.#client.get('/users/cash-receivers'); }
|
|
10
11
|
edit(id, data) { return this.#client.patch(`/users/${id}`, data); }
|
|
11
12
|
|
|
12
13
|
#nested(userId, path) {
|
|
@@ -61,6 +62,17 @@ export default class Users {
|
|
|
61
62
|
|
|
62
63
|
statistics(userId) { return { get: () => this.#client.get(`/users/${userId}/statistics`) }; }
|
|
63
64
|
|
|
65
|
+
experiences(userId) {
|
|
66
|
+
const base = `/users/${userId}/experiences`;
|
|
67
|
+
const client = this.#client;
|
|
68
|
+
return {
|
|
69
|
+
get: (id = null) => id ? client.get(`${base}/${id}`) : client.get(base),
|
|
70
|
+
create: (data) => client.put(base, data),
|
|
71
|
+
edit: (id, data) => client.patch(`${base}/${id}`, data),
|
|
72
|
+
delete: (id) => client.del(`${base}/${id}`),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
64
76
|
organizations(userId) {
|
|
65
77
|
const base = `/users/${userId}/organizations`;
|
|
66
78
|
const client = this.#client;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Blob } from 'buffer';
|
|
1
2
|
import InsigniaApiV1 from '../../../../src/api/v1/index.js';
|
|
2
3
|
|
|
3
4
|
const api = new InsigniaApiV1(process.env.INSIGNIA_EDUCATION_API_BASE_URL);
|
|
@@ -16,10 +17,23 @@ describe('api/v1/files', () => {
|
|
|
16
17
|
expect(Array.isArray(response)).toBe(true);
|
|
17
18
|
response.forEach(file => {
|
|
18
19
|
expect(file["id"]).toBeDefined();
|
|
19
|
-
expect(file["path"]).toBeDefined();
|
|
20
20
|
expect(file["created_at"]).toBeDefined();
|
|
21
21
|
expect(file["updated_at"]).toBeDefined();
|
|
22
22
|
});
|
|
23
23
|
});
|
|
24
24
|
});
|
|
25
|
+
|
|
26
|
+
test('upload | authenticated', async () => {
|
|
27
|
+
await login();
|
|
28
|
+
const blob = new Blob(['test'], { type: 'image/png' });
|
|
29
|
+
const fd = new FormData();
|
|
30
|
+
fd.append('file', blob, 'test.png');
|
|
31
|
+
fd.append('type', 'uploads');
|
|
32
|
+
await api.files.upload(fd)
|
|
33
|
+
.then(response => {
|
|
34
|
+
expect(response).toBeDefined();
|
|
35
|
+
expect(response['url']).toBeDefined();
|
|
36
|
+
expect(response['id']).toBeDefined();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
25
39
|
});
|