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

Generic endpoints support #15

Closed
Refzlund opened this issue Apr 18, 2023 · 5 comments
Closed

Generic endpoints support #15

Refzlund opened this issue Apr 18, 2023 · 5 comments
Assignees
Labels
enhancement New feature or request

Comments

@Refzlund
Copy link
Owner

On pull request #12 we discussed adding generics to endpoints.

Normal endpoint

interface Post {
	body: {
		foo: string
	}
}
export function POST(event: API<Post>) {
	....
}

Generic endpoint

interface Post {
	body: {
		foo: string
	}
}
export function POST<const TRequest extends Post>(event: API<TRequest>) {
	return Ok({
		body: {...} as TRequest['body']
	})
}

Where TRequest will be the request init, and you can therefore set the type based on input.

ex. api.users.GET({ /* Request Init */ }).Ok(res => { /* res.body reflects the contents of request init */ })

@Refzlund Refzlund added the enhancement New feature or request label Apr 18, 2023
@Refzlund Refzlund self-assigned this Apr 18, 2023
Refzlund added a commit that referenced this issue Apr 20, 2023
@Refzlund
Copy link
Owner Author

API Endpoint
image

Frontend
image

@kauderk
Copy link
Contributor

kauderk commented Apr 20, 2023

  • My Recursive type implementation doesn't play well with v0.14.0. It returns any no matter what :(
  • Reading the sveltekit-zero-api.d.ts types for generics
    • Why not use "<const T ..." if the actual endpoint uses the const keyword

Rambling.

// While using the API, I noticed that my methods are GET and that I don't need the signature.
type GET<T> = (event: { query?: any, body?: any }) => any;

// Why not use overloads?
type GET<T> = (event: { query?: any, body?: any }) => any | ((query: any) => any);

// Because doing the ceremony of returning early from a callback seems like a pattern on its own.
const myData = await API.endpoint.GET({query:{...myQuery}}).$.OK(res=>res.body)

// What if the signature for the OK function would be...
type OK<T> = (res: T & RequestInit) => any | ((body: T) => any);

// IDK, maybe treat it like an ORM or the "you pass only data" mentality from SvelteKit?
// It would be used like so:
const myData = await API.endpoint.GET({ ...myQuery }).OK();
                             .OK(res=>mapMyData(res.body));
// Maybe simplify it a little bit more?
const myData = await API.endpoint.GET({ ...myQuery });

// Because I expect that my program reaches this line of code.
// If the user decides or forgets to catch the promise, it's on them - right?

@Refzlund
Copy link
Owner Author

@kauderk I'll try to take a look and see why it returns any. Is the issue you've linked related to this?

Not sure if I understand your second question ... Are you referring to the response methods (e.g. return Ok(...), BadRequest(...) etc.)? In which case, const is new, so I haven't gotten around to adding them to those yet.

While using the API, I noticed that my methods are GET and that I don't need the signature.

I don't understand what you mean by not needing a signature. Are you saying you don't need the callbacks (.Ok(res => ...))?

Because doing the ceremony of returning early from a callback seems like a pattern on its own.

So .$. was a later addition. When working with colleagues there was being thrown
const body = (await api.endpoint).body
around, and the point is really to expect a certain result from the endpoint.

The issue with wrapping await is that now you have a body, but that might as well be a bad request (400).

Another part is, you could return 200, 201 etc. and have different content. So being explicit to which content you are expecting is helpful in terms of a typed safety net.

Maybe simplify it a little bit more?

const response = await.api.endpoint.GET(...)

Will just set response to be whatever response you'd get. However response.body will already be parsed into JSON.

We could probably shorten
await api.endpoint.GET(...).$.Ok(res => res.body)
to
await api.endpoint.GET(...).$.Ok()

But the dollar has to stay to allow multiple callbacks, and explicitly telling the reader what body from const body is referring to.


If there's something I misunderstand or you need help with something specifically feel free to ask and elaborate!

@kauderk
Copy link
Contributor

kauderk commented Apr 23, 2023

I decided to remove sveltekit-zero-api from my package


Things I've learned

Things I couldn't figure out

  • how to "tree-shake" the package.json. Currently I remove the "scripts" and "dependencies" manually before publishing so that my users don't have to install unnecessary stuff.

I like the idea of calling it just like a "function"

// optimistic data
const myQuery = await Api.query(complexQuery)

// handle the promise
Api.query({
	...complexQuery,
	manual: true,
}).then(async(res) => {
	if (!res?.ok) {
		res.body.errors.id
		//              ^?
		return
	}

	const myQuery = await res.json()
	//    ^?
})

@Refzlund
Copy link
Owner Author

I think that's a good idea! While Typescript is awesome, it's definitely not perfect. Especially when heavily relying on types that can be pretty deep. I don't personally think sveltekit-zero-api was meant for a library, although I'm still open-minded to the different use-cases, so thank you for enlightening me on that front by sharing what you've learned!

You also helped me with the idea of generic endpoints, which ultimatively inspired me to tackle a solution I've been thinking about for a while — which is the endpoint pipelines — where when working on the generics, I realized how to tackle the implementation for that. So thank you!

Happy coding🕺🦒

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants