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 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ export function createAction<BackendAction extends GenericBackendAction>(
}

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

export type ActionFor<BackendQuery extends GenericBackendAction> =
Parameters<BackendQuery> extends []
sodic marked this conversation as resolved.
Show resolved Hide resolved
? Action<void, _Awaited<_ReturnType<BackendQuery>>>
: Action<Parameters<BackendQuery>[0], _Awaited<_ReturnType<BackendQuery>>>

type GenericBackendAction = (args: never, context: any) => unknown
44 changes: 25 additions & 19 deletions waspc/data/Generator/templates/sdk/wasp/client/operations/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,50 @@ import {
useQuery as rqUseQuery,
UseQueryResult,
} from "@tanstack/react-query";
import { Route } from "wasp/client";
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>;
};
// PRIVATE API (but should maybe be public, users define values of this type)
export type Query<Input, Output> = Operation<Input, Output>

// PUBLIC API
export function useQuery<Input, Output>(
queryFn: 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 queryFn !== 'function') {
throw new TypeError('useQuery requires queryFn to be a function.')
}
if (!queryFn.queryCacheKey) {
throw new TypeError(
"queryFn needs to have queryCacheKey property defined."
);
const internalQueryFn = queryFn as InternalViewOf<typeof queryFn>

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

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

// PRIVATE API (needed in SDK)
export type InternalViewOf<Q extends Query<never, unknown>> = Q & {
route: Route,
queryCacheKey: string[],
}

// PRIVATE API (but should maybe be public, users define values of this type)
export type Action<Input, Output> = Operation<Input, Output>

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
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 { Query, InternalViewOf } from '../core.js'
import { callOperation, makeOperationRoute } from '../internal/index.js'
import {
addResourcesUsedByQuery,
getActiveOptimisticUpdates,
} from '../internal/resources'

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

async function query(queryKey, queryArgs) {
type Q = QueryFor<BackendQuery>
const query: Q = async (queryArgs) => {
// Assumes `addMetadataToQuery` added the `queryCacheKey` property to the query.
const queryKey = (query as InternalViewOf<Q>).queryCacheKey
const serverResult = await callOperation(queryRoute, queryArgs)
return getActiveOptimisticUpdates(queryKey).reduce(
(result, update) => update(result),
Expand All @@ -26,28 +30,23 @@ export function createQuery<BackendQuery extends GenericBackendQuery>(
return query
}

// 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]
query.route = queryRoute
addResourcesUsedByQuery(query.queryCacheKey, entitiesUsed)
// PRIVATE API (used in SDK)
export function addMetadataToQuery<Input, Output>(
query: Query<Input, Output>,
{ relativeQueryPath, queryRoute, entitiesUsed }:
{ relativeQueryPath: string, queryRoute: Route, entitiesUsed: string[] }
): asserts query is InternalViewOf<typeof query> {
const internalQuery = query as InternalViewOf<typeof query>

internalQuery.queryCacheKey = [relativeQueryPath]
internalQuery.route = queryRoute
addResourcesUsedByQuery(internalQuery.queryCacheKey, entitiesUsed)
}

export type QueryFor<BackendQuery extends GenericBackendQuery> =
Query<Parameters<BackendQuery>[0], _Awaited<_ReturnType<BackendQuery>>>
export type QueryFor<BackendQuery extends GenericBackendQuery> =
Parameters<BackendQuery> extends []
? Query<void, _Awaited<_ReturnType<BackendQuery>>>
: Query<Parameters<BackendQuery>[0], _Awaited<_ReturnType<BackendQuery>>>

type GenericBackendQuery = (args: never, context: any) => unknown

type GenericBackendQuery = (args: never, context: any) => unknown
102 changes: 102 additions & 0 deletions waspc/examples/todoApp/src/TestRpcTypes.ts
Copy link
Contributor Author

@sodic sodic Apr 25, 2024

Choose a reason for hiding this comment

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

That's right. I've pulled a Miho.

In the future, we'll probably want to replace this with an existing type testing library:
https://stackoverflow.com/a/58831534

Copy link
Member

Choose a reason for hiding this comment

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

Open an issue for it?

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 already have one: #1951

Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {
getTask,
getTasks,
createTask,
updateTaskIsDone,
deleteCompletedTasks,
toggleAllTasks,
getNumTasks,
getDate,
getAnything,
getTrueVoid,
} from 'wasp/client/operations'
import { Task } from 'wasp/entities'
import { Payload } from 'wasp/server/_types'

// For the details of this specification, see
// https://github.com/wasp-lang/wasp/pull/1090#discussion_r1159732471

// This should be [],
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe put a TODO here

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'll do you one better: #2004

// but I couldn't get it to work yet.
type VoidOperationPayload = [args?: void | undefined]

// When the user doesn't specify an operation payload,
// we want to be as permissive as possible.
type UnspecifiedOperationPayload = [args?: unknown]

type TQ1 = Assert<
InputsAndOutputsAre<typeof getTask, [Pick<Task, 'id'>], Promise<Task>>
>

type TQ2 = Assert<
InputsAndOutputsAre<typeof getTasks, VoidOperationPayload, Promise<Task[]>>
>

type TQ3 = Assert<
InputsAndOutputsAre<typeof getNumTasks, VoidOperationPayload, Promise<number>>
>

type TQ4 = Assert<
InputsAndOutputsAre<typeof getDate, VoidOperationPayload, Promise<Date>>
>

type TQ5 = Assert<
InputsAndOutputsAre<
typeof getAnything,
UnspecifiedOperationPayload,
Promise<Payload>
>
>

type TQ6 = Assert<
InputsAndOutputsAre<typeof getTrueVoid, VoidOperationPayload, Promise<string>>
>

type TA1 = Assert<
InputsAndOutputsAre<
typeof createTask,
[Pick<Task, 'description'>],
Promise<Task>
>
>

type TA2 = Assert<
InputsAndOutputsAre<
typeof updateTaskIsDone,
[Pick<Task, 'id' | 'isDone'>],
Promise<Payload>
>
>

type TA3 = Assert<
InputsAndOutputsAre<
typeof deleteCompletedTasks,
UnspecifiedOperationPayload,
Promise<Payload>
>
>

type TA4 = Assert<
InputsAndOutputsAre<
typeof toggleAllTasks,
UnspecifiedOperationPayload,
Promise<Payload>
>
>

type InputsAndOutputsAre<
OperationType extends (...args: any[]) => any,
ExpectedParams,
ExpectedReturn
> = {
params: AreEqual<Parameters<OperationType>, ExpectedParams>
return: AreEqual<ReturnType<OperationType>, ExpectedReturn>
}

type Assert<T extends { params: true; return: true }> = T

type AreEqual<T, Expected> = T extends Expected
? Expected extends T
? true
: false
: false
2 changes: 1 addition & 1 deletion waspc/examples/todoApp/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from "wasp/server/operations";
import { getSomeResource } from './serverSetup.js'

export const createTask: CreateTask<Pick<Task, 'description'>> = async (
export const createTask: CreateTask<Pick<Task, 'description'>, Task> = async (
task,
context
) => {
Expand Down
41 changes: 30 additions & 11 deletions waspc/examples/todoApp/src/queries.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { type Task } from "wasp/entities";
import { HttpError } from "wasp/server";
import { type GetNumTasks, type GetTask, type GetTasks, type GetDate } from "wasp/server/operations";
import { type Task } from 'wasp/entities'
import { HttpError } from 'wasp/server'
import {
type GetNumTasks,
type GetTask,
type GetTasks,
type GetDate,
GetAnything,
GetTrueVoid,
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 prefix it with type as well?

} from 'wasp/server/operations'

export const getTasks: GetTasks<void, Task[]> = async (_args, context) => {
if (!context.user) {
Expand All @@ -10,20 +17,24 @@ export const getTasks: GetTasks<void, Task[]> = async (_args, context) => {
console.log('TEST_ENV_VAR', process.env.TEST_ENV_VAR)

const Task = context.entities.Task
const tasks = await Task.findMany(
{
where: { user: { id: context.user.id } },
orderBy: { id: 'asc' },
}
)
const tasks = await Task.findMany({
where: { user: { id: context.user.id } },
orderBy: { id: 'asc' },
})
return tasks
}

export const getNumTasks: GetNumTasks<void, number> = async (_args, context) => {
export const getNumTasks: GetNumTasks<void, number> = async (
_args,
context
) => {
return context.entities.Task.count()
}

export const getTask: GetTask<Pick<Task, 'id'>, Task> = async (where, context) => {
export const getTask: GetTask<Pick<Task, 'id'>, Task> = async (
where,
context
) => {
if (!context.user) {
throw new HttpError(401)
}
Expand All @@ -47,3 +58,11 @@ export const getTask: GetTask<Pick<Task, 'id'>, Task> = async (where, context) =
export const getDate: GetDate<void, Date> = async () => {
return new Date()
}

export const getAnything: GetAnything = async () => {
return 'anything'
}

export const getTrueVoid = (async () => {
return 'anything'
}) satisfies GetTrueVoid
Loading