diff --git a/.eslintrc.json b/.eslintrc.json index ab2e68a..d0956d4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -15,7 +15,8 @@ "sourceType": "module" }, "rules": { - "prettier/prettier": "error" + "prettier/prettier": "error", + "@typescript-eslint/no-explicit-any": 0 }, "plugins": ["prettier", "@typescript-eslint"] } diff --git a/package.json b/package.json index cf054d9..1c966be 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,12 @@ "AUTHORS" ], "lint-staged": { - "*.js": "eslint --cache --fix" + "*.ts": [ + "prettier -w", + "eslint --cache" + ], + "*.md": "prettier -w", + "*.{json,jsonc}": "prettier -w" }, "dependencies": { "@notionhq/client": "0.4.11", diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..4903bf1 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,46 @@ +import request from './request' +import type { Databases, Pages } from 'notion-api-types/endpoints' + +export function queryDatabase( + args: { + token: string + database_id: string + } & Databases.Query.Request['params'] +): Promise { + const { token, database_id, ...params } = args + return request({ + token, + endpoint: `databases/${database_id}/query`, + method: 'POST', + params, + }) +} + +export function createPage( + args: { + token: string + } & Pages.Create.Request['params'] +): Promise { + const { token, ...params } = args + return request({ + token, + endpoint: 'pages', + method: 'POST', + params, + }) +} + +export function updatePage( + args: { + token: string + page_id: string + } & Pages.Update.Request['params'] +): Promise { + const { token, page_id, ...params } = args + return request({ + token, + endpoint: `pages/${page_id}`, + method: 'PATCH', + params: params, + }) +} diff --git a/src/api/request.ts b/src/api/request.ts new file mode 100644 index 0000000..563cf9d --- /dev/null +++ b/src/api/request.ts @@ -0,0 +1,64 @@ +import fetch, { RequestInit } from 'node-fetch' +import { URL } from 'url' +import type { RequestTemplate } from 'notion-api-types/endpoints/global' + +export default request +async function request( + args: Omit & { token: string } +): Promise +async function request(args: DirectRequest & { token: string }): Promise +async function request( + args: T extends RequestTemplate + ? Omit & { params?: any; token: string } + : T & { token: string } +) { + const init: RequestInit = { + method: args.method, + headers: { + 'Notion-Version': '2021-08-16', + Authorization: `Bearer ${args.token}`, + }, + } + + let urlStr: string + if ('url' in args) urlStr = args.url + else { + const url = new URL('https://api.notion.com/v1/' + args.endpoint) + if ('params' in args) + if (args.method == 'GET') + for (const key in args.params) + url.searchParams.set(key, args.params[key].toString()) + else init.body = JSON.stringify(args.params) + urlStr = url.href + } + + const res = await fetch(urlStr, init) + + if (res.ok) return await res.json() + else throw (await res.json()) as NotionError +} + +interface DirectRequest { + method: 'GET' | 'POST' | 'PATCH' + url: string +} + +export interface NotionError { + object: 'error' + status: number + code: + | 'invalid_json' + | 'invalid_request_url' + | 'invalid_request' + | 'validation_error' + | 'missing_version' + | 'unauthorized' + | 'restricted_resource' + | 'object_not_found' + | 'conflict_error' + | 'rate_limited' + | 'internal_server_error' + | 'service_unavailable' + | 'database_connection_unavailable' + message: string +} diff --git a/src/database.ts b/src/database.ts new file mode 100644 index 0000000..7f91023 --- /dev/null +++ b/src/database.ts @@ -0,0 +1,39 @@ +import { queryDatabase, createPage } from './api' +import { getEntireList } from './list' +import type { Item } from '.' +import type { CustomProps, Page, Properties } from './item' +import type { Filter } from 'notion-api-types/endpoints/databases/query' + +export default class Database

{ + constructor( + private readonly item: ItemClass

, + private readonly token: string, + private readonly id: string, + private readonly filter?: Filter + ) {} + + readonly items = getEntireList(queryDatabase, { + token: this.token, + database_id: this.id, + filter: this.filter, + page_size: 100, + }).then(pages => + pages.map(page => new this.item(this.token, page as Page

)) + ) + + async newItem(properties: Partial>) { + const page = await createPage({ + token: this.token, + parent: { database_id: this.id }, + properties: properties as Properties

, + }) + const item = new this.item(this.token, page as Page

) + ;(await this.items).push(item) + return item + } +} + +type ItemClass

= new ( + token: string, + value: Page

+) => Item

diff --git a/src/index.ts b/src/index.ts index 3f90ef2..a34c3ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,39 +1,4 @@ -import { getEntireList } from './list' -import NotionClient from './request' -export { NotionClient } -import type { Item, CustomProps, ItemParams, Page, Properties } from './item' -export { Item } from './item' -export type { CustomProps, Page, Properties } -export type { Property, PropertyType } from './item' -import type { Filter } from 'notion-api-types/endpoints/databases/query' - -export class Database

> { - constructor( - private readonly item: new (...args: ItemParams

) => T, - private readonly client: NotionClient, - private readonly id: string, - private readonly filter?: Filter - ) {} - - readonly items: T[] = [] - - readonly getItemsPromise = getEntireList({ - method: this.client.database.query, - database_id: this.id, - page_size: 100, - filter: this.filter - }).then((list) => { - this.items.push( - ...list.map( - item => new this.item(item as Page

, this.client.pages.update) - ) - ) - }) - - newItem(properties: Partial>) { - return this.client.pages.create({ - parent: { database_id: this.id }, - properties: properties as Properties

, - }) - } -} +import Database from './database' +import Item from './item' +export { Database, Item } +export type { Page, Property, PropertyType } from './item' diff --git a/src/item.ts b/src/item.ts index 740f3c5..17586ad 100644 --- a/src/item.ts +++ b/src/item.ts @@ -1,40 +1,37 @@ -import NotionClient from './request' +import { updatePage } from './api' import type { NotionRequest, NotionResponse } from 'notion-api-types' -export class Item

{ - readonly properties: Page

['properties'] +export default class Item

{ + #props: Page

['properties'] + protected get properties() { + return this.#props + } + private set properties(value: Page

['properties']) { + this.#props = value + } private readonly id: Page

['id'] - protected readonly updatePage: NotionClient['pages']['update'] - constructor(...[value, updatePage]: ItemParams

) { - this.properties = value.properties + constructor(private readonly token: string, value: Page

) { + this.#props = value.properties this.id = value.id - this.updatePage = updatePage } - // getProp(name: K) { - // const value = this.properties[name] - // return value[value.type as P[K]] - // } - - update(props: Partial>) { - return this.updatePage({ + async update(props: Partial>) { + const page = await updatePage({ + token: this.token, page_id: this.id, - properties: props as Properties

+ properties: props as Properties

, }) + this.properties = page.properties as Page

['properties'] + return page } } -export type ItemParams

= [ - data: Page

, - updatePage: Item

['updatePage'] -] - export type PropertyType = NonNullable export type CustomProps = Record -export type Property = T extends T +export type Property = T extends T ? Extract> : never @@ -45,7 +42,7 @@ export type Properties = { export interface Page

extends NotionResponse.Page { properties: { [K in keyof P]: P[K] extends P[K] - ? Extract> - : never + ? Extract> + : never } } diff --git a/src/list.ts b/src/list.ts index 0859d1c..e39774d 100644 --- a/src/list.ts +++ b/src/list.ts @@ -1,23 +1,16 @@ import type { List } from 'notion-api-types/endpoints/global' -export function getEntireList< - A extends {}, - R extends List, - T extends R['results'][number] ->( - args: { - method: (args: A) => Promise - } & A +export async function getEntireList( + method: (args: A) => Promise>, + args: A ): Promise { - return loop() - async function loop(next_cursor?: string | null) { - const { method, ...params } = args - // @ts-ignore - params.next_cursor = next_cursor - const response = await method(params as unknown as A) - const list = response.results as T[] - if (next_cursor && response.has_more) - list.push(...(await loop(response.next_cursor))) - return list + const list: T[] = [] + async function requestPage(thisCursor?: string) { + args.start_cursor = thisCursor + const { results, next_cursor } = await method(args) + list.push(...results) + if (next_cursor) await requestPage(next_cursor) } -} \ No newline at end of file + await requestPage() + return list +} diff --git a/src/request.ts b/src/request.ts deleted file mode 100644 index 62b2513..0000000 --- a/src/request.ts +++ /dev/null @@ -1,102 +0,0 @@ -import fetch, { RequestInit } from 'node-fetch' -import { URL } from 'url' -import type { Endpoints } from 'notion-api-types' -import type { RequestTemplate } from 'notion-api-types/endpoints/global' - -export default class NotionClient { - constructor(private readonly token: string) {} - private readonly request = request - - readonly database = { - query: ( - params: { - database_id: string - } & Endpoints.Databases.Query.Request['params'] - ): Promise => - this.request({ - endpoint: `databases/${params.database_id}/query`, - method: 'POST', - params: params, - }), - } - - readonly pages = { - create: ( - params: Endpoints.Pages.Create.Request['params'] - ): Promise => - this.request({ - endpoint: 'pages', - method: 'POST', - params: params, - }), - update: ( - params: { - page_id: string - } & Endpoints.Pages.Update.Request['params'] - ): Promise => - this.request({ - endpoint: `pages/${params.page_id}`, - method: 'PATCH', - params: params, - }), - } -} - -async function request( - this: NotionClient, - args: Omit & { params?: any } -): Promise -async function request(this: NotionClient, args: DirectRequest): Promise -async function request( - this: NotionClient, - args: T extends RequestTemplate ? Omit & { params?: any } : T -) { - const init: RequestInit = { - method: args.method, - headers: { - 'Notion-Version': '2021-08-16', - Authorization: `Bearer ${this['token']}`, - }, - } - - let urlStr: string - if ('url' in args) urlStr = args.url - else { - const url = new URL('https://api.notion.com/v1/' + args.endpoint) - if (args.method == 'GET') - for (const key in args.params) - url.searchParams.set(key, args.params[key].toString()) - else init.body = JSON.stringify(args.params) - urlStr = url.href - } - - const res = await fetch(urlStr, init) - - if (res.ok) return await res.json() - else throw (await res.json()) as NotionError -} - -interface DirectRequest { - method: 'GET' | 'POST' | 'PATCH' - url: string -} - -export interface NotionError { - object: 'error' - status: number - code: - | 'invalid_json' - | 'invalid_request_url' - | 'invalid_request' - | 'validation_error' - | 'missing_version' - | 'unauthorized' - | 'restricted_resource' - | 'object_not_found' - | 'conflict_error' - | 'rate_limited' - | 'internal_server_error' - | 'service_unavailable' - | 'database_connection_unavailable' - message: string -}