Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V5 #55

Open
wants to merge 63 commits into
base: master
Choose a base branch
from
Open

V5 #55

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 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
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
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
47 changes: 47 additions & 0 deletions .azure/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# This pipeline publishes the `@kth/canvas-api` package to npm
pr: none
trigger:
branches:
include:
- main
- v5

variables:
- group: team-elarande-general-params

steps:
- checkout: self
path: github

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

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

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

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

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

This file was deleted.

206 changes: 83 additions & 123 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,172 +4,132 @@
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

### 🆕 New from v4. SIS Imports

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

> 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`
## Features

[post sis_imports]: https://canvas.instructure.com/doc/api/sis_imports.html#method.sis_imports_api.create
### SIS Imports

### `listItems` and `listPages`
Use the method `.sisImport()`

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.

If you want to get all **pages** you can use `listPages`:
```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 pages = canvas.listPages("accounts/1/courses");
const buffer = await readFile("<FILE PATH>");

// 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;
// Note: you must give the file name with the correct extension
const file = new File([buffer], "test.csv");

for (const course of courses) {
console.log(course.id, course.name);
}
}
const { json } = await canvas.sisImport(file);
console.log(json);
```

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:

```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 courses = canvas.listItems("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);
}
```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);
```

[get /accounts/1/courses]: https://canvas.instructure.com/doc/api/accounts.html#method.accounts.courses_api
### Pagination

### 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:
Use the method `.listPages` to automatically traverse pages.

```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;
}

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

console.log(body);
```

### Error handling
import { CanvasApi } from "@kth/canvas-api";

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:
const canvas = new CanvasApi(
"<YOUR CANVAS INSTANCE>/api/v1",
"<YOUR CANVAS TOKEN>"
);

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

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);
}
for await (const { json } of pages) {
console.log(json);
}
```

#### Shorter error objects
If the page returns a list of items, you can use `.listItems` to traverse through the items.

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:
Note: the returned iterator does not include response headers

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

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

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)
const courses = canvas.listItems("accounts/1/courses");

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

- 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.
### Type safety

You can use this function to create your own error objects:
This library parses JSON responses from Canvas and convert them as JavaScript object. If you want to check types in runtime, use a library like Zod:

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

const canvas = new CanvasApi("...");

canvas.errorHandler = function customHandler(err: unknown): never {
if (err instanceof HTTPError) {
throw new CustomError(`Oh! An error! ${err.message}`);
}

throw err;
};
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);
```

## Design philosophy

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.

2. **Offer "lower-level" API** instead of trying to implement every possible feature, expose the "internals" to make it easy to extend.
### Error handling

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)
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
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
);
10 changes: 4 additions & 6 deletions examples/error-handling-1.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@
/* eslint-disable */
require("dotenv").config();
const canvasApiUrl = process.env.CANVAS_API_URL;
const canvasApiToken = process.env.CANVAS_API_TOKEN;
const Canvas = require("../dist/index").default;
const { CanvasApiError } = require("../dist/index");
const { CanvasApi, CanvasApiResponseError } = require("@kth/canvas-api");

async function start() {
const canvas = new Canvas(canvasApiUrl, "-------");
const canvas = new CanvasApi(canvasApiUrl, "-------");
const pages = canvas.listPages("accounts/1/courses");

// Now `pages` is an iterator that goes through every page
Expand All @@ -21,10 +19,10 @@ async function start() {
}
}
} catch (err) {
if (err instanceof CanvasApiError) {
console.log(err.options.url);
if (err instanceof CanvasApiResponseError) {
console.log(err.response.statusCode);
console.log(err.message);
console.log(err.response.text);
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions examples/get-root-account.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
require("dotenv").config();
const canvasApiUrl = process.env.CANVAS_API_URL;
const canvasApiToken = process.env.CANVAS_API_TOKEN;
const Canvas = require("../dist/index").default;
const { CanvasApi } = require("@kth/canvas-api");

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

const { body } = await canvas.get("accounts/1");
console.log(body);
Expand Down
4 changes: 2 additions & 2 deletions examples/list-courses-1.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
require("dotenv").config();
const canvasApiUrl = process.env.CANVAS_API_URL;
const canvasApiToken = process.env.CANVAS_API_TOKEN;
const Canvas = require("../dist/index").default;
const { CanvasApi } = require("@kth/canvas-api");

async function start() {
const canvas = new Canvas(canvasApiUrl, canvasApiToken);
const canvas = new CanvasApi(canvasApiUrl, canvasApiToken);

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

Expand Down
Loading