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

Fix directly calling operations on the frontend #1992

Merged
merged 33 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d8651a8
Improve directly calling queries on the client
sodic Apr 24, 2024
c7d8adf
Improve typescript support in operations
sodic Apr 25, 2024
c309ad3
Improve full stack type safety
sodic Apr 25, 2024
83d24dc
Add e2e tests for full stack type safety
sodic Apr 25, 2024
7f5876b
Add more features to todoApp
sodic Apr 25, 2024
6bc94cf
Make queryCacheKey visible to users
sodic Apr 25, 2024
e8273c9
Remove redundancies from useAuth hook
sodic Apr 25, 2024
43a9d71
Rename InternalViewFor to QueryForFunction
sodic Apr 25, 2024
6249c97
Remove redundant type assertion
sodic Apr 25, 2024
f9b674a
Add one more type to be extra safe
sodic Apr 25, 2024
a659278
Improve naming
sodic Apr 25, 2024
e81f2e4
Add type tests for getMe
sodic Apr 26, 2024
9d4e2d1
Improve function that adds metadata to queries
sodic Apr 26, 2024
ccde43a
DRY up RPC types
sodic Apr 26, 2024
15224f8
Separate RPC from hooks
sodic Apr 26, 2024
e11581e
Remove redundant import
sodic Apr 26, 2024
e4f175f
Remove redundant parentheses
sodic Apr 26, 2024
2b9f022
Change formatting
sodic Apr 29, 2024
3c8aac5
Add comment explaining type
sodic Apr 29, 2024
3271aaa
Update docs
sodic Apr 29, 2024
ee25c74
Add type modifier to imports
sodic May 2, 2024
dcfb9c9
Reorganize type tests
sodic May 2, 2024
fc5a20a
Reformat and improve types for queries/core
sodic May 2, 2024
d7e1591
Add comments explaining RPC types
sodic May 3, 2024
d52a931
Add comment that links to void type issue
sodic May 3, 2024
74d5e2a
Remove unnecessary exports and add clarifying comments
sodic May 6, 2024
c4de989
Remove empty line
sodic May 6, 2024
606ee83
Fix type errors in queries/core
sodic May 6, 2024
bfc6fe8
Fix optimistic update issues
sodic May 6, 2024
ab6f014
Remove redundant type
sodic May 7, 2024
590c33a
Update e2e tests
sodic May 7, 2024
1fd0042
Merge branch 'main' of github.com:wasp-lang/wasp into filip-fix-front…
sodic May 7, 2024
8e93092
Add operations to todoApp
sodic May 7, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions waspc/data/Generator/templates/sdk/wasp/auth/useAuth.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
{{={= =}=}}
import { deserialize as superjsonDeserialize } from 'superjson'
import { useQuery, addMetadataToQuery } from 'wasp/client/operations'
import { useQuery, buildAndRegisterQuery } from 'wasp/client/operations'
import type { QueryFunction, Query } from 'wasp/client/operations/rpc'
import { api, handleApiError } from 'wasp/client/api'
import { HttpMethod } from 'wasp/client'
import type { AuthUser } from '../server/auth/user.js'
import { UseQueryResult } from '@tanstack/react-query'

// PUBLIC API
export const getMe: () => Promise<AuthUser | null> = createUserGetter()
export const getMe: Query<void, AuthUser | null> = createUserGetter()

// PUBLIC API
export default function useAuth(queryFnArgs?: unknown, config?: any): UseQueryResult<AuthUser> {
return useQuery(getMe, queryFnArgs, config)
}
export default function useAuth(): UseQueryResult<AuthUser | null> {
return useQuery(getMe)
}

