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

Client basics #1

Merged
merged 12 commits into from
Apr 23, 2021
Merged

Client basics #1

merged 12 commits into from
Apr 23, 2021

Conversation

aoberoi
Copy link
Contributor

@aoberoi aoberoi commented Apr 21, 2021

Goals

  • Establish a project / file structure
  • Establish build configuration (TypeScript)
  • Implement some basic client features (the API was established in an internal RFC)
    • Initializing a client instance (with options)
    • Sending requests via endpoint methods (only two representative methods are currently available)
    • Sending requests via a generic request interface
  • Establish some type definition patterns that we can support in the future with automatic generation.
  • Establish some testing and linting configuration

The following items remain:

  • Add the remaining endpoint methods I decided to add this in a follow up PR in order to help this change land sooner, and make some incremental progress.

Other client features will be implemented in subsequent PRs, such as implementing error types.

Feedback

At this point, I'm seeking input on the following:

  • Does the project / file structure make sense?
  • Are you happy with the build, testing, and linting config?
  • Are there any red flags in the way the Client is implemented so far?
  • Any thoughts regarding how the type definitions are structured?

* Notion API Endpoints
*
* This file contains metadata about each of the API endpoints such as the HTTP method, the parameters, and the types.
* In the future, the contents of this file will be generated from an API definition.
Copy link
Contributor Author

@aoberoi aoberoi Apr 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file and src/api-types.ts contain a lot of information that we'd like to generate from the internal source. For now, the plan is to do this manually. In the future the generation would occur in two steps. First, the internal source would be used to generate an OpenAPI definition (accompanied with JSON Schema for object types). That output would be shared publicly for many use cases beyond API clients (e.g. Postman, testing, etc). Next, the OpenAPI definition would be used to generate these files in this client, and similar code for the Python client.

src/Client.ts Outdated
* @param body
* @returns
*/
public async request<Response extends {}>(path: string, method: Method, query?: QueryParams, body?: {}, auth?: string): Promise<Response> {
Copy link
Contributor Author

@aoberoi aoberoi Apr 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future, I would like to include a more type-aware overload of this method. The goal would be for the return type to depend on the individual arguments (without any generic inputs at the call site). It might look something like the following (but QP and B should also be constrained to more specific types based on P and M):

public async request<P extends string, M extends Method, QP extends QueryParams, B extends {}>(path: P, method: M, query?: QP, body?: B): Promise<APIResponse<P, M>>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • let’s use named arguments
  • Better to have object as the type bound

Copy link
Contributor Author

@aoberoi aoberoi Apr 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that method signature is getting long / illegible. good call on named args!

regarding the type constraint on Response (the diff above is outdated) i'm getting a hassle from the linter. when i constrain to Response extends object i get a violation of the @typescript-eslint/ban-types rule:

Don't use `object` as a type. The `object` type is currently hard to use ([see this issue](https://github.com/microsoft/TypeScript/issues/21732)).
Consider using `Record<string, unknown>` instead, as it allows you to more easily inspect and use the keys.

i believe Response extends {} is actually the same thing as above, but the same rule says that's a violation because its unclear:

Don't use `{}` as a type. `{}` actually means "any non-nullish value".
- If you want a type meaning "any object", you probably want `Record<string, unknown>` instead.
- If you want a type meaning "any value", you probably want `unknown` instead.
- If you want a type meaning "empty object", you probably want `Record<string, never>` instead.

if i take its advice and use Response extends Record<string, unknown> then the linter is cool but i get a type check error on L75 (where I use DatabasesRetrieveResponse as Response):

Type 'DatabasesRetrieveResponse' does not satisfy the constraint 'Record<string, unknown>'.
  Index signature is missing in type 'DatabasesRetrieveResponse'.

that seems wrong because i don't want to introduce an index signature to DatabasesRetrieveResponse. if i did, then when a developer uses a response and misspells a property name (e.g. response.ib, they wouldn't necessarily get a compiler error, it would just evaluate to unknown which can slide through).

in conclusion, i left off the type constraint because i think the right way to solve this problem is with an overload of the method signature that constrains method and path to types that match our list of endpoints, and then infers the response type based on that. this function signature would stick around for the developers who really know that their response is going to be a specific type (that maybe the client doesn't know about).

thank you for reading my TED talk (tried to be as concise as i could, sorry). do you agree with that last bit? any other ideas?

ps. i think the linter's recommended rules are reasonable, so i'm not super in favor of turning them off or reconfiguring them. maybe you can convince me otherwise?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, I think the rule is being silly, but, oh well. It seems that object is the correct thing to do here per the compiler, but it doesn't lint. If you wan this repo to look like "typescript on average" then go for the linting experience and leave off the type constraint

interface DatabasesRetrieveBodyParameters {}

export interface DatabasesRetrieveParameters extends DatabasesRetrievePathParameters, DatabasesRetrieveQueryParameters, DatabasesRetrieveBodyParameters {}
export interface DatabasesRetrieveResponse extends NotionDatabase {}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future, *Response types can become generic on the *Parameters type, so that the type system can support any variation in response based on certain kinds of params (conditional types). This kind of relationship may prove difficult or impossible to express in an OpenAPI definition file, so we are intentionally saving this exploration for later.

src/Client.ts Outdated Show resolved Hide resolved
src/Client.ts Outdated
databasesRetrieve.method,
pick(args, ...databasesRetrieve.queryParams),
pick(args, ...databasesRetrieve.bodyParams),
args.auth,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like we never actually use this.#auth?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's used in Client.authAsHeaders()

@aoberoi
Copy link
Contributor Author

aoberoi commented Apr 22, 2021

i've added some basic linting, but we still likely want to customize and add a few more rules. i don't consider getting the rules completely right as blocking for this PR. we can follow up on that.

ava was chosen as a test runner (well-maintained, excellent typescript support)
adding this config required slightlying changing the build output config too.
the distributable artifacts are now one level deeper, so that they can remain
in a separate directory from the test artifacts. for clarity, the output directory
was renamed from `dist` to build`, and within build, there is `src` and `test`.
only `build/src` is meant for packaging (and that is reflected in the "main"
property of package.json).
@aoberoi aoberoi marked this pull request as ready for review April 22, 2021 16:10
@alicial alicial requested review from pllnk and justjake April 22, 2021 16:51
@aoberoi
Copy link
Contributor Author

aoberoi commented Apr 22, 2021

@justjake adjusted Client.request() to take named params as you suggested.

Copy link
Contributor

@justjake justjake left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left some comments. seems legit

@@ -0,0 +1,9 @@
export default {
Copy link
Contributor

@justjake justjake Apr 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

never used this. we use mocha in the main app.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

me neither actually. but the allure of parallelized tests in worker threads and strong typing was too much for me to turn down. and packages by @sindresorhus are generally awesome.

"author": "",
"license": "MIT",
"dependencies": {
"got": "^11.8.2"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initially i was dubious of got since it's 90kb to make HTTP requests even though HTTP client is built into the standard library, but it seems to do a lot of stuff and generally looks useful.

You should be careful to isolate our return and exception types from Got's -- Got throws GotHttpError or similar; we should be careful about consumers needing to install Got to handle our exceptions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed. i made type aliases for Method and QueryParams in order to create that separation. error types are still a TODO.

src/Client.ts Outdated
* @param level The level for this message
* @param args Arguments to send to the console
*/
private log(level: LogLevel, ...args: any[]) { // eslint-disable-line @typescript-eslint/no-explicit-any
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer unknown

/**
* Retrieve a database
*/
retrieve: (args: WithAuth<DatabasesRetrieveParameters>): Promise<DatabasesRetrieveResponse> => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get?

* Type aliases to support the generic request interface.
*/
type Method = 'get' | 'post' | 'patch';
type QueryParams = GotOptions['searchParams'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move up?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd like to try keeping non-exported things at the bottom. that way when a consumer is reading the source, the parts they care about at closest to the top.

it's kinda arbitrary that these are not exported, since they are accessible via the interface of Client. i just chose not to export as a small signal that these types aren't that important or useful on their own.

src/Client.ts Outdated
this.#logLevel = options?.logLevel ?? LogLevel.WARN;

const prefixUrl = (options?.baseUrl ?? 'https://api.notion.com') + '/v1/';
const timeout = options?.timeout ?? 60;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

60?

// Node LTS Fermium (14.x) has mostly complete support for ES2019 (as reported by https://node.green/)
"target": "ES2019",
"module": "commonjs",
// "esModuleInterop": true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could turn this on, but it just emits a little extra and possibly unnecessary code. we're just as good as long as src/index.ts only emits named exports, because those are 1:1 with CommonJS.


// Strict mode
"strict": true,

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we turn on isolatedModules: true?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also commonly have

"moduleResolution": "node",
"resolveJsonModule": true,

not sure if wanted/needed

Copy link
Contributor Author

@aoberoi aoberoi Apr 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

of those, i think we might want resolveJsonModule when i start reading package.json for the module's version number to stick it in User-Agent.


"include": [
"src/**/*",
"test/**/*",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will output compiled tests. that's okay, but often test runners can run from the .ts files directly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ava supports both: https://github.com/avajs/ava/blob/main/docs/recipes/typescript.md#enabling-avas-support-for-typescript-test-files

this way was just much simpler. i think the ts-node option becomes more attractive when there's more involved in the build step (babel, webpack, etc), which i hope to avoid.

"engines": {
"node": ">=14.15.0"
},
"main": "./build/src",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shall we exclude the tests from packaging?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yaaa good call. i should add a files key or an npmignore.

@aoberoi aoberoi merged commit fa21dd6 into main Apr 23, 2021
@aoberoi aoberoi deleted the feat-basic-functionality branch April 23, 2021 00:43
jessmitch42 referenced this pull request in jessmitch42/notion-sdk-js Aug 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants