Notion authentication setup and first endpoint, but not tested yet

This commit is contained in:
Matt Aitken
2023-02-22 12:43:13 +00:00
parent 5f1ac75044
commit 8767403a44
12 changed files with 273 additions and 3 deletions
@@ -1,4 +1,4 @@
import { JSONSchema } from "./types";
import { JSONSchema, JSONSchemaInstanceType } from "./types";
export function makeStringSchema(
title: string,
@@ -139,3 +139,26 @@ export function makeAnyOf(title: string, schemas: JSONSchema[]): JSONSchema {
anyOf: schemas,
};
}
export function makeNullable(schema: JSONSchema): JSONSchema {
let combinedTypes: JSONSchemaInstanceType[] = [];
if (!schema.type) {
throw new Error("Schema must have a type");
}
switch (typeof schema.type) {
case "string":
combinedTypes = [schema.type, "null"];
break;
case "object":
combinedTypes = [...schema.type, "null"];
break;
default:
throw new Error(`Invalid schema type: ${typeof schema.type}`);
}
return {
...schema,
type: combinedTypes,
};
}
+2 -1
View File
@@ -1,4 +1,5 @@
import { Schema, OutputUnit } from "@cfworker/json-schema";
import { Schema, OutputUnit, InstanceType } from "@cfworker/json-schema";
export type JSONSchema = Schema;
export type JSONSchemaInstanceType = InstanceType;
export type JSONSchemaError = OutputUnit;
-1
View File
@@ -4,7 +4,6 @@ import { handleServices } from "api/v2/services";
import dotenv from "dotenv";
import express, { Express, NextFunction, Request, Response } from "express";
import morgan from "morgan";
dotenv.config();
const app: Express = express();
@@ -1,10 +1,12 @@
import { Catalog } from "core/catalog";
import { airtable } from "./airtable";
import { notion } from "./notion";
import { sendgrid } from "./sendgrid";
export const catalog: Catalog = {
services: {
airtable,
notion,
sendgrid,
},
};
@@ -0,0 +1,5 @@
import { makeSimpleActions } from "core/action/makeAction";
import endpoints from "../endpoints/endpoints";
const actions = makeSimpleActions(endpoints);
export default actions;
@@ -0,0 +1,16 @@
import { IntegrationAuthentication } from "core/authentication/types";
export const authentication: IntegrationAuthentication = {
oauth: {
type: "oauth2",
placement: {
in: "header",
type: "bearer",
key: "Authorization",
},
authorizationUrl: "https://api.notion.com/v1/oauth/authorize",
tokenUrl: "https://api.notion.com/v1/oauth/token",
flow: "accessCode",
scopes: {},
},
};
@@ -0,0 +1,21 @@
import { EndpointSpecParameter } from "core/endpoint/types";
import {
makeArraySchema,
makeBooleanSchema,
makeNumberSchema,
makeObjectSchema,
makeOneOf,
makeStringSchema,
} from "core/schemas/makeSchema";
import { JSONSchema } from "core/schemas/types";
export const VersionHeaderParam: EndpointSpecParameter = {
name: "Notion-Version",
in: "header",
description:
"The Notion API is versioned. Our API versions are named for the date the version is released, for example, 2022-06-28",
schema: {
type: "string",
},
required: true,
};
@@ -0,0 +1,8 @@
import { makeEndpoints } from "core/endpoint/endpoint";
import { authentication } from "../authentication";
import * as specs from "./specs";
const baseUrl = "https://api.notion.com/v1";
const endpoints = makeEndpoints(baseUrl, authentication, specs);
export default endpoints;
@@ -0,0 +1,148 @@
import { EndpointSpec, EndpointSpecResponse } from "core/endpoint/types";
import {
makeBooleanSchema,
makeNullable,
makeObjectSchema,
makeOneOf,
makeStringSchema,
} from "core/schemas/makeSchema";
import { VersionHeaderParam } from "../common/schemas";
const errorResponse: EndpointSpecResponse = {
success: false,
name: "Error",
description: "Error response",
schema: {},
};
export const getUser: EndpointSpec = {
path: "/users/{userId}",
method: "GET",
metadata: {
name: "getUser",
description: `List records in a table. Note that table names and table ids can be used interchangeably. We recommend using table IDs so you don't need to modify your API request when your table name changes.\n
The server returns one page of records at a time. Each page will contain pageSize records, which is 100 by default. If there are more records, the response will contain an offset. To fetch the next page of records, include offset in the next request's parameters. Pagination will stop when you've reached the end of your table. If the maxRecords parameter is passed, pagination will stop once you've reached this maximum.\n
Returned records do not include any fields with "empty" values, e.g. "", [], or false.`,
displayProperties: {
title: "List records from table ${parameters.tableIdOrName}",
},
externalDocs: {
description: "API method documentation",
url: "https://airtable.com/developers/web/api/list-records",
},
tags: ["users"],
},
security: {
oauth: [],
},
parameters: [
{
name: "userId",
in: "path",
description: "ID of the user you would like info about",
schema: {
type: "string",
},
required: true,
},
VersionHeaderParam,
],
request: {},
responses: {
200: [
{
success: true,
name: "Success",
description: "Typical success response",
schema: makeOneOf("User", [
makeObjectSchema("Person", {
requiredProperties: {
object: makeStringSchema("Object type", "This is always user", {
enum: ["user"],
}),
id: makeStringSchema(
"User ID",
"Unique identifier for this user"
),
type: makeStringSchema(
"User type",
"The user's type, either person or bot",
{
enum: ["person"],
}
),
person: makeObjectSchema("Person", {
requiredProperties: {
email: makeStringSchema("Email", "The user's email address"),
},
}),
},
optionalProperties: {
name: makeStringSchema(
"The user's full name",
"The user's full name"
),
avatar_url: makeNullable(
makeStringSchema("Avatar URL", "The user's avatar URL")
),
},
}),
makeObjectSchema("Bot", {
requiredProperties: {
object: makeStringSchema("Object type", "This is always user", {
enum: ["user"],
}),
id: makeStringSchema(
"User ID",
"Unique identifier for this user"
),
type: makeStringSchema(
"User type",
"The user's type, either person or bot",
{
enum: ["bot"],
}
),
bot: makeObjectSchema("Bot", {
requiredProperties: {
owner: makeObjectSchema("Owner", {
requiredProperties: {
type: makeStringSchema(
"Owner type",
"The owner's type, either user or workspace",
{
enum: ["user", "workspace"],
}
),
},
optionalProperties: {
workspace: makeNullable(
makeBooleanSchema("Workspace", "Is this a workspace?")
),
},
}),
workspace_name: makeNullable(
makeStringSchema(
"Workspace name",
"The name of the workspace"
)
),
},
}),
},
optionalProperties: {
name: makeStringSchema(
"The user's full name",
"The user's full name"
),
avatar_url: makeNullable(
makeStringSchema("Avatar URL", "The user's avatar URL")
),
},
}),
]),
},
],
default: [errorResponse],
},
};
@@ -0,0 +1,14 @@
import { Service } from "core/service/types";
import { authentication } from "./authentication";
import actions from "./actions/actions";
export const notion: Service = {
name: "Notion",
service: "notion",
version: "0.1.22",
baseUrl: "https://api.notion.com",
live: true,
authentication,
actions,
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
};
@@ -0,0 +1,30 @@
import { startNock, stopNock } from "testing/nock";
import { describe, expect, test } from "vitest";
import endpoints from "../endpoints/endpoints";
const authToken = () => process.env.AIRTABLE_TOKEN ?? "";
describe("notion.endpoints", async () => {
test("getUser", async () => {
const accessToken = authToken();
// const nockDone = await startNock("notion.getUser");
const data = await endpoints.getUser.request({
parameters: {
baseId: "appBlf3KsalIQeMUo",
tableIdOrName: "tblvXn2TOeVPC9c6m",
recordId: "recHcnB1MbBr9Rd2P",
},
credentials: {
type: "oauth2",
name: "oauth",
accessToken,
scopes: ["data.records:read"],
},
});
expect(data.status).toEqual(200);
expect(data.success).toEqual(true);
expect(data.body).not.toBeNull();
// stopNock(nockDone);
});
});
+3
View File
@@ -5,6 +5,9 @@
"github": {
"client_id": "cd763219ce4005e58c00"
},
"notion": {
"client_id": "ab5f331c-9f02-440c-8d60-ad53f7285e7d"
},
"slack": {
"client_id": "276370297397.4579145654276"
}