From f0231d225164c5d5943adaa249ba152d4f1b75b9 Mon Sep 17 00:00:00 2001 From: John Agan Date: Wed, 16 Nov 2022 08:54:43 -0800 Subject: [PATCH] Added User Access Groups support and improved test coverage (#75) * added access groups endpoint * added: improved prettier support * added: improved lint testing * improved: tests and coverage * corrected formatting (prettier) * fixed: replaced any types where possible * fixed: added eslint rules to allow unsafe args/assignments * added back jest typings for GH Actions * updated: consolidated github actions into a single file * fixed: give github actions a name * fixed: added names for each test --- .editorconfig | 2 - .eslintignore | 1 - .eslintrc | 34 +++++++--- .github/workflows/code_quality.yml | 102 +++++++++++++++++++++++++++++ .github/workflows/main.yml | 21 ------ .gitignore | 65 +++++++++++++++++- .npmignore | 2 - .prettierignore | 5 ++ .prettierrc | 6 ++ README.md | 58 +++++++++++----- package.json | 18 +++-- src/api/collection.ts | 6 +- src/api/item.ts | 28 +++++--- src/api/oauth.ts | 17 +++-- src/api/site.ts | 5 +- src/api/user.ts | 44 ++++++++++++- src/api/webhook.ts | 12 +++- src/core/error.ts | 2 +- src/core/webflow.ts | 48 +++++++++++++- tests/api/user.test.ts | 21 +++++- tests/core/error.test.ts | 19 ++++++ tests/core/response.test.ts | 36 ++++++++++ tests/fixtures/item.fixture.ts | 12 +++- tests/fixtures/site.fixture.ts | 9 ++- tests/fixtures/user.fixture.ts | 2 +- tests/index.test.js | 23 ------- tests/webflow.test.ts | 26 +++++++- tsconfig.eslint.json | 7 ++ tsconfig.json | 2 +- yarn.lock | 8 +-- 30 files changed, 518 insertions(+), 123 deletions(-) delete mode 100644 .editorconfig delete mode 100644 .eslintignore create mode 100644 .github/workflows/code_quality.yml delete mode 100644 .github/workflows/main.yml delete mode 100644 .npmignore create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 tests/core/error.test.ts create mode 100644 tests/core/response.test.ts delete mode 100644 tests/index.test.js create mode 100644 tsconfig.eslint.json diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 9757c273..00000000 --- a/.editorconfig +++ /dev/null @@ -1,2 +0,0 @@ -[*] -max_line_length = 80 \ No newline at end of file diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 8d87b1d2..00000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -node_modules/* diff --git a/.eslintrc b/.eslintrc index ecf76a16..1b92771e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,15 +1,31 @@ { - "root": true, + "env": { + "es2021": true, + "browser": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking" + ], "parser": "@typescript-eslint/parser", - "ignorePatterns": ["dist", "**/*.d.ts"], - "plugins": ["prettier", "@typescript-eslint"], - "extends": ["plugin:@typescript-eslint/recommended", "prettier"], "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module" + "ecmaVersion": 12, + "project": "tsconfig.eslint.json" }, + "plugins": ["@typescript-eslint", "prettier"], "rules": { - "@typescript-eslint/no-explicit-any": "warn", - "prettier/prettier": ["error"] - } + "prefer-const": "error", + "prettier/prettier": "error", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-unused-params": "off", + "@typescript-eslint/no-unsafe-argument": "warn", + "@typescript-eslint/no-unsafe-assignment": "warn" + }, + "overrides": [ + { + "env": { "jest": true, "node": true }, + "files": ["tests/**/*.ts"] + } + ] } diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml new file mode 100644 index 00000000..5541e7ef --- /dev/null +++ b/.github/workflows/code_quality.yml @@ -0,0 +1,102 @@ +name: Code Quality + +on: push + +jobs: + build: + name: TypeScript Build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install Node.js + uses: actions/setup-node@v1 + with: + node-version: 14.x + + - name: yarn install + run: yarn install + + - run: yarn build + name: yarn build + + typecheck: + name: TypeScript Typecheck + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install Node.js + uses: actions/setup-node@v1 + with: + node-version: 14.x + + - name: yarn install + run: yarn install + + - run: yarn typecheck + name: yarn typecheck + + test: + name: Jest CI Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install Node.js + uses: actions/setup-node@v1 + with: + node-version: 14.x + + - name: yarn install + run: yarn install + + - name: yarn build + run: yarn build + + - run: yarn test:ci + name: yarn test:ci + + prettier: + name: Prettier Formatting + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install Node.js + uses: actions/setup-node@v1 + with: + node-version: 14.x + + - name: yarn install + run: yarn install + + - run: yarn format:check + name: yarn format:check + + lint: + name: ESLint Linting + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install Node.js + uses: actions/setup-node@v1 + with: + node-version: 14.x + + - name: yarn install + run: yarn install + + - run: yarn lint + name: yarn lint diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index e3ec40fa..00000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Tests - -on: [push] - -jobs: - test: - name: Jest - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - - run: yarn install - - run: yarn test - lint: - name: ESLint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - - run: yarn install - - run: yarn test diff --git a/.gitignore b/.gitignore index 21aff38f..d88e57b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,63 @@ -node_modules/ -dist/ +# Logs +logs *.log -.env \ No newline at end of file +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test +.env.production + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +temp +.DS_Store + +# Compiled files +dist/ \ No newline at end of file diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 064798d3..00000000 --- a/.npmignore +++ /dev/null @@ -1,2 +0,0 @@ -tests/ -src/ \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..9814b5e5 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +dist +temp +*.json +coverage +.DS_Store diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..1f2718fe --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": true, + "useTabs": false, + "printWidth": 100, + "trailingComma": "all" +} diff --git a/README.md b/README.md index da7e8c0e..c9014abd 100644 --- a/README.md +++ b/README.md @@ -9,18 +9,20 @@ $ npm install webflow-api ``` using yarn + ``` $ yarn add webflow-api ``` ## Usage + The constructor takes in a few optional parameters to initialize the API client -* `token` - the access token to use -* `headers` - additional headers to add to the request -* `version` - the version of the API you wish to use +- `token` - the access token to use +- `headers` - additional headers to add to the request +- `version` - the version of the API you wish to use -``` javascript +```javascript const Webflow = require("webflow-api"); // initialize the client with the access token @@ -31,12 +33,15 @@ const webflow = new Webflow({ token: "[ACCESS TOKEN]", version: "1.0.0", headers: { - "User-Agent": "My Webflow App / 1.0" - } + "User-Agent": "My Webflow App / 1.0", + }, }); ``` + ## Basic Usage + ### Chaining Calls + You can retrieve child resources by chaining calls on the parent object. ```javascript @@ -54,6 +59,7 @@ const item = await collection.items({ itemId: "[ITEM ID]" }); ``` ### Pagination + To paginate results, pass in the `limit` and `offset` options. ```javascript @@ -65,6 +71,7 @@ const page2 = await collection.items({ limit: 20, offset: 20 }); ``` ### Rate Limit + Check rate limit status on each call by checking the `_meta` property. ```javascript @@ -74,7 +81,9 @@ const site = await webflow.site({ siteId: "[SITE ID]" }); // check rate limit const { rateLimit } = site._meta; // { limit: 60, remaining: 56 } ``` + ### Update Token + If you need to update the access token, you can set the `token` property at any time. ```javascript @@ -87,7 +96,9 @@ webflow.token = "[ACCESS TOKEN]"; // remove the token webflow.clearToken(); ``` + ### Calling APIs Directly + All Webflow API endpoints can be called directly using the `get`, `post`, `put`, and `delete` methods. ```javascript @@ -96,14 +107,16 @@ const sites = await webflow.get("/sites"); // post to an endpoint directly const result = await webflow.post("/sites/[SITE ID]/publish", { - domains: ["hello-webflow.com"] + domains: ["hello-webflow.com"], }); ``` ## OAuth + To implement OAuth, you'll need a Webflow App registered and a webserver running, that is publicly facing. ### Authorize + The first step in OAuth is to generate an authorization url to redirect the user to. ```javascript @@ -111,7 +124,7 @@ The first step in OAuth is to generate an authorization url to redirect the user const url = webflow.authorizeUrl({ client_id: "[CLIENT ID]", state: "1234567890", // optional - redirect_uri: "https://my.server.com/oauth/callback" // optional + redirect_uri: "https://my.server.com/oauth/callback", // optional }); // redirect user from your server route @@ -119,6 +132,7 @@ res.redirect(url); ``` ### Access Token + Once a user has authorized their Webflow resource(s), Webflow will redirect back to your server with a `code`. Use this to get an access token. ```javascript @@ -126,7 +140,7 @@ const auth = await webflow.accessToken({ client_id, client_secret, code, - redirect_uri // optional - required if used in the authorize step + redirect_uri, // optional - required if used in the authorize step }); // you now have the user's access token to make API requests with @@ -137,21 +151,24 @@ const authenticatedUser = await userWF.authenticatedUser(); ``` ### Revoke Token + If the user decides to disconnect from your server, you should call revoke token to remove the authorization. ```javascript const result = await webflow.revokeToken({ client_id, client_secret, - access_token + access_token, }); // ensure it went through -result.didRevoke === true +result.didRevoke === true; ``` ## Examples + ### Sites + Get all sites available or lookup by site id. ```javascript @@ -163,7 +180,9 @@ const site = await webflow.sites({ siteId: "[SITE ID]" }); ``` ### Collections + Get all collections available for a site or lookup by collection id. + ```javascript // Get a site's collection from the site const collections = await site.collections(); @@ -176,7 +195,9 @@ const collection = await webflow.collection({ collectionId: "[COLLECTION ID]" }) ``` ### Collection Items + Get all collection items available for a collection or lookup by item id. + ```javascript // Get the items from a collection const items = await collection.items(); @@ -187,7 +208,9 @@ const items = await collection.items({ limit: 10, offset: 2 }); // Get a single item const item = await webflow.item({ collectionId: "[COLLECTION ID]", itemId: "[ITEM ID]" }); ``` + ### Update an Item + ```javascript // Set the fields to update const fields = { @@ -206,19 +229,20 @@ const updatedItem = await webflow.updateItem({ ``` ### Memberships + ```javascript // Get a site's users from the site const users = await site.users(); // Get a site's users with a site id const users = await webflow.users({ - siteId: "[SITE ID]" + siteId: "[SITE ID]", }); // Get a single user const user = await site.user({ siteId: "[SITE ID]", - userId: "[USER ID]" + userId: "[USER ID]", }); // Get a site's access groups @@ -226,11 +250,12 @@ const accessGroups = await site.accessGroups(); // Get a site's access groups with a site id const accessGroups = await webflow.accessGroups({ - siteId: "[SITE ID]" + siteId: "[SITE ID]", }); ``` ### Webhooks + ```javascript // get webhooks for a site const webhooks = await site.webhooks(); @@ -238,18 +263,17 @@ const webhooks = await site.webhooks(); // create a webhook const webhook = await site.createWebhook({ triggerType: "form_submission", - url: "https://webhook.site" + url: "https://webhook.site", }); - ``` ### Authenticated User + ```javascript // pull information for the authenticated user const authenticatedUser = await webflow.authenticatedUser(); ``` - ## Contributing Contributions are welcome - feel free to open an issue or pull request. diff --git a/package.json b/package.json index dbe2fa8d..b1e1420e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "webflow-api", "description": "Webflow's official Node.js SDK for Data APIs", - "version": "1.2.0", + "version": "1.2.1", "types": "dist/index.d.ts", "main": "dist/index.js", "contributors": [ @@ -19,15 +19,21 @@ "yarn.lock" ], "scripts": { - "build": "yarn clean && tsc -p ./", - "lint": "eslint . --ext .ts", + "test": "yarn build && jest", + "build": "yarn clean && tsc", + "lint": "eslint src --ext .ts", + "test:snapshot": "yarn test --updateSnapshot", + "test:coverage": "yarn test --collectCoverage", + "test:ci": "yarn test --ci --coverage --forceExit", + "format": "prettier --write .", + "format:check": "prettier --check .", "prepublish": "yarn build", - "watch": "tsc -watch -p ./", + "watch": "tsc --watch", "clean": "rm -rf dist", - "test": "jest" + "typecheck": "tsc --noEmit" }, "devDependencies": { - "@types/jest": "^29.2.1", + "@types/jest": "^29.2.3", "@types/node": "^18.11.9", "@typescript-eslint/eslint-plugin": "^5.42.0", "@typescript-eslint/parser": "^5.42.0", diff --git a/src/api/collection.ts b/src/api/collection.ts index cf6f49a1..166af745 100644 --- a/src/api/collection.ts +++ b/src/api/collection.ts @@ -32,7 +32,7 @@ export type CollectionField = { required: boolean; editable: boolean; // TODO: add a better type - validations?: any; + validations?: Record; }; /************************************************************** @@ -134,7 +134,7 @@ export class Collection extends WebflowRecord implements ICollectio * @param fields The Item fields to create * @returns The created Item */ - async createItem(fields: any) { + async createItem(fields: object) { const res = await Item.create({ collectionId: this._id, fields }, this.client); return new Item(this.client, res); } @@ -146,7 +146,7 @@ export class Collection extends WebflowRecord implements ICollectio * @param params.fields The fields to update * @returns The updated Item */ - async updateItem({ itemId, fields }: { itemId: string; fields: any }) { + async updateItem({ itemId, fields }: { itemId: string; fields: object }) { const params = { itemId, collectionId: this._id, fields }; const res = await Item.update(params, this.client); return new Item(this.client, res); diff --git a/src/api/item.ts b/src/api/item.ts index 83bfa54a..3f2196c1 100644 --- a/src/api/item.ts +++ b/src/api/item.ts @@ -70,7 +70,10 @@ export class Item extends WebflowRecord implements IItem { * @param client The Axios client instance * @returns A single Item */ - static getOne({ collectionId, itemId }: { collectionId: string; itemId: string }, client: AxiosInstance) { + static getOne( + { collectionId, itemId }: { collectionId: string; itemId: string }, + client: AxiosInstance, + ) { requireArgs({ collectionId, itemId }); const path = `/collections/${collectionId}/items/${itemId}`; // The API returns a paginated list with one record :( @@ -88,7 +91,7 @@ export class Item extends WebflowRecord implements IItem { */ static list( { collectionId, limit, offset }: { collectionId: string; limit?: number; offset?: number }, - client: AxiosInstance + client: AxiosInstance, ) { requireArgs({ collectionId }); const params = { limit, offset }; @@ -104,7 +107,10 @@ export class Item extends WebflowRecord implements IItem { * @param client The Axios client instance * @returns The created Item */ - static create({ collectionId, fields }: { fields: any; collectionId: string }, client: AxiosInstance) { + static create( + { collectionId, fields }: { fields: any; collectionId: string }, + client: AxiosInstance, + ) { requireArgs({ collectionId }); const path = `/collections/${collectionId}/items`; return client.post(path, { fields }); @@ -129,7 +135,7 @@ export class Item extends WebflowRecord implements IItem { itemId: string; collectionId: string; }, - client: AxiosInstance + client: AxiosInstance, ) { requireArgs({ collectionId, itemId }); const path = `/collections/${collectionId}/items/${itemId}`; @@ -155,7 +161,7 @@ export class Item extends WebflowRecord implements IItem { itemId: string; collectionId: string; }, - client: AxiosInstance + client: AxiosInstance, ) { requireArgs({ collectionId, itemId }); const path = `/collections/${collectionId}/items/${itemId}`; @@ -178,7 +184,7 @@ export class Item extends WebflowRecord implements IItem { itemId: string; collectionId: string; }, - client: AxiosInstance + client: AxiosInstance, ) { requireArgs({ collectionId, itemId }); const path = `/collections/${collectionId}/items/${itemId}`; @@ -203,7 +209,7 @@ export class Item extends WebflowRecord implements IItem { itemIds: string[]; collectionId: string; }, - client: AxiosInstance + client: AxiosInstance, ) { requireArgs({ collectionId, itemIds }); const params = { live }; @@ -226,8 +232,12 @@ export class Item extends WebflowRecord implements IItem { * @returns The result of the publish */ static publish( - { itemIds, live = false, collectionId }: { live?: boolean; itemIds: string[]; collectionId: string }, - client: AxiosInstance + { + itemIds, + live = false, + collectionId, + }: { live?: boolean; itemIds: string[]; collectionId: string }, + client: AxiosInstance, ) { requireArgs({ collectionId, itemIds }); const params = { live }; diff --git a/src/api/oauth.ts b/src/api/oauth.ts index 96b06e46..5ae3a42b 100644 --- a/src/api/oauth.ts +++ b/src/api/oauth.ts @@ -53,7 +53,7 @@ export class OAuth { */ static authorizeUrl( { response_type = "code", redirect_uri, client_id, state, scope }: IAuthorizeUrlParams, - client: AxiosInstance + client: AxiosInstance, ) { requireArgs({ client_id }); @@ -78,8 +78,14 @@ export class OAuth { * @returns An access token */ static accessToken( - { grant_type = "authorization_code", client_secret, redirect_uri, client_id, code }: IAccessTokenParams, - client: AxiosInstance + { + grant_type = "authorization_code", + client_secret, + redirect_uri, + client_id, + code, + }: IAccessTokenParams, + client: AxiosInstance, ) { requireArgs({ client_id, client_secret, code }); @@ -97,7 +103,10 @@ export class OAuth { * @param client The Axios client instance * @returns The result of the revoke */ - static revokeToken({ client_secret, access_token, client_id }: IRevokeTokenParams, client: AxiosInstance) { + static revokeToken( + { client_secret, access_token, client_id }: IRevokeTokenParams, + client: AxiosInstance, + ) { requireArgs({ client_id, client_secret, access_token }); const path = "/oauth/revoke_authorization"; diff --git a/src/api/site.ts b/src/api/site.ts index b03eb619..3245ccda 100644 --- a/src/api/site.ts +++ b/src/api/site.ts @@ -71,7 +71,10 @@ export class Site extends WebflowRecord implements ISite { * @param client The Axios client instance * @returns The publish result */ - static publish({ siteId, domains }: { siteId: string; domains: string[] }, client: AxiosInstance) { + static publish( + { siteId, domains }: { siteId: string; domains: string[] }, + client: AxiosInstance, + ) { requireArgs({ siteId, domains }); const path = `/sites/${siteId}/publish`; return client.post(path, { domains }); diff --git a/src/api/user.ts b/src/api/user.ts index 85f0240d..df4d77e5 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -12,6 +12,14 @@ export interface IUser { data: any; } +export interface IAcessGroup { + _id: string; + name: string; + shortId: string; + slug: string; + createdOn: string; +} + export interface IUserDelete { deleted: number; } @@ -23,6 +31,10 @@ export type PaginatedUsers = PaginatedData & { users: IUser[]; }; +export type PaginatedAccessGroups = PaginatedData & { + accessGroups: IAcessGroup[]; +}; + export type UserIdParam = { siteId: string; userId: string }; /************************************************************** @@ -50,7 +62,10 @@ export class User extends WebflowRecord implements IUser { * @param client The Axios client instance * @returns A list of Users */ - static list({ siteId, limit, offset }: { siteId: string; limit?: number; offset?: number }, client: AxiosInstance) { + static list( + { siteId, limit, offset }: { siteId: string; limit?: number; offset?: number }, + client: AxiosInstance, + ) { requireArgs({ siteId }); const params = { limit, offset }; const path = `/sites/${siteId}/users`; @@ -90,7 +105,7 @@ export class User extends WebflowRecord implements IUser { siteId: string; userId: string; }, - client: AxiosInstance + client: AxiosInstance, ) { requireArgs({ siteId, userId }); const path = `/sites/${siteId}/users/${userId}`; @@ -125,6 +140,31 @@ export class User extends WebflowRecord implements IUser { return client.delete(path); } + /** + * Get a list of User Access Groups + * @param params The params for the request + * @param params.siteId The site ID + * @param params.limit The number of items to return (optional) + * @param params.offset The number of items to skip (optional) + * @param params.sort The sort order of the groups (optional) + * @param client The Axios client instance + * @returns A list of Access Groups + */ + static accessGroups( + { + siteId, + limit, + offset, + sort, + }: { siteId: string; limit?: number; offset?: number; sort?: string }, + client: AxiosInstance, + ) { + requireArgs({ siteId }); + const params = { limit, offset, sort }; + const path = `/sites/${siteId}/accessgroups`; + return client.get(path, { params }); + } + /************************************************************** * Instance Methods **************************************************************/ diff --git a/src/api/webhook.ts b/src/api/webhook.ts index 97c7508c..7af10f04 100644 --- a/src/api/webhook.ts +++ b/src/api/webhook.ts @@ -75,7 +75,10 @@ export class Webhook extends WebflowRecord implements IWebhook { * @param client The Axios client instance * @returns A single Webhook */ - static getOne({ siteId, webhookId }: { siteId: string; webhookId: string }, client: AxiosInstance) { + static getOne( + { siteId, webhookId }: { siteId: string; webhookId: string }, + client: AxiosInstance, + ) { requireArgs({ siteId, webhookId }); const path = `/sites/${siteId}/webhooks/${webhookId}`; return client.get(path); @@ -104,7 +107,7 @@ export class Webhook extends WebflowRecord implements IWebhook { filter?: WebhookFilter; triggerType: TriggerType; }, - client: AxiosInstance + client: AxiosInstance, ) { requireArgs({ siteId, triggerType, url }); const path = `/sites/${siteId}/webhooks`; @@ -120,7 +123,10 @@ export class Webhook extends WebflowRecord implements IWebhook { * @param client The Axios client instance * @returns The result of the removal */ - static remove({ siteId, webhookId }: { siteId: string; webhookId: string }, client: AxiosInstance) { + static remove( + { siteId, webhookId }: { siteId: string; webhookId: string }, + client: AxiosInstance, + ) { requireArgs({ siteId, webhookId }); const path = `/sites/${siteId}/webhooks/${webhookId}`; return client.delete(path); diff --git a/src/core/error.ts b/src/core/error.ts index 7f429c7a..e27126f2 100644 --- a/src/core/error.ts +++ b/src/core/error.ts @@ -34,7 +34,7 @@ export function requireArgs(args: object) { } // throw an error if Webflow error -export function ErrorInterceptor(res: AxiosResponse) { +export function ErrorInterceptor(res: AxiosResponse) { if (res.data.err) throw new RequestError(res.data); return res; } diff --git a/src/core/webflow.ts b/src/core/webflow.ts index b53bc6ba..2ace899f 100644 --- a/src/core/webflow.ts +++ b/src/core/webflow.ts @@ -1,5 +1,5 @@ import axios, { AxiosInstance, AxiosRequestConfig } from "axios"; -import { PaginationFilter } from "../core"; +import { PaginationFilter, ErrorInterceptor } from "../core"; import { Collection, IAccessTokenParams, @@ -31,6 +31,7 @@ export class Webflow { private client: AxiosInstance; constructor(public options: Options = {}) { this.client = axios.create(this.config); + this.client.interceptors.response.use(ErrorInterceptor); } // Set the Authentication token @@ -334,7 +335,15 @@ export class Webflow { * @param params.live Update the live version * @returns The unpublished Collection Item result */ - async deleteItems({ collectionId, itemIds, live }: { collectionId: string; itemIds: string[]; live?: boolean }) { + async deleteItems({ + collectionId, + itemIds, + live, + }: { + collectionId: string; + itemIds: string[]; + live?: boolean; + }) { const res = await Item.unpublish({ collectionId, itemIds, live }, this.client); return res.data; } @@ -346,7 +355,15 @@ export class Webflow { * @param params.live Update the live version * @returns The Published Collection Item result */ - async publishItems({ collectionId, itemIds, live }: { collectionId: string; itemIds: string[]; live?: boolean }) { + async publishItems({ + collectionId, + itemIds, + live, + }: { + collectionId: string; + itemIds: string[]; + live?: boolean; + }) { const res = await Item.publish({ collectionId, itemIds, live }, this.client); return res.data; } @@ -416,6 +433,31 @@ export class Webflow { return res.data; } + /** + * Get a list of User Access Groups + * @param params The params for the request + * @param params.siteId The site ID + * @param params.limit The number of items to return (optional) + * @param params.offset The number of items to skip (optional) + * @param params.sort The sort order of the groups (optional) + * @returns A list of Access Groups + */ + async accessGroups({ + siteId, + limit, + offset, + sort, + }: { + siteId: string; + limit?: number; + offset?: number; + sort?: string; + }) { + const params = { siteId, limit, offset, sort }; + const res = await User.accessGroups(params, this.client); + return res.data; + } + /************************************************************** * Webhook Endpoints **************************************************************/ diff --git a/tests/api/user.test.ts b/tests/api/user.test.ts index 6e1f6c44..d7db709e 100644 --- a/tests/api/user.test.ts +++ b/tests/api/user.test.ts @@ -72,11 +72,30 @@ describe("Users", () => { expect(data).toBeDefined(); expect(data.deleted).toBe(response.deleted); }); + + it("should respond with a list of access groups", async () => { + const { response, parameters } = UserFixture.accessGroups; + const { siteId } = parameters; + const path = `/sites/${siteId}/accessgroups`; + + mock.onGet(path).reply(200, response); + const { data } = await User.accessGroups(parameters, client); + + expect(data).toBeDefined(); + expect(data.accessGroups.length).toBe(response.accessGroups.length); + expect(data.accessGroups[0]).toMatchObject(response.accessGroups[0]); + }); }); describe("Instance Methods", () => { const { parameters, response } = UserFixture.getOne; - const res = { data: {}, status: 200, statusText: "", headers: {}, config: {} }; + const res = { + data: {}, + status: 200, + statusText: "", + headers: {}, + config: {}, + }; const user = new User(client, res, response, { siteId: parameters.siteId }); it("should update a user", async () => { diff --git a/tests/core/error.test.ts b/tests/core/error.test.ts new file mode 100644 index 00000000..6cc114c0 --- /dev/null +++ b/tests/core/error.test.ts @@ -0,0 +1,19 @@ +import { ArgumentError, requireArgs } from "../../src/core/error"; +import MockAdapter from "axios-mock-adapter"; +import axios from "axios"; + +describe("Error", () => { + const mock = new MockAdapter(axios); + + it("should throw an ArgumentError", () => { + expect(() => { + throw new ArgumentError("name"); + }).toThrow("Argument 'name' is required but was not present"); + }); + + it("should throw an ArgumentError for required args", () => { + expect(() => { + requireArgs({ name: undefined }); + }).toThrow("Argument 'name' is required but was not present"); + }); +}); diff --git a/tests/core/response.test.ts b/tests/core/response.test.ts new file mode 100644 index 00000000..52b68d69 --- /dev/null +++ b/tests/core/response.test.ts @@ -0,0 +1,36 @@ +import axios, { AxiosResponse } from "axios"; +import { WebflowRecord, MetaResponse } from "../../src/core/response"; + +describe("Response", () => { + const client = axios.create(); + const response: AxiosResponse = { + status: 200, + statusText: "OK", + headers: { + "x-ratelimit-limit": "100", + "x-ratelimit-remaining": "99", + }, + config: {}, + data: {}, + }; + + it("should create a MetaResponse", () => { + const meta = new MetaResponse(response); + expect(meta.rateLimit.limit).toBe(100); + expect(meta.rateLimit.remaining).toBe(99); + }); + + it("should create a WebflowRecord", () => { + const record = new WebflowRecord(client, response, { id: "123" }); + + // confirm client + expect(record.client).toBe(client); + + // confirm response + expect(record.response).toBe(response); + + // confirm meta records + expect(record._meta.rateLimit.limit).toBe(100); + expect(record._meta.rateLimit.remaining).toBe(99); + }); +}); diff --git a/tests/fixtures/item.fixture.ts b/tests/fixtures/item.fixture.ts index 03c235c9..cd1be19c 100644 --- a/tests/fixtures/item.fixture.ts +++ b/tests/fixtures/item.fixture.ts @@ -158,7 +158,11 @@ export const ItemFixture = { itemIds: ["62aa37923cf7a9de1ca4469c", "62aa37923cf7a9de1ca44697", "62aa37923cf7a9de1ca44696"], }, response: { - publishedItemIds: ["62aa37923cf7a9de1ca4469c", "62aa37923cf7a9de1ca44697", "62aa37923cf7a9de1ca44696"], + publishedItemIds: [ + "62aa37923cf7a9de1ca4469c", + "62aa37923cf7a9de1ca44697", + "62aa37923cf7a9de1ca44696", + ], errors: [], }, }, @@ -169,7 +173,11 @@ export const ItemFixture = { itemIds: ["62aa37923cf7a9de1ca4469c", "62aa37923cf7a9de1ca44697", "62aa37923cf7a9de1ca44696"], }, response: { - deletedItemIds: ["62aa37923cf7a9de1ca4469c", "62aa37923cf7a9de1ca44697", "62aa37923cf7a9de1ca44696"], + deletedItemIds: [ + "62aa37923cf7a9de1ca4469c", + "62aa37923cf7a9de1ca44697", + "62aa37923cf7a9de1ca44696", + ], errors: [], }, }, diff --git a/tests/fixtures/site.fixture.ts b/tests/fixtures/site.fixture.ts index bce1abdb..b6ed8a25 100644 --- a/tests/fixtures/site.fixture.ts +++ b/tests/fixtures/site.fixture.ts @@ -7,7 +7,8 @@ export const SiteFixture = { name: "api_docs_sample_json", shortName: "api-docs-sample-json", lastPublished: "2016-10-24T23:06:51.251Z", - previewUrl: "https://d1otoma47x30pg.cloudfront.net/580e63e98c9a982ac9b8b741/201610241603.png", + previewUrl: + "https://d1otoma47x30pg.cloudfront.net/580e63e98c9a982ac9b8b741/201610241603.png", timezone: "America/Los_Angeles", database: "580e63fc8c9a982ac9b8b744", }, @@ -17,7 +18,8 @@ export const SiteFixture = { name: "Copy of api_docs_sample_json", shortName: "api-docs-sample-json-086c6538f9b0583762", lastPublished: null, - previewUrl: "https://d1otoma47x30pg.cloudfront.net/580e63e98c9a982ac9b8b741/201610241603.png", + previewUrl: + "https://d1otoma47x30pg.cloudfront.net/580e63e98c9a982ac9b8b741/201610241603.png", timezone: "America/Los_Angeles", database: "580ff8c3ba3e45ba9fe588bf", }, @@ -27,7 +29,8 @@ export const SiteFixture = { name: "Copy of api_docs_sample_json", shortName: "api-docs-sample-json-ce077aa6c5cd3e0177", lastPublished: null, - previewUrl: "https://d1otoma47x30pg.cloudfront.net/580e63e98c9a982ac9b8b741/201610241603.png", + previewUrl: + "https://d1otoma47x30pg.cloudfront.net/580e63e98c9a982ac9b8b741/201610241603.png", timezone: "America/Los_Angeles", database: "580ff8d7ba3e45ba9fe588ed", }, diff --git a/tests/fixtures/user.fixture.ts b/tests/fixtures/user.fixture.ts index 77629454..b50fe856 100644 --- a/tests/fixtures/user.fixture.ts +++ b/tests/fixtures/user.fixture.ts @@ -152,7 +152,7 @@ export const UserFixture = { deleted: 1, }, }, - access_group: { + accessGroups: { parameters: { siteId: "580e63e98c9a982ac9b8b741", }, diff --git a/tests/index.test.js b/tests/index.test.js deleted file mode 100644 index 50db79db..00000000 --- a/tests/index.test.js +++ /dev/null @@ -1,23 +0,0 @@ -const Webflow = require("../dist"); - -// This is purposely a javascript file so that we can test the -// transpiled version of the code. - -describe("Webflow", () => { - it("should create a new instance of Webflow", () => { - expect(() => new Webflow()).not.toThrowError(); - }); - - it("should create a new instance of Webflow with options", () => { - const options = { token: "token", host: "test.com" }; - const webflow = new Webflow(options); - expect(webflow.options).toEqual(options); - }); - - it("should set the authorization token", () => { - const webflow = new Webflow(); - webflow.token = "token"; - - expect(webflow.options.token).toEqual("token"); - }); -}); diff --git a/tests/webflow.test.ts b/tests/webflow.test.ts index 7a43ebcf..726d21cd 100644 --- a/tests/webflow.test.ts +++ b/tests/webflow.test.ts @@ -1,6 +1,6 @@ import axios from "axios"; import MockAdapter from "axios-mock-adapter"; -import { Webflow } from "../src/core"; +import { Webflow, RequestError } from "../src/core"; import { MetaFixture, SiteFixture, @@ -82,6 +82,17 @@ describe("Webflow", () => { expect(mock.history.patch.length).toBe(1); expect(mock.history.patch[0].params).toMatchObject(query); }); + it("should throw a RequestError when Webflow returns a 200 with error", async () => { + mock.onGet("/").reply(200, { + msg: "msg", + code: 400, + name: "name", + path: "path", + err: "err", + }); + + await expect(webflow.get("/")).rejects.toThrowError(RequestError); + }); }); describe("API Calls", () => { @@ -442,6 +453,19 @@ describe("Webflow", () => { expect(result).toBeDefined(); expect(result.deleted).toBe(response.deleted); }); + + it("should respond with a list of access groups", async () => { + const { response, parameters } = UserFixture.accessGroups; + const { siteId } = parameters; + const path = `/sites/${siteId}/accessgroups`; + + mock.onGet(path).reply(200, response); + const result = await webflow.accessGroups(parameters); + + expect(result).toBeDefined(); + expect(result.accessGroups.length).toBe(response.accessGroups.length); + expect(result.accessGroups[0]).toMatchObject(response.accessGroups[0]); + }); }); describe("Webhooks", () => { diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 00000000..eef55205 --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["jest"] + }, + "include": ["src", "tests"] +} diff --git a/tsconfig.json b/tsconfig.json index 422707ac..ee0b463f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,5 +10,5 @@ "baseUrl": "src", "experimentalDecorators": true }, - "exclude": ["**/node_modules/*", "**/dist", "**/tests"] + "include": ["src"] } diff --git a/yarn.lock b/yarn.lock index 6d8d0ec9..26b68ade 100644 --- a/yarn.lock +++ b/yarn.lock @@ -678,10 +678,10 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^29.2.1": - version "29.2.1" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.2.1.tgz#31fda30bdf2861706abc5f1730be78bed54f83ee" - integrity sha512-nKixEdnGDqFOZkMTF74avFNr3yRqB1ZJ6sRZv5/28D5x2oLN14KApv7F9mfDT/vUic0L3tRCsh3XWpWjtJisUQ== +"@types/jest@^29.2.3": + version "29.2.3" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.2.3.tgz#f5fd88e43e5a9e4221ca361e23790d48fcf0a211" + integrity sha512-6XwoEbmatfyoCjWRX7z0fKMmgYKe9+/HrviJ5k0X/tjJWHGAezZOfYaxqQKuzG/TvQyr+ktjm4jgbk0s4/oF2w== dependencies: expect "^29.0.0" pretty-format "^29.0.0"