Fix the auth helpers to only require fields they need from AuthIdentity (#2057)

This commit is contained in:
Mihovil Ilakovac 2024-06-25 14:01:56 +02:00 committed by GitHub
parent 720ef76a7a
commit b869eab352
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 196 additions and 111 deletions

View File

@ -1,12 +1,14 @@
{{={= =}=}}
import { type {= authIdentityEntityName =} } from '../entities/index.js'
import { type ProviderName } from '../server/_types/index.js'
import type { AuthUserData, AuthUser } from '../server/auth/user.js'
import type {
AuthUserData,
AuthUser,
UserEntityWithAuth,
} from '../server/auth/user.js'
/**
* We split the user.ts code into two files to avoid some server-only
* code (Oslo's hashing functions) being imported on the client.
*/
import { type UserEntityWithAuth } from '../server/auth/user.js'
// PUBLIC API
export function getEmail(user: UserEntityWithAuth): string | null {
@ -49,7 +51,7 @@ function makeAuthUser(data: AuthUserData): AuthUser {
};
}
function findUserIdentity(user: UserEntityWithAuth, providerName: ProviderName): {= authIdentityEntityName =} | null {
function findUserIdentity(user: UserEntityWithAuth, providerName: ProviderName): UserEntityWithAuth['auth']['identities'][number] | null {
if (!user.auth) {
return null;
}

View File

@ -29,7 +29,7 @@ export type AuthUser = AuthUserData & {
*
* TODO: Change this once/if we switch to strict mode. https://github.com/wasp-lang/wasp/issues/1938
*/
export type AuthUserData = Omit<UserEntityWithAuth, '{= authFieldOnUserEntityName =}'> & {
export type AuthUserData = Omit<CompleteUserEntityWithAuth, '{= authFieldOnUserEntityName =}'> & {
identities: {
{=# enabledProviders.isEmailAuthEnabled =}
email: Expand<UserFacingProviderData<'email'>> | null
@ -54,17 +54,36 @@ type UserFacingProviderData<PN extends ProviderName> = {
} & Omit<PossibleProviderData[PN], 'hashedPassword'>
// PRIVATE API
export type UserEntityWithAuth = {= userEntityName =} & {
{= authFieldOnUserEntityName =}: AuthEntityWithIdentities | null
export type CompleteUserEntityWithAuth =
MakeUserEntityWithAuth<CompleteAuthEntityWithIdentities>
// PRIVATE API
export type CompleteAuthEntityWithIdentities =
MakeAuthEntityWithIdentities<{= authIdentityEntityName =}>
// PRIVATE API
/**
* User entity with all of the auth related data that's needed for the user facing
* helper functions like `getUsername` and `getEmail`.
*/
export type UserEntityWithAuth = MakeUserEntityWithAuth<
MakeAuthEntityWithIdentities<
// It's constructed like the Complete* types, but only with the fields needed
// for the user facing functions.
Pick<{= authIdentityEntityName =}, 'providerName' | 'providerUserId'>
>
>
type MakeUserEntityWithAuth<AuthType> = {= userEntityName =} & {
{= authFieldOnUserEntityName =}: AuthType | null
}
type MakeAuthEntityWithIdentities<IdentityType> = {= authEntityName =} & {
{= identitiesFieldOnAuthEntityName =}: IdentityType[]
}
// PRIVATE API
export type AuthEntityWithIdentities = {= authEntityName =} & {
{= identitiesFieldOnAuthEntityName =}: {= authIdentityEntityName =}[]
}
// PRIVATE API
export function createAuthUserData(user: UserEntityWithAuth): AuthUserData {
export function createAuthUserData(user: CompleteUserEntityWithAuth): AuthUserData {
const { {= authFieldOnUserEntityName =}, ...rest } = user
if (!{= authFieldOnUserEntityName =}) {
throw new Error(`🐝 Error: trying to create a user without auth data.
@ -94,7 +113,7 @@ This should never happen, but it did which means there is a bug in the code.`)
}
function getProviderInfo<PN extends ProviderName>(
auth: AuthEntityWithIdentities,
auth: CompleteAuthEntityWithIdentities,
providerName: PN
):
| UserFacingProviderData<PN>
@ -112,7 +131,7 @@ function getProviderInfo<PN extends ProviderName>(
}
function getIdentity(
auth: AuthEntityWithIdentities,
auth: CompleteAuthEntityWithIdentities,
providerName: ProviderName
): {= authIdentityEntityName =} | null {
return auth.{= identitiesFieldOnAuthEntityName =}.find((i) => i.providerName === providerName) ?? null

View File

@ -158,7 +158,7 @@
"file",
"../out/sdk/wasp/auth/user.ts"
],
"43743b68e46a4bed06983596968b8d9cd6536d0eb8fb0cbd17100d7f82fc54cd"
"19b44865cb0a36e86b8ddcd7779ca33d517e08da2b25f66003a66bda543f1c44"
],
[
[
@ -536,7 +536,7 @@
"file",
"../out/sdk/wasp/server/auth/user.ts"
],
"eb5754af49265911b6423004068cd829cf2e4b1b6a25e2477318f7e07e3029c9"
"bc415fd61fe7333ffe30d850bd07b4eae823a6e7b70321f03d0d881808d1a412"
],
[
[

View File

@ -1,11 +1,14 @@
import { type AuthIdentity } from '../entities/index.js'
import { type ProviderName } from '../server/_types/index.js'
import type { AuthUserData, AuthUser } from '../server/auth/user.js'
import type {
AuthUserData,
AuthUser,
UserEntityWithAuth,
} from '../server/auth/user.js'
/**
* We split the user.ts code into two files to avoid some server-only
* code (Oslo's hashing functions) being imported on the client.
*/
import { type UserEntityWithAuth } from '../server/auth/user.js'
// PUBLIC API
export function getEmail(user: UserEntityWithAuth): string | null {
@ -48,7 +51,7 @@ function makeAuthUser(data: AuthUserData): AuthUser {
};
}
function findUserIdentity(user: UserEntityWithAuth, providerName: ProviderName): AuthIdentity | null {
function findUserIdentity(user: UserEntityWithAuth, providerName: ProviderName): UserEntityWithAuth['auth']['identities'][number] | null {
if (!user.auth) {
return null;
}

View File

@ -1,9 +1,8 @@
import type { AuthUserData, AuthUser } from '../server/auth/user.js';
import type { AuthUserData, AuthUser, UserEntityWithAuth } from '../server/auth/user.js';
/**
* We split the user.ts code into two files to avoid some server-only
* code (Oslo's hashing functions) being imported on the client.
*/
import { type UserEntityWithAuth } from '../server/auth/user.js';
export declare function getEmail(user: UserEntityWithAuth): string | null;
export declare function getUsername(user: UserEntityWithAuth): string | null;
export declare function getFirstProviderUserId(user?: UserEntityWithAuth): string | null;

View File

@ -1,3 +1,7 @@
/**
* We split the user.ts code into two files to avoid some server-only
* code (Oslo's hashing functions) being imported on the client.
*/
// PUBLIC API
export function getEmail(user) {
var _a, _b;

View File

@ -1 +1 @@
{"version":3,"file":"user.js","sourceRoot":"","sources":["../../auth/user.ts"],"names":[],"mappings":"AASA,aAAa;AACb,MAAM,UAAU,QAAQ,CAAC,IAAwB;;IAC/C,OAAO,MAAA,MAAA,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,0CAAE,cAAc,mCAAI,IAAI,CAAC;AACjE,CAAC;AAED,aAAa;AACb,MAAM,UAAU,WAAW,CAAC,IAAwB;;IAClD,OAAO,MAAA,MAAA,gBAAgB,CAAC,IAAI,EAAE,UAAU,CAAC,0CAAE,cAAc,mCAAI,IAAI,CAAC;AACpE,CAAC;AAED,aAAa;AACb,MAAM,UAAU,sBAAsB,CAAC,IAAyB;;IAC9D,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtF,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,MAAA,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,cAAc,mCAAI,IAAI,CAAC;AACxD,CAAC;AAQD,MAAM,UAAU,sBAAsB,CACpC,IAAyB;IAEzB,OAAO,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AACzC,CAAC;AAED,SAAS,YAAY,CAAC,IAAkB;IACtC,uCACK,IAAI,KACP,sBAAsB,EAAE,GAAG,EAAE;YAC3B,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAClE,OAAO,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACzD,CAAC,IACD;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAwB,EAAE,YAA0B;;IAC5E,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACf,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,MAAA,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAC9B,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,YAAY,KAAK,YAAY,CACrD,mCAAI,IAAI,CAAC;AACZ,CAAC"}
{"version":3,"file":"user.js","sourceRoot":"","sources":["../../auth/user.ts"],"names":[],"mappings":"AAOA;;;GAGG;AAEH,aAAa;AACb,MAAM,UAAU,QAAQ,CAAC,IAAwB;;IAC/C,OAAO,MAAA,MAAA,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,0CAAE,cAAc,mCAAI,IAAI,CAAC;AACjE,CAAC;AAED,aAAa;AACb,MAAM,UAAU,WAAW,CAAC,IAAwB;;IAClD,OAAO,MAAA,MAAA,gBAAgB,CAAC,IAAI,EAAE,UAAU,CAAC,0CAAE,cAAc,mCAAI,IAAI,CAAC;AACpE,CAAC;AAED,aAAa;AACb,MAAM,UAAU,sBAAsB,CAAC,IAAyB;;IAC9D,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtF,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,MAAA,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,cAAc,mCAAI,IAAI,CAAC;AACxD,CAAC;AAQD,MAAM,UAAU,sBAAsB,CACpC,IAAyB;IAEzB,OAAO,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AACzC,CAAC;AAED,SAAS,YAAY,CAAC,IAAkB;IACtC,uCACK,IAAI,KACP,sBAAsB,EAAE,GAAG,EAAE;YAC3B,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAClE,OAAO,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACzD,CAAC,IACD;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAwB,EAAE,YAA0B;;IAC5E,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACf,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,MAAA,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAC9B,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,YAAY,KAAK,YAAY,CACrD,mCAAI,IAAI,CAAC;AACZ,CAAC"}

View File

@ -5,7 +5,7 @@ import { Expand } from '../../universal/types.js';
export type AuthUser = AuthUserData & {
getFirstProviderUserId: () => string | null;
};
export type AuthUserData = Omit<UserEntityWithAuth, 'auth'> & {
export type AuthUserData = Omit<CompleteUserEntityWithAuth, 'auth'> & {
identities: {
google: Expand<UserFacingProviderData<'google'>> | null;
};
@ -13,11 +13,18 @@ export type AuthUserData = Omit<UserEntityWithAuth, 'auth'> & {
type UserFacingProviderData<PN extends ProviderName> = {
id: string;
} & Omit<PossibleProviderData[PN], 'hashedPassword'>;
export type UserEntityWithAuth = User & {
auth: AuthEntityWithIdentities | null;
export type CompleteUserEntityWithAuth = MakeUserEntityWithAuth<CompleteAuthEntityWithIdentities>;
export type CompleteAuthEntityWithIdentities = MakeAuthEntityWithIdentities<AuthIdentity>;
/**
* User entity with all of the auth related data that's needed for the user facing
* helper functions like `getUsername` and `getEmail`.
*/
export type UserEntityWithAuth = MakeUserEntityWithAuth<MakeAuthEntityWithIdentities<Pick<AuthIdentity, 'providerName' | 'providerUserId'>>>;
type MakeUserEntityWithAuth<AuthType> = User & {
auth: AuthType | null;
};
export type AuthEntityWithIdentities = Auth & {
identities: AuthIdentity[];
type MakeAuthEntityWithIdentities<IdentityType> = Auth & {
identities: IdentityType[];
};
export declare function createAuthUserData(user: UserEntityWithAuth): AuthUserData;
export declare function createAuthUserData(user: CompleteUserEntityWithAuth): AuthUserData;
export {};

View File

@ -1 +1 @@
{"version":3,"file":"user.js","sourceRoot":"","sources":["../../../server/auth/user.ts"],"names":[],"mappings":";;;;;;;;;;;AAKA,OAAO,EAEL,kCAAkC,EACnC,MAAM,qBAAqB,CAAA;AA0C5B,cAAc;AACd,MAAM,UAAU,kBAAkB,CAAC,IAAwB;IACzD,MAAM,EAAE,IAAI,KAAc,IAAI,EAAb,IAAI,UAAK,IAAI,EAAxB,QAAiB,CAAO,CAAA;IAC9B,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC;6EACyD,CAAC,CAAA;IAC5E,CAAC;IACD,MAAM,UAAU,GAAG;QACjB,MAAM,EAAE,eAAe,CAAW,IAAI,EAAE,QAAQ,CAAC;KAClD,CAAA;IACD,uCACK,IAAI,KACP,UAAU,IACX;AACH,CAAC;AAED,SAAS,eAAe,CACtB,IAA8B,EAC9B,YAAgB;IAIhB,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;IAChD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,IAAI,CAAA;IACb,CAAC;IACD,uCACK,kCAAkC,CAAK,QAAQ,CAAC,YAAY,EAAE;QAC/D,yBAAyB,EAAE,IAAI;KAChC,CAAC,KACF,EAAE,EAAE,QAAQ,CAAC,cAAc,IAC5B;AACH,CAAC;AAED,SAAS,WAAW,CAClB,IAA8B,EAC9B,YAA0B;;IAE1B,OAAO,MAAA,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,KAAK,YAAY,CAAC,mCAAI,IAAI,CAAA;AAC7E,CAAC"}
{"version":3,"file":"user.js","sourceRoot":"","sources":["../../../server/auth/user.ts"],"names":[],"mappings":";;;;;;;;;;;AAKA,OAAO,EAEL,kCAAkC,EACnC,MAAM,qBAAqB,CAAA;AA6D5B,cAAc;AACd,MAAM,UAAU,kBAAkB,CAAC,IAAgC;IACjE,MAAM,EAAE,IAAI,KAAc,IAAI,EAAb,IAAI,UAAK,IAAI,EAAxB,QAAiB,CAAO,CAAA;IAC9B,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC;6EACyD,CAAC,CAAA;IAC5E,CAAC;IACD,MAAM,UAAU,GAAG;QACjB,MAAM,EAAE,eAAe,CAAW,IAAI,EAAE,QAAQ,CAAC;KAClD,CAAA;IACD,uCACK,IAAI,KACP,UAAU,IACX;AACH,CAAC;AAED,SAAS,eAAe,CACtB,IAAsC,EACtC,YAAgB;IAIhB,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;IAChD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,IAAI,CAAA;IACb,CAAC;IACD,uCACK,kCAAkC,CAAK,QAAQ,CAAC,YAAY,EAAE;QAC/D,yBAAyB,EAAE,IAAI;KAChC,CAAC,KACF,EAAE,EAAE,QAAQ,CAAC,cAAc,IAC5B;AACH,CAAC;AAED,SAAS,WAAW,CAClB,IAAsC,EACtC,YAA0B;;IAE1B,OAAO,MAAA,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,KAAK,YAAY,CAAC,mCAAI,IAAI,CAAA;AAC7E,CAAC"}

View File

@ -28,7 +28,7 @@ export type AuthUser = AuthUserData & {
*
* TODO: Change this once/if we switch to strict mode. https://github.com/wasp-lang/wasp/issues/1938
*/
export type AuthUserData = Omit<UserEntityWithAuth, 'auth'> & {
export type AuthUserData = Omit<CompleteUserEntityWithAuth, 'auth'> & {
identities: {
google: Expand<UserFacingProviderData<'google'>> | null
},
@ -39,17 +39,36 @@ type UserFacingProviderData<PN extends ProviderName> = {
} & Omit<PossibleProviderData[PN], 'hashedPassword'>
// PRIVATE API
export type UserEntityWithAuth = User & {
auth: AuthEntityWithIdentities | null
export type CompleteUserEntityWithAuth =
MakeUserEntityWithAuth<CompleteAuthEntityWithIdentities>
// PRIVATE API
export type CompleteAuthEntityWithIdentities =
MakeAuthEntityWithIdentities<AuthIdentity>
// PRIVATE API
/**
* User entity with all of the auth related data that's needed for the user facing
* helper functions like `getUsername` and `getEmail`.
*/
export type UserEntityWithAuth = MakeUserEntityWithAuth<
MakeAuthEntityWithIdentities<
// It's constructed like the Complete* types, but only with the fields needed
// for the user facing functions.
Pick<AuthIdentity, 'providerName' | 'providerUserId'>
>
>
type MakeUserEntityWithAuth<AuthType> = User & {
auth: AuthType | null
}
type MakeAuthEntityWithIdentities<IdentityType> = Auth & {
identities: IdentityType[]
}
// PRIVATE API
export type AuthEntityWithIdentities = Auth & {
identities: AuthIdentity[]
}
// PRIVATE API
export function createAuthUserData(user: UserEntityWithAuth): AuthUserData {
export function createAuthUserData(user: CompleteUserEntityWithAuth): AuthUserData {
const { auth, ...rest } = user
if (!auth) {
throw new Error(`🐝 Error: trying to create a user without auth data.
@ -65,7 +84,7 @@ This should never happen, but it did which means there is a bug in the code.`)
}
function getProviderInfo<PN extends ProviderName>(
auth: AuthEntityWithIdentities,
auth: CompleteAuthEntityWithIdentities,
providerName: PN
):
| UserFacingProviderData<PN>
@ -83,7 +102,7 @@ function getProviderInfo<PN extends ProviderName>(
}
function getIdentity(
auth: AuthEntityWithIdentities,
auth: CompleteAuthEntityWithIdentities,
providerName: ProviderName
): AuthIdentity | null {
return auth.identities.find((i) => i.providerName === providerName) ?? null

View File

@ -1,6 +1,6 @@
app crudTesting {
wasp: {
version: "^0.13.0"
version: "^0.14.0"
},
head: [
"<link rel=\"stylesheet\" href=\"https://unpkg.com/mvp.css@1.12/mvp.css\">"

View File

@ -1,5 +1,4 @@
import { Link } from 'wasp/client/router'
import { type Task } from 'wasp/entities'
import {
useAction,
@ -13,12 +12,15 @@ import {
} from 'wasp/client/operations'
import React, { useState, FormEventHandler, ChangeEventHandler } from 'react'
import { getEmail } from 'wasp/auth'
type NonEmptyArray<T> = [T, ...T[]]
type TaskWithUser = Awaited<ReturnType<typeof getTasks>>[number]
export function areThereAnyTasks(
tasks: Task[] | undefined
): tasks is NonEmptyArray<Task> {
tasks: TaskWithUser[] | undefined
): tasks is NonEmptyArray<TaskWithUser> {
return !!(tasks && tasks.length > 0)
}
@ -55,7 +57,7 @@ const Todo = () => {
)
}
const Footer = ({ tasks }: { tasks: NonEmptyArray<Task> }) => {
const Footer = ({ tasks }: { tasks: NonEmptyArray<TaskWithUser> }) => {
const numCompletedTasks = tasks.filter((t) => t.isDone).length
const numUncompletedTasks = tasks.filter((t) => !t.isDone).length
@ -83,7 +85,7 @@ const Footer = ({ tasks }: { tasks: NonEmptyArray<Task> }) => {
)
}
const Tasks = ({ tasks }: { tasks: NonEmptyArray<Task> }) => {
const Tasks = ({ tasks }: { tasks: NonEmptyArray<TaskWithUser> }) => {
return (
<div>
<table className="border-separate border-spacing-2">
@ -97,9 +99,9 @@ const Tasks = ({ tasks }: { tasks: NonEmptyArray<Task> }) => {
)
}
type UpdateTaskIsDonePayload = Pick<Task, 'id' | 'isDone'>
type UpdateTaskIsDonePayload = Pick<TaskWithUser, 'id' | 'isDone'>
const TaskView = ({ task }: { task: Task }) => {
const TaskView = ({ task }: { task: TaskWithUser }) => {
const updateTaskIsDoneOptimistically = useAction(updateTaskIsDone, {
optimisticUpdates: [
{
@ -114,7 +116,7 @@ const TaskView = ({ task }: { task: Task }) => {
)
}
},
} as OptimisticUpdateDefinition<UpdateTaskIsDonePayload, Task[]>,
} as OptimisticUpdateDefinition<UpdateTaskIsDonePayload, TaskWithUser[]>,
],
})
const handleTaskIsDoneChange: ChangeEventHandler<HTMLInputElement> = async (
@ -130,6 +132,8 @@ const TaskView = ({ task }: { task: Task }) => {
}
}
const email = getEmail(task.user)
return (
<tr>
<td>
@ -143,7 +147,7 @@ const TaskView = ({ task }: { task: Task }) => {
</td>
<td>
<Link to="/task/:id" params={{ id: task.id }}>
{task.description}
{task.description} {email && `by ${email}`}
</Link>
</td>
</tr>
@ -158,21 +162,26 @@ const NewTaskForm = () => {
{
getQuerySpecifier: () => [getTasks],
updateQuery: (newTask, oldTasks) => {
const newTaskWithUser = {
...newTask,
user: {},
} as TaskWithUser
if (oldTasks === undefined) {
// cache is empty
return [newTask as Task]
return [newTaskWithUser]
} else {
return [...oldTasks, newTask as Task]
return [...oldTasks, newTaskWithUser]
}
},
} as OptimisticUpdateDefinition<
Pick<Task, 'isDone' | 'description'>,
Task[]
Pick<TaskWithUser, 'isDone' | 'description'>,
TaskWithUser[]
>,
],
})
const createNewTask = async (description: Task['description']) => {
const createNewTask = async (description: TaskWithUser['description']) => {
const task = { isDone: false, description }
await createTaskFn(task)
}

View File

@ -1,8 +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,
} from 'wasp/server/operations'
export const getTasks: GetTasks<void, Task[]> = async (_args, context) => {
export const getTasks = (async (_args, context) => {
if (!context.user) {
throw new HttpError(401)
}
@ -10,20 +15,40 @@ 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' },
include: {
user: {
include: {
auth: {
include: {
identities: {
select: {
providerName: true,
providerUserId: true,
},
},
},
},
},
},
},
})
return tasks
}
}) satisfies GetTasks<void>
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)
}

View File

@ -1,62 +1,65 @@
import { test, expect } from '@playwright/test'
import { test, expect } from "@playwright/test";
import {
generateRandomCredentials,
performLogin,
performSignup,
} from './helpers'
} from "./helpers";
test('has title', async ({ page }) => {
await page.goto('/')
test("has title", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveTitle(/ToDo App/)
})
test.describe('signup and login', () => {
const { email, password } = generateRandomCredentials()
await expect(page).toHaveTitle(/ToDo App/);
});
test.describe("signup and login", () => {
const { email, password } = generateRandomCredentials();
test.describe.configure({ mode: 'serial' })
test.describe.configure({ mode: "serial" });
test('social button renders', async ({ page }) => {
await page.goto('/signup')
test("social button renders", async ({ page }) => {
await page.goto("/signup");
await page.waitForSelector('text=Create a new account')
await page.waitForSelector("text=Create a new account");
await expect(
page.locator("a[href='http://localhost:3001/auth/google/login']"),
).toBeVisible()
})
page.locator("a[href='http://localhost:3001/auth/google/login']")
).toBeVisible();
});
test('can sign up', async ({ page }) => {
test("can sign up", async ({ page }) => {
await performSignup(page, {
email,
password,
})
});
await expect(page.locator('body')).toContainText(
`You've signed up successfully! Check your email for the confirmation link.`,
)
})
await expect(page.locator("body")).toContainText(
`You've signed up successfully! Check your email for the confirmation link.`
);
});
test('can log in and create a task', async ({ page }) => {
test("can log in and create a task", async ({ page }) => {
await performLogin(page, {
email,
password: '12345678xxx',
})
password: "12345678xxx",
});
await expect(page.locator('body')).toContainText('Invalid credentials')
await expect(page.locator("body")).toContainText("Invalid credentials");
await performLogin(page, {
email,
password,
})
});
await expect(page).toHaveURL('/profile')
await expect(page).toHaveURL("/profile");
await page.goto('/')
await page.goto("/");
const randomTask = `New Task ${Math.random().toString(36).substring(7)}`
await page.locator("input[type='text']").fill(randomTask)
await page.getByText('Create new task').click()
const randomTask = `New Task ${Math.random().toString(36).substring(7)}`;
await page.locator("input[type='text']").fill(randomTask);
await page.getByText("Create new task").click();
await expect(page.locator('body')).toContainText(randomTask)
})
})
const fullTaskText = `${randomTask} by ${email}`;
await page.waitForSelector(`text=${fullTaskText}`);
await expect(page.locator("body")).toContainText(fullTaskText);
});
});

View File

@ -30,7 +30,7 @@ genAuth spec =
Just auth ->
-- shared stuff
sequence
[ genUserTs
[ genFileCopy [relfile|auth/user.ts|]
]
-- client stuff
<++> sequence
@ -133,8 +133,3 @@ genProvidersTypes auth = return $ C.mkTmplFdWithData [relfile|auth/providers/typ
userEntityName = AS.refName $ AS.Auth.userEntity auth
tmplData = object ["userEntityUpper" .= (userEntityName :: String)]
genUserTs :: Generator FileDraft
genUserTs = return $ C.mkTmplFdWithData [relfile|auth/user.ts|] tmplData
where
tmplData = object ["authIdentityEntityName" .= DbAuth.authIdentityEntityName]