Skip to content
Open

V5 #55

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
5b6db13
Skeleton
exacs Jul 3, 2024
a4afc31
Update settings
exacs Jul 3, 2024
59eb565
First implementation of sisImport
exacs Jul 3, 2024
542a6a4
Normalize requests
exacs Jul 3, 2024
53a9164
Advanced Request and Response Errors
exacs Jul 3, 2024
51b005e
New ts, jest config
exacs Jul 3, 2024
b38bd06
Update and simplify eslint
exacs Jul 3, 2024
6d9fa1d
A failing test
exacs Jul 3, 2024
f96267f
Implement `get` without query parameters
exacs Jul 3, 2024
52d65e1
Read dotenv from jest
exacs Jul 3, 2024
015ab64
Export CanvasApiError objects
exacs Jul 3, 2024
072d42e
Successful test
exacs Jul 3, 2024
4d02875
A bit better error message
exacs Jul 3, 2024
d24d40c
Add authorization header
exacs Jul 3, 2024
30d9124
Refactor
exacs Jul 4, 2024
8ad07fd
Update test
exacs Jul 4, 2024
e57c00c
Implement queryParameters
exacs Jul 4, 2024
bc3915b
Accept query parameters in GET requests
exacs Jul 4, 2024
311f77f
Add test with query parameters
exacs Jul 4, 2024
0d8f478
ExtendedGenerator to its own file
exacs Jul 4, 2024
83fd06c
Implement listPages and listItems
exacs Jul 4, 2024
d5f948c
Add ExtendedGenerator tests
exacs Jul 4, 2024
e682ee4
Add custom error
exacs Jul 4, 2024
fc24b82
Remove dependencies
exacs Jul 5, 2024
5cda3d9
fix eslint problems
exacs Jul 5, 2024
91d47a5
Do not put "?" when empty query params
exacs Jul 8, 2024
57ea699
Add tests to intercept errors
exacs Jul 8, 2024
5597816
Implement timeouts
exacs Jul 8, 2024
0f08508
Eslint
exacs Jul 8, 2024
716786a
Split unit and integration tests
exacs Jul 8, 2024
c54f4c1
Add tests for SIS Import
exacs Jul 8, 2024
6488bb7
Add timeout
exacs Jul 9, 2024
944ec1d
Structure tests
exacs Jul 9, 2024
433be8b
Document `ExtendedGenerator` better
exacs Jul 9, 2024
0828aa1
Delete unnecessary files
exacs Jul 9, 2024
7dc7826
Allow URLs without trailing slash
exacs Jul 9, 2024
9a91e2b
Restructure README
exacs Jul 9, 2024
d3a57cc
5.0.0-beta.0
exacs Jul 9, 2024
78e6ac9
Small fixes
exacs Jul 9, 2024
33f4423
Use 'body' as alias of 'json'
exacs Jul 10, 2024
d801132
Rename stuff
exacs Jul 10, 2024
326db6c
Throw errors only for 4xx & 5xx
exacs Jul 10, 2024
523b3bb
Azure pipeline for publish to npm
exacs Jul 11, 2024
ff7544a
5.0.0-beta.1
exacs Jul 11, 2024
ae0d2d5
Remove null check
exacs Aug 12, 2024
d7af6fc
Change SIS Import parameter to File
exacs Aug 12, 2024
7c734ac
Update test
exacs Aug 12, 2024
4069ee1
5.0.0-beta.2
exacs Aug 12, 2024
d4479d7
Update README
exacs Aug 12, 2024
f5b1201
Update examples
exacs Aug 12, 2024
0440535
add test and content-type header
Epicpants-kth Aug 20, 2024
d7a4d9a
5.0.0-beta.3
exacs Aug 20, 2024
15d4281
condition header if not Formdata
Epicpants-kth Aug 22, 2024
35ac4aa
Merge branch 'v5' of github.com:KTH/canvas-api into v5
Epicpants-kth Aug 22, 2024
39c8860
5.0.0-beta.4
exacs Aug 22, 2024
4fb1f86
5.0.0-beta.5
exacs Aug 22, 2024
47f435c
Pin node-version to make sure npm ci doesn't complain about version m…
jhsware Sep 5, 2024
f0cf7e5
Add notes about nix-shell
jhsware Sep 5, 2024
ab2250f
Format
exacs Sep 12, 2024
83788f7
Ensure stack traces
exacs Sep 12, 2024
83d8d2a
Cleanup integration tests
exacs Sep 12, 2024
1a5d524
adapt test scripts
exacs Sep 12, 2024
3218b3b
5.0.0-beta.6
exacs Sep 12, 2024
3490018
Add stack traces
exacs Sep 12, 2024
752294d
5.0.0-beta.7
exacs Sep 12, 2024
e38a10c
Improve stack traces and reduce expensive calls to captureStackTrace
jhsware Aug 1, 2025
6ffd9d4
Added rate limiting support
jhsware Aug 4, 2025
d3d78be
Merge pull request #62 from KTH/add-rate-limiting-support
jhsware Aug 5, 2025
d13a891
Merge pull request #57 from KTH/nixpkgs-config
jhsware Aug 5, 2025
30327b8
Bump nixpkgs to 24.11
jhsware Aug 5, 2025
f8214bc
Formatting
jhsware Aug 5, 2025
fd11e28
Manual publishing
jhsware Aug 5, 2025
a868a2e
5.1.0-beta.0
jhsware Aug 5, 2025
2a8f2a6
Fix name of variable group
jhsware Aug 5, 2025
ad87c66
Use the cet npm-publish workflow
jhsware Aug 5, 2025
557c330
Revert stack trace param in error constructors and improve errors
jhsware Aug 6, 2025
9e81489
Merge pull request #63 from KTH/refactor-stacktrace
jhsware Aug 6, 2025
3b153ca
Rate limiting can be issues in some cases so it is opt-out
jhsware Aug 6, 2025
096fe4e
Merge pull request #64 from KTH/optout-ratelimiting
jhsware Aug 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .azure/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# This pipeline publishes the `@kth/canvas-api` package to npm
pr: none
trigger: none

variables:
- group: elarande-general-params

resources:
repositories:
- repository: stratus-templates
type: git
name: Cloud Excellence Team/stratus-templates
ref: main

extends:
template: templates/security/security-scans.yml@stratus-templates
parameters:
break: false
steps:
- checkout: self
path: github

- task: NodeTool@0
inputs:
versionSource: "spec"
versionSpec: 20.x

- task: Npm@1
displayName: Run `npm ci` for `@kth/style`
inputs:
workingDir: "$(Pipeline.Workspace)/github/@kth/style"
command: ci

- task: Npm@1
displayName: Run `npm run build` for `@kth/style`
inputs:
workingDir: "$(Pipeline.Workspace)/github/@kth/style"
command: custom
customCommand: run build

- task: Npm@1
displayName: Run `npm test` for `@kth/style`
inputs:
workingDir: "$(Pipeline.Workspace)/github/@kth/style"
command: custom
customCommand: test

- task: Npm@1
displayName: Publish `@kth/style` to npm
inputs:
workingDir: "$(Pipeline.Workspace)/github/@kth/style"
command: publish
publishRegistry: useExternalRegistry
publishEndpoint: $(npmServiceConnection)
23 changes: 0 additions & 23 deletions .eslintrc

This file was deleted.

1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ build-information.js
node_modules
.env
dist
nix

# VS Code specific
.vscode
217 changes: 104 additions & 113 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,172 +4,163 @@
npm i @kth/canvas-api
```

Node.JS HTTP client (for both TypeScript and JavaScript) based on [got](https://github.com/sindresorhus/got) for the [Canvas LMS API](https://canvas.instructure.com/doc/api/)
Node.JS HTTP client (for both TypeScript and JavaScript) for the [Canvas LMS API](https://canvas.instructure.com/doc/api/)

## Getting Started

First, generate a token by going to `«YOUR CANVAS INSTANCE»/profile/settings`. For example https://canvas.kth.se/profile/settings. Then you can do something like:

```js
const canvasApiUrl = process.env.CANVAS_API_URL;
const canvasApiToken = process.env.CANVAS_API_TOKEN;
const Canvas = require("@kth/canvas-api").default;

async function start() {
console.log("Making a GET request to /accounts/1");
const canvas = new Canvas(canvasApiUrl, canvasApiToken);

const { body } = await canvas.get("accounts/1");
console.log(body);
}

start();
```

In TypeScript, use `import`:

```ts
import Canvas from "@kth/canvas-api";
import { CanvasApi } from "@kth/canvas-api";

console.log("Making a GET request to /accounts/1");
const canvas = new Canvas(canvasApiUrl, canvasApiToken);
const canvas = new CanvasApi(
"<YOUR CANVAS INSTANCE>/api/v1",
"<YOUR CANVAS TOKEN>"
);

const { body } = await canvas.get("accounts/1");
console.log(body);
const { json } = await canvas.get("accounts/1");
console.log(json);
```

## Concepts
## Features

### 🆕 New from v4. SIS Imports
### SIS Imports

This package implements one function to perform SIS Imports (i.e. call the [POST sis_imports] endpoint).
Use the method `.sisImport()`

> Note: this is the only function that calls a **specific** endpoint. For other endpoints you should use `canvas.get`, `canvas.requestUrl`, `canvas.listItems` and `canvas.listPages`
```ts
import { CanvasApi } from "@kth/canvas-api";

[post sis_imports]: https://canvas.instructure.com/doc/api/sis_imports.html#method.sis_imports_api.create
const canvas = new CanvasApi(
"<YOUR CANVAS INSTANCE>/api/v1",
"<YOUR CANVAS TOKEN>"
);

### `listItems` and `listPages`
const buffer = await readFile("<FILE PATH>");

This package does have pagination support which is offered in two methods: `listItems` and `listPages`. Let's see an example by using the `[GET /accounts/1/courses]` endpoint.
// Note: you must give the file name with the correct extension
const file = new File([buffer], "test.csv");

If you want to get all **pages** you can use `listPages`:
const { json } = await canvas.sisImport(file);
console.log(json);
```

```js
const canvas = new Canvas(canvasApiUrl, canvasApiToken);
If you need to pass extra parameters to Canvas, create a `FormData` object and pass it as `body` to the `request()` method:

const pages = canvas.listPages("accounts/1/courses");
```ts
import { CanvasApi } from "@kth/canvas-api";

const canvas = new CanvasApi(
"<YOUR CANVAS INSTANCE>/api/v1",
"<YOUR CANVAS TOKEN>"
);

const buffer = await readFile("<FILE PATH>");
const file = new File([buffer], "test.csv");
const formData = new FormData();
formData.set("attachment", file);
formData.set("key", "value");

const { json } = await canvas.request(
"accounts/1/sis_import",
"POST",
formData
);
console.log(json);
```

// Now `pages` is an iterator that goes through every page
for await (const coursesResponse of pages) {
// `courses` is the Response object that contains a list of courses
const courses = coursesResponse.body;
### Pagination

for (const course of courses) {
console.log(course.id, course.name);
}
}
```
Use the method `.listPages` to automatically traverse pages.

To avoid writing two `for` loops like above, you can call `listItems`, that iterates elements instead of pages. The following code does exactly the same as before. Note that in this case, you will not have the `Response` object:
```ts
import { CanvasApi } from "@kth/canvas-api";

```js
const canvas = new Canvas(canvasApiUrl, canvasApiToken);
const canvas = new CanvasApi(
"<YOUR CANVAS INSTANCE>/api/v1",
"<YOUR CANVAS TOKEN>"
);

const courses = canvas.listItems("accounts/1/courses");
const pages = canvas.listPages("accounts/1/courses");

// Now `courses` is an iterator that goes through every course
for await (const course of courses) {
console.log(course.id, course.name);
for await (const { json } of pages) {
console.log(json);
}
```

[get /accounts/1/courses]: https://canvas.instructure.com/doc/api/accounts.html#method.accounts.courses_api
If the page returns a list of items, you can use `.listItems` to traverse through the items.

### Typescript support

This package does not contain type definitions to the objects returned by Canvas. If you want such types, you must define them yourself and pass it as type parameter to the methods in this library.

For example, to get typed "account" objects:
Note: the returned iterator does not include response headers

```ts
// First you define the "Account" type (or interface)
// following the Canvas API docs: https://canvas.instructure.com/doc/api/accounts.html
interface CanvasAccount {
id: number;
name: string;
workflow_state: string;
}
import { CanvasApi } from "@kth/canvas-api";

// Then, you can call our methods by passing your custom type as type parameter
const { body } = await canvas.get<CanvasAccount>("accounts/1");
const canvas = new CanvasApi(
"<YOUR CANVAS INSTANCE>/api/v1",
"<YOUR CANVAS TOKEN>"
);

const courses = canvas.listItems("accounts/1/courses");

console.log(body);
for await (const course of courses) {
console.log(course);
}
```

### Error handling
### Rate Limiting

By default, this library throws `CanvasApiError` exceptions when it gets a non-200 HTTP response from the Canvas API. You can catch those exceptions with any of the methods:
CanvasApi will automatically handle rate limiting by throttling calls. Throttling is applied globally for all your CanvasApi instances. This is done using a FIFO-queue where each call is resolved sequentially. If you have slow calls that you want to resolve in parallell you can disable throttling:

```ts
const canvas = new Canvas(canvasApiUrl, "-------");
const pages = canvas.listPages("accounts/1/courses");
const canvas = new CanvasApi("https://canvas.local/", "");

try {
for await (const coursesResponse of pages) {
const courses = coursesResponse.body;

for (const course of courses) {
console.log(course.id, course.name);
}
}
} catch (err) {
if (err instanceof CanvasApiError) {
console.log(err.options.url);
console.log(err.response.statusCode);
console.log(err.message);
}
}
const canvasWithoutThrottling = new CanvasApi("https://canvas.local/", "", {
disableThrottling: true,
});
```

#### Shorter error objects
You can mix instances with and without throttling.

### Type safety

By default, `CanvasApiError` thrown by this library contains a property `response` with a very big object. If you would like to have a smaller `response` in the error object, you can modify the `errorHandler` property:
This library parses JSON responses from Canvas and converts them to JavaScript object. If you want to check types at runtime, use a validation library:

```ts
import CanvasApi, { minimalErrorHandler } from "@kth/canvas-api";
const canvas = new CanvasApi("...");
canvas.errorHandler = minimalErrorHandler;
import { CanvasApi } from "@kth/canvas-api";
import { z } from "zod";

const canvas = new CanvasApi(
"<YOUR CANVAS INSTANCE>/api/v1",
"<YOUR CANVAS TOKEN>"
);
const accountSchema = z.object({
id: z.number(),
name: z.string(),
workflow_state: z.string(),
});

const { json } = client.get("accounts/1");
const parsed = accountSchema.parse(json);
```

#### Custom error objects

You can also pass a custom function in the `.errorHandler` property: that function will be called with whatever is thrown by `got`. Read more about [errors in Got here](https://github.com/sindresorhus/got/blob/main/documentation/8-errors.md)

Notes:

- Argument `err` in the custom handler will be the error thrown by `got`, so it will never be `CanvasApiError`
- Make sure the function you pass never returns something.
### Error handling

You can use this function to create your own error objects:
This library returns instances of `CanvasApiError`. Check the [file `src/canvasApiError.ts`](./src/canvasApiError.ts) to see all the error classes that this library throws

```ts
import CanvasApi from "@kth/canvas-api";
## Development

const canvas = new CanvasApi("...");
### Dev-Env as code with `nix-shell`

canvas.errorHandler = function customHandler(err: unknown): never {
if (err instanceof HTTPError) {
throw new CustomError(`Oh! An error! ${err.message}`);
}
We use nix package manager to get a consistent developer experience across devices (Linux/macOS):

throw err;
};
```
- shell.nix -- equivalent of package.json but for system packages
- .nix/source.json -- equivalent of package-lock.json but pinned to a commit in the nix package repo

## Design philosophy
[Installing the Required Nix Tools](https://confluence.sys.kth.se/confluence/pages/viewpage.action?pageId=193409170) and setting up your editor. This page also contains instructions or pointers for how to set up your editor.

1. **Do not implement every endpoint**. This package does **not** implement every endpoint in Canvas API This package also does not implement type definitions for objects returned by any endpoint nor definition for parameters. That would make it unmaintainable.
Run `nix-shell` in the root directory and it will install the required packages for the project. You won't need nvm or similar to switch Node.js version and you will get the correct version of Node.js, az, openssl, etc.

2. **Offer "lower-level" API** instead of trying to implement every possible feature, expose the "internals" to make it easy to extend.
#### Setting up your own environment

Example: you can use `.client` to get the `Got` instance that is used internally. With such object, you have access to all options given by the library [got](https://github.com/sindresorhus/got)
The Nixpkgs-setup is a declarative configuration of the development environment. You can choose to install the packages manually on your local system.
12 changes: 12 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// @ts-check

import eslint from "@eslint/js";
import tseslint from "typescript-eslint";

export default tseslint.config(
{
ignores: ["dist/"],
},
eslint.configs.recommended,
...tseslint.configs.recommended
);
Loading