function createUserGetter() {
function createUserGetter(): Query<void, AuthUser | null> {
const getMeRelativePath = 'auth/me'
const getMeRoute = { method: HttpMethod.Get, path: `/${getMeRelativePath}` }
async function getMe(): Promise<AuthUser | null> {
const getMe: QueryFunction<void, AuthUser | null> = async () => {
try {
const response = await api.get(getMeRoute.path)

Expand All @@ -31,11 +32,9 @@ function createUserGetter() {
}
}

addMetadataToQuery(getMe, {
relativeQueryPath: getMeRelativePath,
return buildAndRegisterQuery(getMe, {
queryCacheKey: [getMeRelativePath],
queryRoute: getMeRoute,
entitiesUsed: {=& entitiesGetMeDependsOn =},
})

return getMe
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { Expand, _Awaited, _ReturnType } from 'wasp/universal/types'
import { type Action } from '../core.js'
import type { _Awaited, _ReturnType } from 'wasp/universal/types'
import type { OperationRpcFor, GenericBackendOperation } from '../rpc.js'
import { callOperation, makeOperationRoute } from '../internal/index.js'
import {
registerActionInProgress,
registerActionDone,
} from '../internal/resources.js'

// PRIVATE API
export function createAction<BackendAction extends GenericBackendAction>(
export function createAction<BackendAction extends GenericBackendOperation>(
relativeActionRoute: string,
entitiesUsed: unknown[]
): ActionFor<BackendAction> {
Expand Down Expand Up @@ -41,8 +41,5 @@ export function createAction<BackendAction extends GenericBackendAction>(
}

// PRIVATE API
export type ActionFor<BackendAction extends GenericBackendAction> =
Action<Parameters<BackendAction>[0], _Awaited<_ReturnType<BackendAction>>>


type GenericBackendAction = (args: never, context: any) => unknown
export type ActionFor<BackendAction extends GenericBackendOperation> =
OperationRpcFor<BackendAction>
Original file line number Diff line number Diff line change
Expand Up @@ -7,47 +7,34 @@ import {
useQuery as rqUseQuery,
UseQueryResult,
} from "@tanstack/react-query";
import { Action, Query } from "./rpc";
export { configureQueryClient } from "./queryClient";

// PRIVATE API (but should maybe be public, users use values of this type)
export type Query<Input, Output> = {
(queryCacheKey: string[], args: Input): Promise<Output>;
};

// PUBLIC API
export function useQuery<Input, Output>(
queryFn: Query<Input, Output>,
query: Query<Input, Output>,
queryFnArgs?: Input,
options?: any
): UseQueryResult<Output, Error>;

// PUBLIC API
export function useQuery(queryFn, queryFnArgs, options) {
if (typeof queryFn !== "function") {
throw new TypeError("useQuery requires queryFn to be a function.");
): UseQueryResult<Output, Error> {
if (typeof query !== 'function') {
throw new TypeError('useQuery requires queryFn to be a function.')
}
if (!queryFn.queryCacheKey) {
throw new TypeError(
"queryFn needs to have queryCacheKey property defined."
);

if (!query.queryCacheKey) {
throw new TypeError('queryFn needs to have queryCacheKey property defined.')
}

const queryKey =
queryFnArgs !== undefined
? [...queryFn.queryCacheKey, queryFnArgs]
: queryFn.queryCacheKey;
? [...query.queryCacheKey, queryFnArgs]
: query.queryCacheKey
return rqUseQuery({
queryKey,
queryFn: () => queryFn(queryKey, queryFnArgs),
queryFn: () => query(queryFnArgs),
...options,
});
})
}

// PRIVATE API (but should maybe be public, users use values of this type)
export type Action<Input, Output> = [Input] extends [never]
? (args?: unknown) => Promise<Output>
: (args: Input) => Promise<Output>;

// PRIVATE API (but should maybe be public, users define values of this type)
/**
* An options object passed into the `useAction` hook and used to enhance the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export {
useQuery,
// PUBLIC API
type OptimisticUpdateDefinition,
} from './core'
} from './hooks'

export {
// PUBLIC API
Expand Down
Original file line number Diff line number Diff line change
@@ -1,53 +1,64 @@
import { Route } from 'wasp/client'
import type { Expand, _Awaited, _ReturnType } from 'wasp/universal/types'
import { type Query } from '../core.js'
import type { _Awaited, _ReturnType } from 'wasp/universal/types'
import type {
ClientOperation,
GenericBackendOperation,
OperationRpcFor,
QueryFunction,
QueryMetadata,
} from '../rpc.js'
import { callOperation, makeOperationRoute } from '../internal/index.js'
import {
addResourcesUsedByQuery,
getActiveOptimisticUpdates,
} from '../internal/resources'

export function createQuery<BackendQuery extends GenericBackendQuery>(
// PRIVATE API (unsed in SDK)
export function createQuery<BackendQuery extends GenericBackendOperation>(
relativeQueryPath: string,
entitiesUsed: string[]
): QueryFor<BackendQuery> {
const queryRoute = makeOperationRoute(relativeQueryPath)
const queryCacheKey = [relativeQueryPath]

async function query(queryKey, queryArgs) {
const queryFn: QueryFunctionFor<BackendQuery> = async (queryArgs) => {
const serverResult = await callOperation(queryRoute, queryArgs)
return getActiveOptimisticUpdates(queryKey).reduce(
return getActiveOptimisticUpdates(queryCacheKey).reduce(
(result, update) => update(result),
serverResult,
)
}

addMetadataToQuery(query, { relativeQueryPath, queryRoute, entitiesUsed })

return query
return buildAndRegisterQuery(
queryFn,
{ queryCacheKey, queryRoute, entitiesUsed },
)
}

// PRIVATE API
export function addMetadataToQuery(
query: (...args: any[]) => Promise<unknown>,
metadata: {
relativeQueryPath: string
queryRoute: Route
entitiesUsed: string[]
}
): void

// PRIVATE API
export function addMetadataToQuery(
query,
{ relativeQueryPath, queryRoute, entitiesUsed }
) {
query.queryCacheKey = [relativeQueryPath]
// PRIVATE API (used in SDK)
export function buildAndRegisterQuery<Input, Output>(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Having a proper function (i.e., a smart constructor) seemed cleaner than having a function with side effects that you might forget to call.

queryFn: ClientOperation<Input, Output>,
Copy link
Contributor

Choose a reason for hiding this comment

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

How not use QueryFunction here?

Copy link
Contributor Author

@sodic sodic May 2, 2024

Choose a reason for hiding this comment

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

Nice catch. I introduced QueryFunction after writing this code and forgot to update it.

{ queryCacheKey, queryRoute, entitiesUsed }:
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 took the creation of the queryCacheKey out to make it easily accessible in createQuery. The bonus is that this function now has fewer responsibilities.

{ queryCacheKey: string[], queryRoute: Route, entitiesUsed: string[] }
): QueryForFunction<typeof queryFn> {
const query = queryFn as QueryForFunction<typeof queryFn>
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice cast


query.queryCacheKey = queryCacheKey
query.route = queryRoute
addResourcesUsedByQuery(query.queryCacheKey, entitiesUsed)

return query

Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: extra new line

}

export type QueryFor<BackendQuery extends GenericBackendQuery> =
Query<Parameters<BackendQuery>[0], _Awaited<_ReturnType<BackendQuery>>>
// PRIVATE API (but should maybe be public, users define values of this type)
export type QueryFor<BackendQuery extends GenericBackendOperation> =
QueryForFunction<QueryFunctionFor<BackendQuery>>

type QueryFunctionFor<BackendQuery extends GenericBackendOperation> =
OperationRpcFor<BackendQuery>

// PRIVATE API (needed in SDK)
type QueryForFunction<QF extends QueryFunction<never, unknown>> =
QF & QueryMetadata
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 decided to avoid duplicating the same code for Queries and Actions this time around, mostly because that's what caused an incorrect type in RPC calls for queries.

Copy link
Contributor

Choose a reason for hiding this comment

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

I found this block hard to parse, maybe put some why comment on top to make it easier for us in the future to get back into this code.


type GenericBackendQuery = (args: never, context: any) => unknown
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ export const {= operationName =}: QueryFor<{= operationTypeName =}> = createQuer
)
{=/ queries =}

// PRIVATE API
export { addMetadataToQuery } from './core'
// PRIVATE API (used in SDK)
Copy link
Contributor

Choose a reason for hiding this comment

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

If used only in SDK, do you need to expose in the package.json?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, that's what we'll do as part of #1922.

But for now, yes (since other parts of SDK use it as a package import).

export { buildAndRegisterQuery } from './core'
38 changes: 38 additions & 0 deletions waspc/data/Generator/templates/sdk/wasp/client/operations/rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { type Route } from "wasp/client";
import type {
_Awaited,
_ReturnType,
} from "wasp/universal/types"

// PRIVATE API (but should maybe be public, users define values of this type)
export type Query<Input, Output> = QueryFunction<Input, Output> & QueryMetadata

// PRIVATE API (but should maybe be public, users define values of this type)
export type Action<Input, Output> = ClientOperation<Input, Output>
sodic marked this conversation as resolved.
Show resolved Hide resolved

// PRIVATE API
export type QueryFunction<Input, Output> = ClientOperation<Input, Output>

// PRIVATE API
export type QueryMetadata = {
queryCacheKey: string[]
route: Route
}
sodic marked this conversation as resolved.
Show resolved Hide resolved

// PRIVATE API (needed in SDK)
export type OperationRpcFor<BackendOperation extends GenericBackendOperation> =
Parameters<BackendOperation> extends []
Copy link
Contributor Author

@sodic sodic Apr 29, 2024

Choose a reason for hiding this comment

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

TS Playground link explaining why this check was necessary.

? ClientOperation<void, _Awaited<_ReturnType<BackendOperation>>>
: ClientOperation<
Parameters<BackendOperation>[0],
_Awaited<_ReturnType<BackendOperation>>
>

// PRIVATE API (but should maybe be public, users use values of this type)
// Read this to understand the type: https://github.com/wasp-lang/wasp/pull/1090#discussion_r1159732471
export type ClientOperation<Input, Output> = [Input] extends [never]
? (args?: unknown) => Promise<Output>
: (args: Input) => Promise<Output>;

// PRIVATE API (needed in SDK)
export type GenericBackendOperation = (args: never, context: any) => unknown
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { BrowserRouter as Router } from 'react-router-dom'
import { render, RenderResult, cleanup } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { beforeAll, afterEach, afterAll } from 'vitest'
import { Query } from 'wasp/client/operations/core'
import { Query } from 'wasp/client/operations/rpc'
import { config } from 'wasp/client'
import { HttpMethod, Route } from 'wasp/client'

Expand Down
2 changes: 1 addition & 1 deletion waspc/data/Generator/templates/sdk/wasp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@

{=! todo(filip): Fixes below are for type errors in 0.13.1, remove ASAP =}
{=! Used by our code (SDK for full-stack type safety), uncodumented (but accessible) for users. =}
"./client/operations/core": "./dist/client/operations/core.js",
"./client/operations/rpc": "./dist/client/operations/rpc.js",
"./server/crud/Tasks": "./dist/server/crud/Tasks.js",
"./server/operations/actions": "./dist/server/operations/actions/index.js",
"./server/operations/queries": "./dist/server/operations/queries/index.js",
Expand Down
10 changes: 6 additions & 4 deletions waspc/examples/todoApp/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useSocket } from "wasp/client/webSocket";
import { Link } from "wasp/client/router";
import { logout, useAuth } from "wasp/client/auth";
import { useQuery, getDate } from "wasp/client/operations";
import { useSocket } from 'wasp/client/webSocket'
import { Link } from 'wasp/client/router'
import { logout, useAuth } from 'wasp/client/auth'
import { useQuery, getDate } from 'wasp/client/operations'

import './Main.css'
import { getName } from './user'
// Necessary to trigger type tests.
import './TestRpcTypes'

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd recommend maybe creating a type-tests folder or something like that and then import a index.ts from that folder to keep things a bit more organised.

Copy link
Contributor

Choose a reason for hiding this comment

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

Then you could put the helpers in typeTestHelpers or something like that and then the RPC tests just in its own file.

export function App({ children }: any) {
const { data: user } = useAuth()
Expand Down
Loading
Loading