-
Notifications
You must be signed in to change notification settings - Fork 548
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
Conversation
* 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. |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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>>
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 {} |
There was a problem hiding this comment.
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
databasesRetrieve.method, | ||
pick(args, ...databasesRetrieve.queryParams), | ||
pick(args, ...databasesRetrieve.bodyParams), | ||
args.auth, |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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()
3692995
to
d6c81e8
Compare
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).
…ams were getting chunky
@justjake adjusted |
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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> => { |
There was a problem hiding this comment.
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']; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move up?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not?
There was a problem hiding this comment.
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, | ||
|
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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/**/*", |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
Add block type example
Goals
The following items remain:
Add the remaining endpoint methodsI 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:
Client
is implemented so far?