From da8b259bb146b1c07afc84be6b31b56fab148525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Sat, 4 Nov 2023 19:44:52 +0100 Subject: [PATCH 01/54] Early prototype --- .../templates/react-app/src/index.tsx | 2 +- .../templates/react-app/src/router.tsx | 2 +- waspc/examples/todo-typescript/.gitignore | 4 + waspc/examples/todo-typescript/.wasproot | 1 + waspc/examples/todo-typescript/main.wasp | 68 ++++ .../20230816092617_migracijone/migration.sql | 18 + .../migrations/migration_lock.toml | 3 + .../node_modules/@wasp/.queries/core.d.ts | 23 ++ .../node_modules/@wasp/.queries/core.js | 27 ++ .../node_modules/@wasp/.queries/getTasks.ts | 10 + .../node_modules/@wasp/.queries/index.d.ts | 10 + .../node_modules/@wasp/.queries/index.js | 18 + .../node_modules/@wasp/.queries/queries.ts | 4 + .../node_modules/@wasp/api/events.ts | 11 + .../node_modules/@wasp/api/index.ts | 109 ++++++ .../node_modules/@wasp/api/package.json | 3 + .../node_modules/@wasp/auth/forms/Auth.tsx | 89 +++++ .../node_modules/@wasp/auth/forms/Login.tsx | 18 + .../node_modules/@wasp/auth/forms/Signup.tsx | 18 + .../@wasp/auth/forms/internal/Form.tsx | 83 +++++ .../@wasp/auth/forms/internal/Message.tsx | 18 + .../forms/internal/common/LoginSignupForm.tsx | 89 +++++ .../useUsernameAndPassword.ts | 42 +++ .../node_modules/@wasp/auth/forms/types.ts | 17 + .../node_modules/@wasp/auth/helpers/user.ts | 14 + .../node_modules/@wasp/auth/login.ts | 16 + .../node_modules/@wasp/auth/logout.ts | 9 + .../node_modules/@wasp/auth/package.json | 3 + .../auth/pages/createAuthRequiredPage.jsx | 30 ++ .../node_modules/@wasp/auth/signup.ts | 12 + .../@wasp/auth/stitches.config.js | 31 ++ .../node_modules/@wasp/auth/types.ts | 2 + .../node_modules/@wasp/auth/useAuth.ts | 38 ++ .../node_modules/@wasp/core/AuthError.js | 17 + .../node_modules/@wasp/core/HttpError.js | 22 ++ .../node_modules/@wasp/core/auth.js | 130 +++++++ .../@wasp/core/auth/prismaMiddleware.js | 84 +++++ .../@wasp/core/auth/validators.ts | 5 + .../node_modules/@wasp/core/config.js | 11 + .../node_modules/@wasp/core/package.json | 20 ++ .../@wasp/core/stitches.config.js | 31 ++ .../node_modules/@wasp/core/storage.ts | 50 +++ .../node_modules/@wasp/entities/index.ts | 19 + .../node_modules/@wasp/ext-src/actions.ts | 36 ++ .../node_modules/@wasp/ext-src/queries.ts | 14 + .../node_modules/@wasp/ext-src/serverTypes.ts | 11 + .../node_modules/@wasp/operations/index.ts | 22 ++ .../@wasp/operations/package.json | 7 + .../@wasp/operations/resources.js | 85 +++++ .../@wasp/operations/updateHandlersMap.js | 37 ++ .../node_modules/@wasp/rpc/actions/core.d.ts | 13 + .../node_modules/@wasp/rpc/actions/core.js | 40 +++ .../node_modules/@wasp/rpc/actions/index.ts | 10 + .../node_modules/@wasp/rpc/index.ts | 338 ++++++++++++++++++ .../node_modules/@wasp/rpc/package.json | 3 + .../node_modules/@wasp/rpc/queries/core.d.ts | 23 ++ .../node_modules/@wasp/rpc/queries/core.js | 30 ++ .../node_modules/@wasp/rpc/queries/index.ts | 6 + .../node_modules/@wasp/rpc/queryClient.ts | 26 ++ .../node_modules/@wasp/server/_types/index.ts | 100 ++++++ .../@wasp/server/_types/serialization.ts | 35 ++ .../@wasp/server/_types/taggedEntities.ts | 20 ++ .../@wasp/server/actions/index.ts | 27 ++ .../@wasp/server/actions/types.ts | 24 ++ .../@wasp/server/core/AuthError.js | 17 + .../@wasp/server/core/HttpError.js | 22 ++ .../node_modules/@wasp/server/core/auth.js | 130 +++++++ .../server/core/auth/prismaMiddleware.js | 84 +++++ .../@wasp/server/core/auth/validators.ts | 5 + .../node_modules/@wasp/server/dbClient.ts | 15 + .../@wasp/server/queries/index.ts | 13 + .../@wasp/server/queries/types.ts | 6 + .../node_modules/@wasp/types/index.ts | 9 + .../node_modules/@wasp/types/package.json | 3 + .../node_modules/@wasp/universal/package.json | 3 + .../node_modules/@wasp/universal/types.ts | 31 ++ .../node_modules/@wasp/universal/url.ts | 3 + .../todo-typescript/package-lock.json | 202 +++++++++++ waspc/examples/todo-typescript/package.json | 5 + .../examples/todo-typescript/src/.waspignore | 3 + .../todo-typescript/src/client/LoginPage.tsx | 16 + .../todo-typescript/src/client/Main.css | 53 +++ .../todo-typescript/src/client/MainPage.tsx | 94 +++++ .../todo-typescript/src/client/SignupPage.tsx | 16 + .../todo-typescript/src/client/opcije.md | 132 +++++++ .../todo-typescript/src/client/tsconfig.json | 28 ++ .../todo-typescript/src/client/types.ts | 6 + .../todo-typescript/src/client/vite-env.d.ts | 1 + .../todo-typescript/src/client/waspLogo.png | Bin 0 -> 24877 bytes .../todo-typescript/src/server/actions.ts | 37 ++ .../todo-typescript/src/server/queries.ts | 9 + .../todo-typescript/src/server/serverTypes.ts | 11 + .../todo-typescript/src/server/tsconfig.json | 48 +++ .../todo-typescript/src/shared/tsconfig.json | 28 ++ waspc/examples/todo-typescript/tsconfig.json | 28 ++ .../Generator/ExternalCodeGenerator/Js.hs | 7 +- waspc/src/Wasp/Generator/Setup.hs | 7 +- 97 files changed, 3198 insertions(+), 12 deletions(-) create mode 100644 waspc/examples/todo-typescript/.gitignore create mode 100644 waspc/examples/todo-typescript/.wasproot create mode 100644 waspc/examples/todo-typescript/main.wasp create mode 100644 waspc/examples/todo-typescript/migrations/20230816092617_migracijone/migration.sql create mode 100644 waspc/examples/todo-typescript/migrations/migration_lock.toml create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.d.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.js create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/.queries/getTasks.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/.queries/index.d.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/.queries/index.js create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/.queries/queries.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/api/events.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/api/index.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/api/package.json create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/Auth.tsx create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/Login.tsx create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/Signup.tsx create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/Form.tsx create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/Message.tsx create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/common/LoginSignupForm.tsx create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/types.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/helpers/user.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/login.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/logout.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/package.json create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/pages/createAuthRequiredPage.jsx create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/signup.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/stitches.config.js create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/types.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/useAuth.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/core/AuthError.js create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/core/HttpError.js create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/core/auth.js create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/core/auth/prismaMiddleware.js create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/core/auth/validators.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/core/config.js create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/core/package.json create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/core/stitches.config.js create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/core/storage.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/entities/index.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/ext-src/actions.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/ext-src/queries.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/ext-src/serverTypes.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/operations/index.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/operations/package.json create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/operations/resources.js create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/operations/updateHandlersMap.js create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/core.d.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/core.js create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/index.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/rpc/index.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/rpc/package.json create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/core.d.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/core.js create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/index.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/rpc/queryClient.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/server/_types/index.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/server/_types/serialization.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/server/_types/taggedEntities.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/server/actions/index.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/server/actions/types.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/server/core/AuthError.js create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/server/core/HttpError.js create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth.js create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth/prismaMiddleware.js create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth/validators.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/server/dbClient.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/server/queries/index.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/server/queries/types.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/types/index.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/types/package.json create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/universal/package.json create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/universal/types.ts create mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/universal/url.ts create mode 100644 waspc/examples/todo-typescript/package-lock.json create mode 100644 waspc/examples/todo-typescript/package.json create mode 100644 waspc/examples/todo-typescript/src/.waspignore create mode 100644 waspc/examples/todo-typescript/src/client/LoginPage.tsx create mode 100644 waspc/examples/todo-typescript/src/client/Main.css create mode 100644 waspc/examples/todo-typescript/src/client/MainPage.tsx create mode 100644 waspc/examples/todo-typescript/src/client/SignupPage.tsx create mode 100644 waspc/examples/todo-typescript/src/client/opcije.md create mode 100644 waspc/examples/todo-typescript/src/client/tsconfig.json create mode 100644 waspc/examples/todo-typescript/src/client/types.ts create mode 100644 waspc/examples/todo-typescript/src/client/vite-env.d.ts create mode 100644 waspc/examples/todo-typescript/src/client/waspLogo.png create mode 100644 waspc/examples/todo-typescript/src/server/actions.ts create mode 100644 waspc/examples/todo-typescript/src/server/queries.ts create mode 100644 waspc/examples/todo-typescript/src/server/serverTypes.ts create mode 100644 waspc/examples/todo-typescript/src/server/tsconfig.json create mode 100644 waspc/examples/todo-typescript/src/shared/tsconfig.json create mode 100644 waspc/examples/todo-typescript/tsconfig.json diff --git a/waspc/data/Generator/templates/react-app/src/index.tsx b/waspc/data/Generator/templates/react-app/src/index.tsx index dc0c1171ed..ef2e68bafb 100644 --- a/waspc/data/Generator/templates/react-app/src/index.tsx +++ b/waspc/data/Generator/templates/react-app/src/index.tsx @@ -7,7 +7,7 @@ import router from './router' import { initializeQueryClient, queryClientInitialized, -} from './queryClient' +} from '@wasp/rpc/queryClient' {=# setupFn.isDefined =} {=& setupFn.importStatement =} diff --git a/waspc/data/Generator/templates/react-app/src/router.tsx b/waspc/data/Generator/templates/react-app/src/router.tsx index 46be850b22..1d5f94d713 100644 --- a/waspc/data/Generator/templates/react-app/src/router.tsx +++ b/waspc/data/Generator/templates/react-app/src/router.tsx @@ -12,7 +12,7 @@ import type { {=/ rootComponent.isDefined =} {=# isAuthEnabled =} -import createAuthRequiredPage from "./auth/pages/createAuthRequiredPage" +import createAuthRequiredPage from "@wasp/auth/pages/createAuthRequiredPage" {=/ isAuthEnabled =} {=# pagesToImport =} diff --git a/waspc/examples/todo-typescript/.gitignore b/waspc/examples/todo-typescript/.gitignore new file mode 100644 index 0000000000..ab7cafccec --- /dev/null +++ b/waspc/examples/todo-typescript/.gitignore @@ -0,0 +1,4 @@ +/.wasp/ +/.env.server +/.env.client +/node_modules/ diff --git a/waspc/examples/todo-typescript/.wasproot b/waspc/examples/todo-typescript/.wasproot new file mode 100644 index 0000000000..ca2cfdb482 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasproot @@ -0,0 +1 @@ +File marking the root of Wasp project. diff --git a/waspc/examples/todo-typescript/main.wasp b/waspc/examples/todo-typescript/main.wasp new file mode 100644 index 0000000000..8f3c274adf --- /dev/null +++ b/waspc/examples/todo-typescript/main.wasp @@ -0,0 +1,68 @@ +app TodoTypescript { + wasp: { + version: "^0.11.0" + }, + title: "ToDo TypeScript", + + auth: { + userEntity: User, + methods: { + usernameAndPassword: {}, + }, + onAuthFailedRedirectTo: "/login", + } +} + +// Use Prisma Schema Language (PSL) to define our entities: https://www.prisma.io/docs/concepts/components/prisma-schema +// Run `wasp db migrate-dev` in the CLI to create the database tables +// Then run `wasp db studio` to open Prisma Studio and view your db models +entity User {=psl + id Int @id @default(autoincrement()) + username String @unique + password String + tasks Task[] +psl=} + +entity Task {=psl + id Int @id @default(autoincrement()) + description String + isDone Boolean @default(false) + user User? @relation(fields: [userId], references: [id]) + userId Int? +psl=} + +route RootRoute { path: "/", to: MainPage } +page MainPage { + authRequired: true, + component: import { MainPage } from "@client/MainPage.tsx" +} + +route LoginRoute { path: "/login", to: LoginPage } +page LoginPage { + component: import { LoginPage } from "@client/LoginPage.tsx" +} + +route SignupRoute { path: "/signup", to: SignupPage } +page SignupPage { + component: import { SignupPage } from "@client/SignupPage.tsx" +} + +query getTasks { + // We specify the JS implementation of our query (which is an async JS function) + // Even if you use TS and have a queries.ts file, you will still need to import it using the .js extension. + // see here for more info: https://wasp-lang.dev/docs/tutorials/todo-app/03-listing-tasks#wasp-declaration + fn: import { getTasks } from "@server/queries.js", + // We tell Wasp that this query is doing something with the `Task` entity. With that, Wasp will + // automatically refresh the results of this query when tasks change. + entities: [Task] +} + +action createTask { + fn: import { createTask } from "@server/actions.js", + entities: [Task] +} + +action updateTask { + fn: import { updateTask } from "@server/actions.js", + entities: [Task] +} diff --git a/waspc/examples/todo-typescript/migrations/20230816092617_migracijone/migration.sql b/waspc/examples/todo-typescript/migrations/20230816092617_migracijone/migration.sql new file mode 100644 index 0000000000..7b62627672 --- /dev/null +++ b/waspc/examples/todo-typescript/migrations/20230816092617_migracijone/migration.sql @@ -0,0 +1,18 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "username" TEXT NOT NULL, + "password" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "Task" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "description" TEXT NOT NULL, + "isDone" BOOLEAN NOT NULL DEFAULT false, + "userId" INTEGER, + CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); diff --git a/waspc/examples/todo-typescript/migrations/migration_lock.toml b/waspc/examples/todo-typescript/migrations/migration_lock.toml new file mode 100644 index 0000000000..e5e5c4705a --- /dev/null +++ b/waspc/examples/todo-typescript/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.d.ts b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.d.ts new file mode 100644 index 0000000000..e1bdbe4783 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.d.ts @@ -0,0 +1,23 @@ +import { type Query } from '.' +import { Route } from '../types'; +import type { Expand, _Awaited, _ReturnType } from '../universal/types' + +export function createQuery( + queryRoute: string, + entitiesUsed: any[] +): QueryFor + +export function addMetadataToQuery( + query: (...args: any[]) => Promise, + metadata: { + relativeQueryPath: string; + queryRoute: Route; + entitiesUsed: string[]; + }, +): void + +type QueryFor = Expand< + Query[0], _Awaited<_ReturnType>> +> + +type GenericBackendQuery = (args: never, context: any) => unknown diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.js b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.js new file mode 100644 index 0000000000..5103db1d8b --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.js @@ -0,0 +1,27 @@ +import { callOperation, makeOperationRoute } from '../operations' +import { + addResourcesUsedByQuery, + getActiveOptimisticUpdates, +} from '../operations/resources' + +export function createQuery(relativeQueryPath, entitiesUsed) { + const queryRoute = makeOperationRoute(relativeQueryPath) + + async function query(queryKey, queryArgs) { + const serverResult = await callOperation(queryRoute, queryArgs) + return getActiveOptimisticUpdates(queryKey).reduce( + (result, update) => update(result), + serverResult, + ) + } + + addMetadataToQuery(query, { relativeQueryPath, queryRoute, entitiesUsed }) + + return query +} + +export function addMetadataToQuery(query, { relativeQueryPath, queryRoute, entitiesUsed }) { + query.queryCacheKey = [relativeQueryPath] + query.route = queryRoute + addResourcesUsedByQuery(query.queryCacheKey, entitiesUsed) +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/getTasks.ts b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/getTasks.ts new file mode 100644 index 0000000000..7c1d542ccc --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/getTasks.ts @@ -0,0 +1,10 @@ +import { createQuery } from './core' +import { GetTasks } from '../../../server/src/queries/getTasks' + + +const query = createQuery( + 'operations/get-tasks', + ['Task'], +) + +export default query diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/index.d.ts b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/index.d.ts new file mode 100644 index 0000000000..c007ff4c92 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/index.d.ts @@ -0,0 +1,10 @@ +import { UseQueryResult } from "@tanstack/react-query"; + +export type Query = { + (queryCacheKey: string[], args: Input): Promise +} + +export function useQuery( + queryFn: Query, + queryFnArgs?: Input, options?: any +): UseQueryResult diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/index.js b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/index.js new file mode 100644 index 0000000000..03e52ce0f9 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/index.js @@ -0,0 +1,18 @@ +import { useQuery as rqUseQuery } from '@tanstack/react-query' +export { configureQueryClient } from '../queryClient' + +export function useQuery(queryFn, queryFnArgs, options) { + 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 queryKey = queryFnArgs !== undefined ? [...queryFn.queryCacheKey, queryFnArgs] : queryFn.queryCacheKey + return rqUseQuery({ + queryKey, + queryFn: () => queryFn(queryKey, queryFnArgs), + ...options + }) +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/queries.ts b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/queries.ts new file mode 100644 index 0000000000..678281f07f --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/queries.ts @@ -0,0 +1,4 @@ +import { createQuery } from "./core"; +import { GetTasks } from "../server/queries"; + +export const getTasks = createQuery("operations/get-tasks", ["Task"]); diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/api/events.ts b/waspc/examples/todo-typescript/node_modules/@wasp/api/events.ts new file mode 100644 index 0000000000..9a59b366d3 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/api/events.ts @@ -0,0 +1,11 @@ +import mitt, { Emitter } from 'mitt'; + +type ApiEvents = { + // key: Event name + // type: Event payload type + 'authToken.set': void; + 'authToken.clear': void; +}; + +// Used to allow API clients to register for auth token change events. +export const apiEventsEmitter: Emitter = mitt(); diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/api/index.ts b/waspc/examples/todo-typescript/node_modules/@wasp/api/index.ts new file mode 100644 index 0000000000..b0a2dd7f44 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/api/index.ts @@ -0,0 +1,109 @@ +import axios, { type AxiosError } from "axios"; + +import config from "@wasp/core/config"; +import { storage } from "@wasp/core/storage"; +import { apiEventsEmitter } from "@wasp/api/events"; + +const api = axios.create({ + baseURL: config.apiUrl, +}); + +const WASP_APP_AUTH_TOKEN_NAME = "authToken"; + +let authToken = storage.get(WASP_APP_AUTH_TOKEN_NAME) as string | undefined; + +export function setAuthToken(token: string): void { + authToken = token; + storage.set(WASP_APP_AUTH_TOKEN_NAME, token); + apiEventsEmitter.emit("authToken.set"); +} + +export function getAuthToken(): string | undefined { + return authToken; +} + +export function clearAuthToken(): void { + authToken = undefined; + storage.remove(WASP_APP_AUTH_TOKEN_NAME); + apiEventsEmitter.emit("authToken.clear"); +} + +export function removeLocalUserData(): void { + authToken = undefined; + storage.clear(); + apiEventsEmitter.emit("authToken.clear"); +} + +api.interceptors.request.use((request) => { + if (authToken) { + request.headers["Authorization"] = `Bearer ${authToken}`; + } + return request; +}); + +api.interceptors.response.use(undefined, (error) => { + if (error.response?.status === 401) { + clearAuthToken(); + } + return Promise.reject(error); +}); + +// This handler will run on other tabs (not the active one calling API functions), +// and will ensure they know about auth token changes. +// Ref: https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event +// "Note: This won't work on the same page that is making the changes — it is really a way +// for other pages on the domain using the storage to sync any changes that are made." +window.addEventListener("storage", (event) => { + if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_TOKEN_NAME)) { + if (!!event.newValue) { + authToken = event.newValue; + apiEventsEmitter.emit("authToken.set"); + } else { + authToken = undefined; + apiEventsEmitter.emit("authToken.clear"); + } + } +}); + +/** + * Takes an error returned by the app's API (as returned by axios), and transforms into a more + * standard format to be further used by the client. It is also assumed that given API + * error has been formatted as implemented by HttpError on the server. + */ +export function handleApiError( + error: AxiosError<{ message?: string; data?: unknown }> +): void { + if (error?.response) { + // If error came from HTTP response, we capture most informative message + // and also add .statusCode information to it. + // If error had JSON response, we assume it is of format { message, data } and + // add that info to the error. + // TODO: We might want to use HttpError here instead of just Error, since + // HttpError is also used on server to throw errors like these. + // That would require copying HttpError code to web-app also and using it here. + const responseJson = error.response?.data; + const responseStatusCode = error.response.status; + throw new WaspHttpError( + responseStatusCode, + responseJson?.message ?? error.message, + responseJson + ); + } else { + // If any other error, we just propagate it. + throw error; + } +} + +class WaspHttpError extends Error { + statusCode: number; + + data: unknown; + + constructor(statusCode: number, message: string, data: unknown) { + super(message); + this.statusCode = statusCode; + this.data = data; + } +} + +export default api; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/api/package.json b/waspc/examples/todo-typescript/node_modules/@wasp/api/package.json new file mode 100644 index 0000000000..2673a1a809 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/api/package.json @@ -0,0 +1,3 @@ +{ + "name": "api" +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/Auth.tsx b/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/Auth.tsx new file mode 100644 index 0000000000..e84f0d3717 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/Auth.tsx @@ -0,0 +1,89 @@ +import React from "react"; +import { useState, createContext } from "react"; +import { createTheme } from "@stitches/react"; +import { styled } from "@wasp/core/stitches.config"; + +import { + type State, + type CustomizationOptions, + type ErrorMessage, +} from "./types"; +import { LoginSignupForm } from "./internal/common/LoginSignupForm"; +import { MessageError, MessageSuccess } from "./internal/Message"; + +const logoStyle = { + height: "3rem", +}; + +const Container = styled("div", { + display: "flex", + flexDirection: "column", +}); + +const HeaderText = styled("h2", { + fontSize: "1.875rem", + fontWeight: "700", + marginTop: "1.5rem", +}); + +export const AuthContext = createContext({ + isLoading: false, + setIsLoading: (isLoading: boolean) => {}, + setErrorMessage: (errorMessage: ErrorMessage | null) => {}, + setSuccessMessage: (successMessage: string | null) => {}, +}); + +export function Auth({ + state, + appearance, + logo, + socialLayout = "horizontal", +}: { + state: State; +} & CustomizationOptions) { + const [errorMessage, setErrorMessage] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + // TODO(matija): this is called on every render, is it a problem? + // If we do it in useEffect(), then there is a glitch between the default color and the + // user provided one. + const customTheme = createTheme(appearance ?? {}); + + const titles: Record = { + login: "Log in to your account", + signup: "Create a new account", + }; + const title = titles[state]; + + const socialButtonsDirection = + socialLayout === "vertical" ? "vertical" : "horizontal"; + + return ( + +
+ {logo && Your Company} + {title} +
+ + {errorMessage && ( + + {errorMessage.title} + {errorMessage.description && ": "} + {errorMessage.description} + + )} + {successMessage && {successMessage}} + + {(state === "login" || state === "signup") && ( + + )} + +
+ ); +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/Login.tsx b/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/Login.tsx new file mode 100644 index 0000000000..19ff0fa583 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/Login.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { Auth } from "./Auth"; +import { type CustomizationOptions, State } from "./types"; + +export function LoginForm({ + appearance, + logo, + socialLayout, +}: CustomizationOptions) { + return ( + + ); +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/Signup.tsx b/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/Signup.tsx new file mode 100644 index 0000000000..8978d891d9 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/Signup.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { Auth } from "./Auth"; +import { type CustomizationOptions, State } from "./types"; + +export function SignupForm({ + appearance, + logo, + socialLayout, +}: CustomizationOptions) { + return ( + + ); +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/Form.tsx b/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/Form.tsx new file mode 100644 index 0000000000..611d98f345 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/Form.tsx @@ -0,0 +1,83 @@ +import { styled } from "@wasp/core/stitches.config"; + +export const Form = styled("form", { + marginTop: "1.5rem", +}); + +export const FormItemGroup = styled("div", { + "& + div": { + marginTop: "1.5rem", + }, +}); + +export const FormLabel = styled("label", { + display: "block", + fontSize: "$sm", + fontWeight: "500", +}); + +export const FormInput = styled("input", { + display: "block", + lineHeight: "1.5rem", + fontSize: "$sm", + borderWidth: "1px", + borderColor: "$gray600", + backgroundColor: "#f8f4ff", + boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)", + "&:focus": { + borderWidth: "1px", + borderColor: "$gray700", + boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)", + }, + "&:disabled": { + opacity: 0.5, + cursor: "not-allowed", + backgroundColor: "$gray400", + borderColor: "$gray400", + color: "$gray500", + }, + + borderRadius: "0.375rem", + width: "100%", + + paddingTop: "0.375rem", + paddingBottom: "0.375rem", + paddingLeft: "0.75rem", + paddingRight: "0.75rem", + + marginTop: "0.5rem", +}); + +export const SubmitButton = styled("button", { + display: "flex", + justifyContent: "center", + + width: "100%", + borderWidth: "1px", + borderColor: "$brand", + backgroundColor: "$brand", + color: "$submitButtonText", + + padding: "0.5rem 0.75rem", + boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)", + + fontWeight: "600", + fontSize: "$sm", + lineHeight: "1.25rem", + borderRadius: "0.375rem", + + // TODO(matija): extract this into separate BaseButton component and then inherit it. + "&:hover": { + backgroundColor: "$brandAccent", + borderColor: "$brandAccent", + }, + "&:disabled": { + opacity: 0.5, + cursor: "not-allowed", + backgroundColor: "$gray400", + borderColor: "$gray400", + color: "$gray500", + }, + transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)", + transitionDuration: "100ms", +}); diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/Message.tsx b/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/Message.tsx new file mode 100644 index 0000000000..c0b28e33e9 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/Message.tsx @@ -0,0 +1,18 @@ +import { styled } from "@wasp/core/stitches.config"; + +export const Message = styled("div", { + padding: "0.5rem 0.75rem", + borderRadius: "0.375rem", + marginTop: "1rem", + background: "$gray400", +}); + +export const MessageError = styled(Message, { + background: "$errorBackground", + color: "$errorText", +}); + +export const MessageSuccess = styled(Message, { + background: "$successBackground", + color: "$successText", +}); diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/common/LoginSignupForm.tsx b/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/common/LoginSignupForm.tsx new file mode 100644 index 0000000000..02ee07ec5f --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/common/LoginSignupForm.tsx @@ -0,0 +1,89 @@ +import { useContext, type FormEvent } from "react"; +import { styled } from "@wasp/core/stitches.config"; +import config from "@wasp/core/config.js"; + +import { AuthContext } from "../../Auth"; +import { + Form, + FormInput, + FormItemGroup, + FormLabel, + SubmitButton, +} from "../Form"; +import { useHistory } from "react-router-dom"; +import { useUsernameAndPassword } from "../usernameAndPassword/useUsernameAndPassword"; + +export const LoginSignupForm = ({ + state, + socialButtonsDirection = "horizontal", +}: { + state: "login" | "signup"; + socialButtonsDirection?: "horizontal" | "vertical"; +}) => { + const { isLoading, setErrorMessage, setSuccessMessage, setIsLoading } = + useContext(AuthContext); + const cta = state === "login" ? "Log in" : "Sign up"; + const history = useHistory(); + const onErrorHandler = (error) => { + setErrorMessage({ + title: error.message, + description: error.data?.data?.message, + }); + }; + const { + handleSubmit, + usernameFieldVal, + passwordFieldVal, + setUsernameFieldVal, + setPasswordFieldVal, + } = useUsernameAndPassword({ + isLogin: state === "login", + onError: onErrorHandler, + onSuccess() { + history.push("/"); + }, + }); + async function onSubmit(event: FormEvent) { + event.preventDefault(); + setIsLoading(true); + setErrorMessage(null); + setSuccessMessage(null); + try { + await handleSubmit(); + } finally { + setIsLoading(false); + } + } + + return ( + <> +
+ + Username + setUsernameFieldVal(e.target.value)} + disabled={isLoading} + /> + + + Password + setPasswordFieldVal(e.target.value)} + disabled={isLoading} + /> + + + + {cta} + + +
+ + ); +}; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts b/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts new file mode 100644 index 0000000000..3c27fc84a3 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts @@ -0,0 +1,42 @@ +import { useState } from 'react' +import signup from '../../../signup' +import login from '../../../login' + +export function useUsernameAndPassword({ + onError, + onSuccess, + isLogin, +}: { + onError: (error: Error) => void + onSuccess: () => void + isLogin: boolean +}) { + const [usernameFieldVal, setUsernameFieldVal] = useState('') + const [passwordFieldVal, setPasswordFieldVal] = useState('') + + async function handleSubmit() { + try { + if (!isLogin) { + await signup({ + username: usernameFieldVal, + password: passwordFieldVal, + }) + } + await login(usernameFieldVal, passwordFieldVal) + + setUsernameFieldVal('') + setPasswordFieldVal('') + onSuccess() + } catch (err: unknown) { + onError(err as Error) + } + } + + return { + handleSubmit, + usernameFieldVal, + passwordFieldVal, + setUsernameFieldVal, + setPasswordFieldVal, + } +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/types.ts b/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/types.ts new file mode 100644 index 0000000000..296fe1a98f --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/types.ts @@ -0,0 +1,17 @@ +import { createTheme } from '@stitches/react' + +export enum State { + Login = 'login', + Signup = 'signup', +} + +export type CustomizationOptions = { + logo?: string + socialLayout?: 'horizontal' | 'vertical' + appearance?: Parameters[0] +} + +export type ErrorMessage = { + title: string + description?: string +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/helpers/user.ts b/waspc/examples/todo-typescript/node_modules/@wasp/auth/helpers/user.ts new file mode 100644 index 0000000000..6c4a5ec2b4 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/helpers/user.ts @@ -0,0 +1,14 @@ +import { setAuthToken } from '../../api' +import { invalidateAndRemoveQueries } from '@wasp/operations/resources' + +export async function initSession(token: string): Promise { + setAuthToken(token) + // We need to invalidate queries after login in order to get the correct user + // data in the React components (using `useAuth`). + // Redirects after login won't work properly without this. + + // TODO(filip): We are currently removing all the queries, but we should + // remove only non-public, user-dependent queries - public queries are + // expected not to change in respect to the currently logged in user. + await invalidateAndRemoveQueries() +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/login.ts b/waspc/examples/todo-typescript/node_modules/@wasp/auth/login.ts new file mode 100644 index 0000000000..dbd09feae7 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/login.ts @@ -0,0 +1,16 @@ +import api, { handleApiError } from "@wasp/api"; +import { initSession } from "./helpers/user"; + +export default async function login( + username: string, + password: string +): Promise { + try { + const args = { username, password }; + const response = await api.post("/auth/local/login", args); + + await initSession(response.data.token); + } catch (error) { + handleApiError(error); + } +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/logout.ts b/waspc/examples/todo-typescript/node_modules/@wasp/auth/logout.ts new file mode 100644 index 0000000000..248a371e13 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/logout.ts @@ -0,0 +1,9 @@ +import { removeLocalUserData } from "@wasp/api"; +import { invalidateAndRemoveQueries } from "@wasp/operations/resources"; + +export default async function logout(): Promise { + removeLocalUserData(); + // TODO(filip): We are currently invalidating and removing all the queries, but + // we should remove only the non-public, user-dependent ones. + await invalidateAndRemoveQueries(); +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/package.json b/waspc/examples/todo-typescript/node_modules/@wasp/auth/package.json new file mode 100644 index 0000000000..2c943a0ed6 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/package.json @@ -0,0 +1,3 @@ +{ + "name": "auth" +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/pages/createAuthRequiredPage.jsx b/waspc/examples/todo-typescript/node_modules/@wasp/auth/pages/createAuthRequiredPage.jsx new file mode 100644 index 0000000000..621ef393d9 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/pages/createAuthRequiredPage.jsx @@ -0,0 +1,30 @@ +import React from 'react' + +import { Redirect } from 'react-router-dom' +import useAuth from '../useAuth' + + +const createAuthRequiredPage = (Page) => { + return (props) => { + const { data: user, isError, isSuccess, isLoading } = useAuth() + + if (isSuccess) { + if (user) { + return ( + + ) + } else { + return + } + } else if (isLoading) { + return Loading... + } else if (isError) { + return An error ocurred. Please refresh the page. + } else { + return An unknown error ocurred. Please refresh the page. + } + } +} + +export default createAuthRequiredPage + diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/signup.ts b/waspc/examples/todo-typescript/node_modules/@wasp/auth/signup.ts new file mode 100644 index 0000000000..cfaf25fb6a --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/signup.ts @@ -0,0 +1,12 @@ +import api, { handleApiError } from "@wasp/api"; + +export default async function signup(userFields: { + username: string; + password: string; +}): Promise { + try { + await api.post("/auth/local/signup", userFields); + } catch (error) { + handleApiError(error); + } +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/stitches.config.js b/waspc/examples/todo-typescript/node_modules/@wasp/auth/stitches.config.js new file mode 100644 index 0000000000..741c205603 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/stitches.config.js @@ -0,0 +1,31 @@ +import { createStitches } from '@stitches/react' + +export const { + styled, + css +} = createStitches({ + theme: { + colors: { + waspYellow: '#ffcc00', + gray700: '#a1a5ab', + gray600: '#d1d5db', + gray500: 'gainsboro', + gray400: '#f0f0f0', + red: '#FED7D7', + green: '#C6F6D5', + + brand: '$waspYellow', + brandAccent: '#ffdb46', + errorBackground: '$red', + errorText: '#2D3748', + successBackground: '$green', + successText: '#2D3748', + + submitButtonText: 'black', + + }, + fontSizes: { + sm: '0.875rem' + } + } +}) diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/types.ts b/waspc/examples/todo-typescript/node_modules/@wasp/auth/types.ts new file mode 100644 index 0000000000..b706ce3f67 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/types.ts @@ -0,0 +1,2 @@ +// todo(filip): turn into a proper import/path +export { type SanitizedUser as User } from "@wasp/server/_types/"; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/useAuth.ts b/waspc/examples/todo-typescript/node_modules/@wasp/auth/useAuth.ts new file mode 100644 index 0000000000..c4204bfc8b --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/useAuth.ts @@ -0,0 +1,38 @@ +import { deserialize as superjsonDeserialize } from "superjson"; +import { useQuery } from "@wasp/rpc"; +import api, { handleApiError } from "@wasp/api"; +import { HttpMethod } from "@wasp/types"; +import type { User } from "./types"; +import { addMetadataToQuery } from "@wasp/rpc/queries"; + +export const getMe = createUserGetter(); + +export default function useAuth(queryFnArgs?: unknown, config?: any) { + return useQuery(getMe, queryFnArgs, config); +} + +function createUserGetter() { + const getMeRelativePath = "auth/me"; + const getMeRoute = { method: HttpMethod.Get, path: `/${getMeRelativePath}` }; + async function getMe(): Promise { + try { + const response = await api.get(getMeRoute.path); + + return superjsonDeserialize(response.data); + } catch (error) { + if (error.response?.status === 401) { + } else { + handleApiError(error); + } + return null; + } + } + + addMetadataToQuery(getMe, { + relativeQueryPath: getMeRelativePath, + queryRoute: getMeRoute, + entitiesUsed: ["User"], + }); + + return getMe; +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/core/AuthError.js b/waspc/examples/todo-typescript/node_modules/@wasp/core/AuthError.js new file mode 100644 index 0000000000..2d965c168e --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/core/AuthError.js @@ -0,0 +1,17 @@ +class AuthError extends Error { + constructor (message, data, ...params) { + super(message, ...params) + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, AuthError) + } + + this.name = this.constructor.name + + if (data) { + this.data = data + } + } +} + +export default AuthError diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/core/HttpError.js b/waspc/examples/todo-typescript/node_modules/@wasp/core/HttpError.js new file mode 100644 index 0000000000..8a2cb04db5 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/core/HttpError.js @@ -0,0 +1,22 @@ +class HttpError extends Error { + constructor (statusCode, message, data, ...params) { + super(message, ...params) + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, HttpError) + } + + this.name = this.constructor.name + + if (!(Number.isInteger(statusCode) && statusCode >= 400 && statusCode < 600)) { + throw new Error('statusCode has to be integer in range [400, 600).') + } + this.statusCode = statusCode + + if (data) { + this.data = data + } + } +} + +export default HttpError diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/core/auth.js b/waspc/examples/todo-typescript/node_modules/@wasp/core/auth.js new file mode 100644 index 0000000000..fad124965a --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/core/auth.js @@ -0,0 +1,130 @@ +import jwt from 'jsonwebtoken' +import SecurePassword from 'secure-password' +import util from 'util' +import { randomInt } from 'node:crypto' + +import prisma from '@server/dbClient.js' +import { handleRejection } from '../utils.js' +import HttpError from './HttpError.js' +import config from '../config.js' + +const jwtSign = util.promisify(jwt.sign) +const jwtVerify = util.promisify(jwt.verify) + +const JWT_SECRET = config.auth.jwtSecret + +export const sign = (id, options) => jwtSign({ id }, JWT_SECRET, options) +export const verify = (token) => jwtVerify(token, JWT_SECRET) + +const auth = handleRejection(async (req, res, next) => { + const authHeader = req.get('Authorization') + if (!authHeader) { + // NOTE(matija): for now we let tokenless requests through and make it operation's + // responsibility to verify whether the request is authenticated or not. In the future + // we will develop our own system at Wasp-level for that. + return next() + } + + if (authHeader.startsWith('Bearer ')) { + const token = authHeader.substring(7, authHeader.length) + req.user = await getUserFromToken(token) + } else { + throwInvalidCredentialsError() + } + + next() +}) + +export async function getUserFromToken(token) { + let userIdFromToken + try { + userIdFromToken = (await verify(token)).id + } catch (error) { + if (['TokenExpiredError', 'JsonWebTokenError', 'NotBeforeError'].includes(error.name)) { + throwInvalidCredentialsError() + } else { + throw error + } + } + + const user = await prisma.user.findUnique({ where: { id: userIdFromToken } }) + if (!user) { + throwInvalidCredentialsError() + } + + // TODO: This logic must match the type in types/index.ts (if we remove the + // password field from the object here, we must to do the same there). + // Ideally, these two things would live in the same place: + // https://github.com/wasp-lang/wasp/issues/965 + const { password, ...userView } = user + + return userView +} + +const SP = new SecurePassword() + +export const hashPassword = async (password) => { + const hashedPwdBuffer = await SP.hash(Buffer.from(password)) + return hashedPwdBuffer.toString("base64") +} + +export const verifyPassword = async (hashedPassword, password) => { + const result = await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64")) + if (result !== SecurePassword.VALID) { + throw new Error('Invalid password.') + } +} + +// Generates an unused username that looks similar to "quick-purple-sheep-91231". +// It generates several options and ensures it picks one that is not currently in use. +export function generateAvailableDictionaryUsername() { + const adjectives = ['fuzzy', 'tall', 'short', 'nice', 'happy', 'quick', 'slow', 'good', 'new', 'old', 'first', 'last', 'old', 'young'] + const colors = ['red', 'green', 'blue', 'white', 'black', 'brown', 'purple', 'orange', 'yellow'] + const nouns = ['wasp', 'cat', 'dog', 'lion', 'rabbit', 'duck', 'pig', 'bee', 'goat', 'crab', 'fish', 'chicken', 'horse', 'llama', 'camel', 'sheep'] + + const potentialUsernames = [] + for (let i = 0; i < 10; i++) { + const potentialUsername = `${adjectives[randomInt(adjectives.length)]}-${colors[randomInt(colors.length)]}-${nouns[randomInt(nouns.length)]}-${randomInt(100_000)}` + potentialUsernames.push(potentialUsername) + } + + return findAvailableUsername(potentialUsernames) +} + +// Generates an unused username based on an array of username segments and a separator. +// It generates several options and ensures it picks one that is not currently in use. +export function generateAvailableUsername(usernameSegments, config) { + const separator = config?.separator || '-' + const baseUsername = usernameSegments.join(separator) + + const potentialUsernames = [] + for (let i = 0; i < 10; i++) { + const potentialUsername = `${baseUsername}${separator}${randomInt(100_000)}` + potentialUsernames.push(potentialUsername) + } + + return findAvailableUsername(potentialUsernames) +} + +// Checks the database for an unused username from an array provided and returns first. +async function findAvailableUsername(potentialUsernames) { + const users = await prisma.user.findMany({ + where: { + username: { in: potentialUsernames }, + } + }) + const takenUsernames = users.map(user => user.username) + const availableUsernames = potentialUsernames.filter(username => !takenUsernames.includes(username)) + + if (availableUsernames.length === 0) { + throw new Error('Unable to generate a unique username. Please contact Wasp.') + } + + return availableUsernames[0] +} + +export function throwInvalidCredentialsError(message) { + throw new HttpError(401, 'Invalid credentials', { message }) +} + +export default auth diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/core/auth/prismaMiddleware.js b/waspc/examples/todo-typescript/node_modules/@wasp/core/auth/prismaMiddleware.js new file mode 100644 index 0000000000..53e60c5e1a --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/core/auth/prismaMiddleware.js @@ -0,0 +1,84 @@ +import { hashPassword } from '../auth.js' +import AuthError from '../AuthError.js' + +const USERNAME_FIELD = 'username' +const PASSWORD_FIELD = 'password' + +// Allows flexible validation of a user entity. +// Users can skip default validations by passing _waspSkipDefaultValidations = true +// Users can also add custom validations by passing an array of _waspCustomValidations +// with the same format as our default validations. +// Throws an AuthError on the first validation that fails. +const registerUserEntityValidation = (prismaClient) => { + prismaClient.$use(async (params, next) => { + if (params.model === 'User') { + if (['create', 'update', 'updateMany'].includes(params.action)) { + validateUser(params.args.data, params.args, params.action) + } else if (params.action === 'upsert') { + validateUser(params.args.create, params.args, 'create') + validateUser(params.args.update, params.args, 'update') + } + + // Remove from downstream Prisma processing to avoid "Unknown arg" error + delete params.args._waspSkipDefaultValidations + delete params.args._waspCustomValidations + } + + return next(params) + }) +} + +// Make sure password is always hashed before storing to the database. +const registerPasswordHashing = (prismaClient) => { + prismaClient.$use(async (params, next) => { + if (params.model === 'User') { + if (['create', 'update', 'updateMany'].includes(params.action)) { + if (params.args.data.hasOwnProperty(PASSWORD_FIELD)) { + params.args.data[PASSWORD_FIELD] = await hashPassword(params.args.data[PASSWORD_FIELD]) + } + } else if (params.action === 'upsert') { + if (params.args.create.hasOwnProperty(PASSWORD_FIELD)) { + params.args.create[PASSWORD_FIELD] = + await hashPassword(params.args.create[PASSWORD_FIELD]) + } + if (params.args.update.hasOwnProperty(PASSWORD_FIELD)) { + params.args.update[PASSWORD_FIELD] = + await hashPassword(params.args.update[PASSWORD_FIELD]) + } + } + } + + return next(params) + }) +} + +export const registerAuthMiddleware = (prismaClient) => { + // NOTE: registerUserEntityValidation must come before registerPasswordHashing. + registerUserEntityValidation(prismaClient) + registerPasswordHashing(prismaClient) +} + +const userValidations = [] +userValidations.push({ validates: USERNAME_FIELD, message: 'username must be present', validator: username => !!username }) +userValidations.push({ validates: PASSWORD_FIELD, message: 'password must be present', validator: password => !!password }) +userValidations.push({ validates: PASSWORD_FIELD, message: 'password must be at least 8 characters', validator: password => password.length >= 8 }) +userValidations.push({ validates: PASSWORD_FIELD, message: 'password must contain a number', validator: password => /\d/.test(password) }) + +const validateUser = (user, args, action) => { + user = user || {} + + const validations = [ + ...(args._waspSkipDefaultValidations ? [] : userValidations), + ...(args._waspCustomValidations || []) + ] + + // On 'create' validations run always, otherwise (on updates) + // they run only when the field they are validating is present. + for (const v of validations) { + if (action === 'create' || user.hasOwnProperty(v.validates)) { + if (!v.validator(user[v.validates])) { + throw new AuthError(v.message) + } + } + } +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/core/auth/validators.ts b/waspc/examples/todo-typescript/node_modules/@wasp/core/auth/validators.ts new file mode 100644 index 0000000000..bfefc5fb78 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/core/auth/validators.ts @@ -0,0 +1,5 @@ +const validEmailRegex = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/ + +export function isValidEmail(input: string): boolean { + return input.match(validEmailRegex) !== null +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/core/config.js b/waspc/examples/todo-typescript/node_modules/@wasp/core/config.js new file mode 100644 index 0000000000..2c2cc63358 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/core/config.js @@ -0,0 +1,11 @@ +import { stripTrailingSlash } from "@wasp/universal/url"; + +const apiUrl = + stripTrailingSlash(import.meta.env.REACT_APP_API_URL) || + "http://localhost:3001"; + +const config = { + apiUrl, +}; + +export default config; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/core/package.json b/waspc/examples/todo-typescript/node_modules/@wasp/core/package.json new file mode 100644 index 0000000000..bebae82718 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/core/package.json @@ -0,0 +1,20 @@ +{ + "name": "core", + "type": "module", + "version": "1.0.0", + "description": "", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "types": "tsc --declaration --emitDeclarationOnly --stripInternal --declarationDir dist" + }, + "exports": { + "./HttpError.js": "./HttpError.js", + "./config": "./config.js", + "./stitches.config": "./stitches.config.js", + "./storage": "./storage.ts" + }, + "types": "./index.d.ts", + "author": "", + "license": "ISC", + "include": ["src/**/*"] +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/core/stitches.config.js b/waspc/examples/todo-typescript/node_modules/@wasp/core/stitches.config.js new file mode 100644 index 0000000000..741c205603 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/core/stitches.config.js @@ -0,0 +1,31 @@ +import { createStitches } from '@stitches/react' + +export const { + styled, + css +} = createStitches({ + theme: { + colors: { + waspYellow: '#ffcc00', + gray700: '#a1a5ab', + gray600: '#d1d5db', + gray500: 'gainsboro', + gray400: '#f0f0f0', + red: '#FED7D7', + green: '#C6F6D5', + + brand: '$waspYellow', + brandAccent: '#ffdb46', + errorBackground: '$red', + errorText: '#2D3748', + successBackground: '$green', + successText: '#2D3748', + + submitButtonText: 'black', + + }, + fontSizes: { + sm: '0.875rem' + } + } +}) diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/core/storage.ts b/waspc/examples/todo-typescript/node_modules/@wasp/core/storage.ts new file mode 100644 index 0000000000..0321acea8b --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/core/storage.ts @@ -0,0 +1,50 @@ +export type DataStore = { + getPrefixedKey(key: string): string + set(key: string, value: unknown): void + get(key: string): unknown + remove(key: string): void + clear(): void +} + +function createLocalStorageDataStore(prefix: string): DataStore { + function getPrefixedKey(key: string): string { + return `${prefix}:${key}` + } + + return { + getPrefixedKey, + set(key, value) { + ensureLocalStorageIsAvailable() + localStorage.setItem(getPrefixedKey(key), JSON.stringify(value)) + }, + get(key) { + ensureLocalStorageIsAvailable() + const value = localStorage.getItem(getPrefixedKey(key)) + try { + return value ? JSON.parse(value) : undefined + } catch (e: any) { + return undefined + } + }, + remove(key) { + ensureLocalStorageIsAvailable() + localStorage.removeItem(getPrefixedKey(key)) + }, + clear() { + ensureLocalStorageIsAvailable() + Object.keys(localStorage).forEach((key) => { + if (key.startsWith(prefix)) { + localStorage.removeItem(key) + } + }) + }, + } +} + +export const storage = createLocalStorageDataStore('wasp') + +function ensureLocalStorageIsAvailable(): void { + if (!window.localStorage) { + throw new Error('Local storage is not available.') + } +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/entities/index.ts b/waspc/examples/todo-typescript/node_modules/@wasp/entities/index.ts new file mode 100644 index 0000000000..03a711ea4e --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/entities/index.ts @@ -0,0 +1,19 @@ +import { + type User, + type Task, +} from "@prisma/client" + +export { + type User, + type Task, +} from "@prisma/client" + +export type Entity = + | User + | Task + | never + +export type EntityName = + | "User" + | "Task" + | never diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/actions.ts b/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/actions.ts new file mode 100644 index 0000000000..0245af9e77 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/actions.ts @@ -0,0 +1,36 @@ +import HttpError from "@wasp/core/HttpError.js"; +import { Context, Task } from "./serverTypes"; + +type CreateArgs = Pick; + +export async function createTask( + { description }: CreateArgs, + context: Context +) { + if (!context.user) { + throw new HttpError(401); + } + + return context.entities.Task.create({ + data: { + description, + user: { connect: { id: context.user.id } }, + }, + }); +} + +type UpdateArgs = Pick; + +export async function updateTask({ id, isDone }: UpdateArgs, context: Context) { + if (!context.user) { + throw new HttpError(401); + } + + return context.entities.Task.updateMany({ + where: { + id, + user: { id: context.user.id }, + }, + data: { isDone }, + }); +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/queries.ts b/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/queries.ts new file mode 100644 index 0000000000..e61bd737f9 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/queries.ts @@ -0,0 +1,14 @@ +import HttpError from "@wasp/core/HttpError.js"; +import { Context, Task } from "./serverTypes"; + +export async function getTasks( + args: unknown, + context: Context +): Promise { + if (!context.user) { + throw new HttpError(401); + } + return context.entities.Task.findMany({ + where: { user: { id: context.user.id } }, + }); +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/serverTypes.ts b/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/serverTypes.ts new file mode 100644 index 0000000000..3f1b705b66 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/serverTypes.ts @@ -0,0 +1,11 @@ +import { User, Prisma } from '@prisma/client'; + +export { Task } from '@prisma/client'; + +export type Context = { + user: User; + entities: { + Task: Prisma.TaskDelegate<{}>; + User: Prisma.UserDelegate<{}>; + }; +}; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/operations/index.ts b/waspc/examples/todo-typescript/node_modules/@wasp/operations/index.ts new file mode 100644 index 0000000000..2d4494b050 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/operations/index.ts @@ -0,0 +1,22 @@ +import api, { handleApiError } from '../api' +import { HttpMethod } from '../types' +import { + serialize as superjsonSerialize, + deserialize as superjsonDeserialize, + } from 'superjson' + +export type OperationRoute = { method: HttpMethod, path: string } + +export async function callOperation(operationRoute: OperationRoute & { method: HttpMethod.Post }, args: any) { + try { + const superjsonArgs = superjsonSerialize(args) + const response = await api.post(operationRoute.path, superjsonArgs) + return superjsonDeserialize(response.data) + } catch (error) { + handleApiError(error) + } +} + +export function makeOperationRoute(relativeOperationRoute: string): OperationRoute { + return { method: HttpMethod.Post, path: `/${relativeOperationRoute}` } +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/operations/package.json b/waspc/examples/todo-typescript/node_modules/@wasp/operations/package.json new file mode 100644 index 0000000000..2e944ae8ae --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/operations/package.json @@ -0,0 +1,7 @@ +{ + "name": "operations", + "exports": { + ".": "./index.ts", + "./resources": "./resources.js" + } +} \ No newline at end of file diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/operations/resources.js b/waspc/examples/todo-typescript/node_modules/@wasp/operations/resources.js new file mode 100644 index 0000000000..cb328cb940 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/operations/resources.js @@ -0,0 +1,85 @@ +import { queryClientInitialized } from "@wasp/rpc/queryClient"; +import { makeUpdateHandlersMap } from "./updateHandlersMap"; +import { hashQueryKey } from "@tanstack/react-query"; + +// Map where key is resource name and value is Set +// containing query ids of all the queries that use +// that resource. +const resourceToQueryCacheKeys = new Map(); + +const updateHandlers = makeUpdateHandlersMap(hashQueryKey); +/** + * Remembers that specified query is using specified resources. + * If called multiple times for same query, resources are added, not reset. + * @param {string[]} queryCacheKey - Unique key under used to identify query in the cache. + * @param {string[]} resources - Names of resources that query is using. + */ +export function addResourcesUsedByQuery(queryCacheKey, resources) { + for (const resource of resources) { + let cacheKeys = resourceToQueryCacheKeys.get(resource); + if (!cacheKeys) { + cacheKeys = new Set(); + resourceToQueryCacheKeys.set(resource, cacheKeys); + } + cacheKeys.add(queryCacheKey); + } +} + +export function registerActionInProgress(optimisticUpdateTuples) { + optimisticUpdateTuples.forEach(({ queryKey, updateQuery }) => + updateHandlers.add(queryKey, updateQuery) + ); +} + +export async function registerActionDone(resources, optimisticUpdateTuples) { + optimisticUpdateTuples.forEach(({ queryKey }) => + updateHandlers.remove(queryKey) + ); + await invalidateQueriesUsing(resources); +} + +export function getActiveOptimisticUpdates(queryKey) { + return updateHandlers.getUpdateHandlers(queryKey); +} + +export async function invalidateAndRemoveQueries() { + console.log(queryClientInitialized); + const queryClient = await queryClientInitialized; + // If we don't reset the queries before removing them, Wasp will stay on + // the same page. The user would have to manually refresh the page to "finish" + // logging out. + // When a query is removed, the `Observer` is removed as well, and the components + // that are using the query are not re-rendered. This is why we need to reset + // the queries, so that the `Observer` is re-created and the components are re-rendered. + // For more details: https://github.com/wasp-lang/wasp/pull/1014/files#r1111862125 + queryClient.resetQueries(); + // If we don't remove the queries after invalidating them, the old query data + // remains in the cache, casuing a potential privacy issue. + queryClient.removeQueries(); +} + +/** + * Invalidates all queries that are using specified resources. + * @param {string[]} resources - Names of resources. + */ +async function invalidateQueriesUsing(resources) { + console.log(queryClientInitialized); + const queryClient = await queryClientInitialized; + + const queryCacheKeysToInvalidate = getQueriesUsingResources(resources); + queryCacheKeysToInvalidate.forEach((queryCacheKey) => + queryClient.invalidateQueries(queryCacheKey) + ); +} + +/** + * @param {string} resource - Resource name. + * @returns {string[]} Array of "query cache keys" of queries that use specified resource. + */ +function getQueriesUsingResource(resource) { + return Array.from(resourceToQueryCacheKeys.get(resource) || []); +} + +function getQueriesUsingResources(resources) { + return Array.from(new Set(resources.flatMap(getQueriesUsingResource))); +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/operations/updateHandlersMap.js b/waspc/examples/todo-typescript/node_modules/@wasp/operations/updateHandlersMap.js new file mode 100644 index 0000000000..8c43c0b1ba --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/operations/updateHandlersMap.js @@ -0,0 +1,37 @@ +export function makeUpdateHandlersMap(calculateHash) { + const updateHandlers = new Map() + + function getHandlerTuples(queryKeyHash) { + return updateHandlers.get(queryKeyHash) || []; + } + + function add(queryKey, updateQuery) { + const queryKeyHash = calculateHash(queryKey) + const handlers = getHandlerTuples(queryKeyHash); + updateHandlers.set(queryKeyHash, [...handlers, { queryKey, updateQuery }]) + } + + function getUpdateHandlers(queryKey) { + const queryKeyHash = calculateHash(queryKey) + return getHandlerTuples(queryKeyHash).map(({ updateQuery }) => updateQuery) + } + + function remove(queryKeyToRemove) { + const queryKeyHash = calculateHash(queryKeyToRemove) + const filteredHandlers = getHandlerTuples(queryKeyHash).filter( + ({ queryKey }) => queryKey !== queryKeyToRemove + ) + + if (filteredHandlers.length > 0) { + updateHandlers.set(queryKeyHash, filteredHandlers) + } else { + updateHandlers.delete(queryKeyHash) + } + } + + return { + add, + remove, + getUpdateHandlers, + } +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/core.d.ts b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/core.d.ts new file mode 100644 index 0000000000..c5d5d40d3f --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/core.d.ts @@ -0,0 +1,13 @@ +import { type Action } from '.' +import type { Expand, _Awaited, _ReturnType } from '@wasp/universal/types' + +export function createAction( + actionRoute: string, + entitiesUsed: unknown[] +): ActionFor + +type ActionFor = Expand< + Action[0], _Awaited<_ReturnType>> +> + +type GenericBackendAction = (args: never, context: any) => unknown diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/core.js b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/core.js new file mode 100644 index 0000000000..19c87efe38 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/core.js @@ -0,0 +1,40 @@ +import { callOperation, makeOperationRoute } from "@wasp/operations"; +import { + registerActionInProgress, + registerActionDone, +} from "@wasp/operations/resources"; + +// todo - turn helpers and core into the same thing + +export function createAction(relativeActionRoute, entitiesUsed) { + const actionRoute = makeOperationRoute(relativeActionRoute); + + async function internalAction(args, specificOptimisticUpdateDefinitions) { + registerActionInProgress(specificOptimisticUpdateDefinitions); + try { + // The `return await` is not redundant here. If we removed the await, the + // `finally` block would execute before the action finishes, prematurely + // registering the action as done. + return await callOperation(actionRoute, args); + } finally { + await registerActionDone( + entitiesUsed, + specificOptimisticUpdateDefinitions + ); + } + } + + // We expose (and document) a restricted version of the API for our users, + // while also attaching the full "internal" API to the exposed action. By + // doing this, we can easily use the internal API of an action a users passes + // into our system (e.g., through the `useAction` hook) without needing a + // lookup table. + // + // While it does technically allow our users to access the interal API, it + // shouldn't be a problem in practice. Still, if it turns out to be a problem, + // we can always hide it using a Symbol. + const action = (args) => internalAction(args, []); + action.internal = internalAction; + + return action; +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/index.ts b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/index.ts new file mode 100644 index 0000000000..c570cfe7b6 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/index.ts @@ -0,0 +1,10 @@ +import { createAction } from "./core"; +import { CreateTask, UpdateTask } from "@wasp/server/actions"; + +export const updateTask = createAction("operations/update-task", [ + "Task", +]); + +export const createTask = createAction("operations/create-task", [ + "Task", +]); diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/index.ts b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/index.ts new file mode 100644 index 0000000000..8a743e3456 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/index.ts @@ -0,0 +1,338 @@ +import { + QueryClient, + QueryKey, + useMutation, + UseMutationOptions, + useQueryClient, + useQuery as rqUseQuery, + UseQueryResult, +} from "@tanstack/react-query"; +export { configureQueryClient } from "./queryClient"; + +export type Query = { + (queryCacheKey: string[], args: Input): Promise; +}; + +export function useQuery( + queryFn: Query, + queryFnArgs?: Input, + options?: any +): UseQueryResult; + +export function useQuery(queryFn, queryFnArgs, options) { + 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 queryKey = + queryFnArgs !== undefined + ? [...queryFn.queryCacheKey, queryFnArgs] + : queryFn.queryCacheKey; + return rqUseQuery({ + queryKey, + queryFn: () => queryFn(queryKey, queryFnArgs), + ...options, + }); +} + +// todo - turn helpers and core into the same thing + +export type Action = [Input] extends [never] + ? (args?: unknown) => Promise + : (args: Input) => Promise; + +/** + * An options object passed into the `useAction` hook and used to enhance the + * action with extra options. + * + */ +export type ActionOptions = { + optimisticUpdates: OptimisticUpdateDefinition[]; +}; + +/** + * A documented (public) way to define optimistic updates. + */ +export type OptimisticUpdateDefinition = { + getQuerySpecifier: GetQuerySpecifier; + updateQuery: UpdateQuery; +}; + +/** + * A function that takes an item and returns a Wasp Query specifier. + */ +export type GetQuerySpecifier = ( + item: ActionInput +) => QuerySpecifier; + +/** + * A function that takes an item and the previous state of the cache, and returns + * the desired (new) state of the cache. + */ +export type UpdateQuery = ( + item: ActionInput, + oldData: CachedData | undefined +) => CachedData; + +/** + * A public query specifier used for addressing Wasp queries. See our docs for details: + * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + */ +export type QuerySpecifier = [Query, ...any[]]; + +/** + * A hook for adding extra behavior to a Wasp Action (e.g., optimistic updates). + * + * @param actionFn The Wasp Action you wish to enhance/decorate. + * @param actionOptions An options object for enhancing/decorating the given Action. + * @returns A decorated Action with added behavior but an unchanged API. + */ +export function useAction( + actionFn: Action, + actionOptions?: ActionOptions +): typeof actionFn { + const queryClient = useQueryClient(); + + let mutationFn = actionFn; + let options = {}; + if (actionOptions?.optimisticUpdates) { + const optimisticUpdatesDefinitions = actionOptions.optimisticUpdates.map( + translateToInternalDefinition + ); + mutationFn = makeOptimisticUpdateMutationFn( + actionFn, + optimisticUpdatesDefinitions + ); + options = makeRqOptimisticUpdateOptions( + queryClient, + optimisticUpdatesDefinitions + ); + } + + // NOTE: We decided to hide React Query's extra mutation features (e.g., + // isLoading, onSuccess and onError callbacks, synchronous mutate) and only + // expose a simple async function whose API matches the original Action. + // We did this to avoid cluttering the API with stuff we're not sure we need + // yet (e.g., isLoading), to postpone the action vs mutation dilemma, and to + // clearly separate our opinionated API from React Query's lower-level + // advanced API (which users can also use) + const mutation = useMutation(mutationFn, options); + return (args) => mutation.mutateAsync(args); +} + +/** + * An internal (undocumented, private, desugared) way of defining optimistic updates. + */ +type InternalOptimisticUpdateDefinition = { + getQueryKey: (item: ActionInput) => QueryKey; + updateQuery: UpdateQuery; +}; + +/** + * An UpdateQuery function "instantiated" with a specific item. It only takes + * the current state of the cache and returns the desired (new) state of the + * cache. + */ +type SpecificUpdateQuery = (oldData: CachedData) => CachedData; + +/** + * A specific, "instantiated" optimistic update definition which contains a + * fully-constructed query key and a specific update function. + */ +type SpecificOptimisticUpdateDefinition = { + queryKey: QueryKey; + updateQuery: SpecificUpdateQuery; +}; + +type InternalAction = Action & { + internal( + item: Input, + optimisticUpdateDefinitions: SpecificOptimisticUpdateDefinition[] + ): Promise; +}; + +/** + * Translates/Desugars a public optimistic update definition object into a + * definition object our system uses internally. + * + * @param publicOptimisticUpdateDefinition An optimistic update definition + * object that's a part of the public API: + * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * @returns An internally-used optimistic update definition object. + */ +function translateToInternalDefinition( + publicOptimisticUpdateDefinition: OptimisticUpdateDefinition +): InternalOptimisticUpdateDefinition { + const { getQuerySpecifier, updateQuery } = publicOptimisticUpdateDefinition; + + const definitionErrors = []; + if (typeof getQuerySpecifier !== "function") { + definitionErrors.push("`getQuerySpecifier` is not a function."); + } + if (typeof updateQuery !== "function") { + definitionErrors.push("`updateQuery` is not a function."); + } + if (definitionErrors.length) { + throw new TypeError( + `Invalid optimistic update definition: ${definitionErrors.join(", ")}.` + ); + } + + return { + getQueryKey: (item) => getRqQueryKeyFromSpecifier(getQuerySpecifier(item)), + updateQuery, + }; +} + +/** + * Creates a function that performs an action while telling it about the + * optimistic updates it caused. + * + * @param actionFn The Wasp Action. + * @param optimisticUpdateDefinitions The optimisitc updates the action causes. + * @returns An decorated action which performs optimistic updates. + */ +function makeOptimisticUpdateMutationFn( + actionFn: Action, + optimisticUpdateDefinitions: InternalOptimisticUpdateDefinition< + Input, + CachedData + >[] +): typeof actionFn { + return function performActionWithOptimisticUpdates(item) { + const specificOptimisticUpdateDefinitions = optimisticUpdateDefinitions.map( + (generalDefinition) => + getOptimisticUpdateDefinitionForSpecificItem(generalDefinition, item) + ); + return (actionFn as InternalAction).internal( + item, + specificOptimisticUpdateDefinitions + ); + }; +} + +/** + * Given a ReactQuery query client and our internal definition of optimistic + * updates, this function constructs an object describing those same optimistic + * updates in a format we can pass into React Query's useMutation hook. In other + * words, it translates our optimistic updates definition into React Query's + * optimistic updates definition. Check their docs for details: + * https://tanstack.com/query/v4/docs/guides/optimistic-updates?from=reactQueryV3&original=https://react-query-v3.tanstack.com/guides/optimistic-updates + * + * @param queryClient The QueryClient instance used by React Query. + * @param optimisticUpdateDefinitions A list containing internal optimistic + * updates definition objects (i.e., a list where each object carries the + * instructions for performing particular optimistic update). + * @returns An object containing 'onMutate' and 'onError' functions + * corresponding to the given optimistic update definitions (check the docs + * linked above for details). + */ +function makeRqOptimisticUpdateOptions( + queryClient: QueryClient, + optimisticUpdateDefinitions: InternalOptimisticUpdateDefinition< + ActionInput, + CachedData + >[] +): Pick { + async function onMutate(item) { + const specificOptimisticUpdateDefinitions = optimisticUpdateDefinitions.map( + (generalDefinition) => + getOptimisticUpdateDefinitionForSpecificItem(generalDefinition, item) + ); + + // Cancel any outgoing refetches (so they don't overwrite our optimistic update). + // Theoretically, we can be a bit faster. Instead of awaiting the + // cancellation of all queries, we could cancel and update them in parallel. + // However, awaiting cancellation hasn't yet proven to be a performance bottleneck. + await Promise.all( + specificOptimisticUpdateDefinitions.map(({ queryKey }) => + queryClient.cancelQueries(queryKey) + ) + ); + + // We're using a Map to correctly serialize query keys that contain objects. + const previousData = new Map(); + specificOptimisticUpdateDefinitions.forEach(({ queryKey, updateQuery }) => { + // Snapshot the currently cached value. + const previousDataForQuery: CachedData = + queryClient.getQueryData(queryKey); + + // Attempt to optimistically update the cache using the new value. + try { + queryClient.setQueryData(queryKey, updateQuery); + } catch (e) { + console.error( + "The `updateQuery` function threw an exception, skipping optimistic update:" + ); + console.error(e); + } + + // Remember the snapshotted value to restore in case of an error. + previousData.set(queryKey, previousDataForQuery); + }); + + return { previousData }; + } + + function onError(_err, _item, context) { + // All we do in case of an error is roll back all optimistic updates. We ensure + // not to do anything else because React Query rethrows the error. This allows + // the programmer to handle the error as they usually would (i.e., we want the + // error handling to work as it would if the programmer wasn't using optimistic + // updates). + context.previousData.forEach(async (data, queryKey) => { + await queryClient.cancelQueries(queryKey); + queryClient.setQueryData(queryKey, data); + }); + } + + return { + onMutate, + onError, + }; +} + +/** + * Constructs the definition for optimistically updating a specific item. It + * uses a closure over the updated item to construct an item-specific query key + * (e.g., useful when the query key depends on an ID). + * + * @param optimisticUpdateDefinition The general, "uninstantiated" optimistic + * update definition with a function for constructing the query key. + * @param item The item triggering the Action/optimistic update (i.e., the + * argument passed to the Action). + * @returns A specific optimistic update definition which corresponds to the + * provided definition and closes over the provided item. + */ +function getOptimisticUpdateDefinitionForSpecificItem( + optimisticUpdateDefinition: InternalOptimisticUpdateDefinition< + ActionInput, + CachedData + >, + item: ActionInput +): SpecificOptimisticUpdateDefinition { + const { getQueryKey, updateQuery } = optimisticUpdateDefinition; + return { + queryKey: getQueryKey(item), + updateQuery: (old) => updateQuery(item, old), + }; +} + +/** + * Translates a Wasp query specifier to a query cache key used by React Query. + * + * @param querySpecifier A query specifier that's a part of the public API: + * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * @returns A cache key React Query internally uses for addressing queries. + */ +function getRqQueryKeyFromSpecifier( + querySpecifier: QuerySpecifier +): QueryKey { + const [queryFn, ...otherKeys] = querySpecifier; + return [...(queryFn as any).queryCacheKey, ...otherKeys]; +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/package.json b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/package.json new file mode 100644 index 0000000000..841a5f80a5 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/package.json @@ -0,0 +1,3 @@ +{ + "name": "rpc" +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/core.d.ts b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/core.d.ts new file mode 100644 index 0000000000..a358ac02b7 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/core.d.ts @@ -0,0 +1,23 @@ +import { type Query } from '..' +import { Route } from '@wasp/types'; +import type { Expand, _Awaited, _ReturnType } from '@wasp/universal/types' + +export function createQuery( + queryRoute: string, + entitiesUsed: any[] +): QueryFor + +export function addMetadataToQuery( + query: (...args: any[]) => Promise, + metadata: { + relativeQueryPath: string; + queryRoute: Route; + entitiesUsed: string[]; + }, +): void + +type QueryFor = Expand< + Query[0], _Awaited<_ReturnType>> +> + +type GenericBackendQuery = (args: never, context: any) => unknown diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/core.js b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/core.js new file mode 100644 index 0000000000..00974ffa08 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/core.js @@ -0,0 +1,30 @@ +import { callOperation, makeOperationRoute } from "@wasp/operations"; +import { + addResourcesUsedByQuery, + getActiveOptimisticUpdates, +} from "@wasp/operations/resources"; + +export function createQuery(relativeQueryPath, entitiesUsed) { + const queryRoute = makeOperationRoute(relativeQueryPath); + + async function query(queryKey, queryArgs) { + const serverResult = await callOperation(queryRoute, queryArgs); + return getActiveOptimisticUpdates(queryKey).reduce( + (result, update) => update(result), + serverResult + ); + } + + addMetadataToQuery(query, { relativeQueryPath, queryRoute, entitiesUsed }); + + return query; +} + +export function addMetadataToQuery( + query, + { relativeQueryPath, queryRoute, entitiesUsed } +) { + query.queryCacheKey = [relativeQueryPath]; + query.route = queryRoute; + addResourcesUsedByQuery(query.queryCacheKey, entitiesUsed); +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/index.ts b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/index.ts new file mode 100644 index 0000000000..ef0bf5ced4 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/index.ts @@ -0,0 +1,6 @@ +import { createQuery } from "./core"; +import { GetTasks } from "@wasp/server/queries"; + +export const getTasks = createQuery("operations/get-tasks", ["Task"]); + +export { addMetadataToQuery } from "./core"; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queryClient.ts b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queryClient.ts new file mode 100644 index 0000000000..2322109b25 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queryClient.ts @@ -0,0 +1,26 @@ +import { QueryClient } from '@tanstack/react-query' + + +type QueryClientConfig = object + +const defaultQueryClientConfig = {} + +let queryClientConfig: QueryClientConfig, resolveQueryClientInitialized: (...args: any[]) => any, isQueryClientInitialized: boolean + +export const queryClientInitialized = new Promise(resolve => { + resolveQueryClientInitialized = resolve +}); + +export function configureQueryClient(config: QueryClientConfig): void { + if (isQueryClientInitialized) { + throw new Error("Attempted to configure the QueryClient after initialization") + } + + queryClientConfig = config +} + +export function initializeQueryClient(): void { + const queryClient = new QueryClient(queryClientConfig ?? defaultQueryClientConfig) + isQueryClientInitialized = true; + resolveQueryClientInitialized(queryClient) +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/_types/index.ts b/waspc/examples/todo-typescript/node_modules/@wasp/server/_types/index.ts new file mode 100644 index 0000000000..9579a5b606 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/server/_types/index.ts @@ -0,0 +1,100 @@ +import { type Request, type Response } from "express"; +import { + type ParamsDictionary as ExpressParams, + type Query as ExpressQuery, +} from "express-serve-static-core"; +import { type Expand } from "../../universal/types.js"; +import prisma from "../dbClient.js"; +import { type User } from "../../entities"; +import { type _Entity } from "./taggedEntities"; +import { type Payload } from "./serialization"; + +export * from "./taggedEntities"; +export * from "./serialization"; + +export type Query< + Entities extends _Entity[], + Input extends Payload, + Output extends Payload +> = Operation; + +export type Action< + Entities extends _Entity[], + Input extends Payload, + Output extends Payload +> = Operation; + +export type AuthenticatedQuery< + Entities extends _Entity[], + Input extends Payload, + Output extends Payload +> = AuthenticatedOperation; + +export type AuthenticatedAction< + Entities extends _Entity[], + Input extends Payload, + Output extends Payload +> = AuthenticatedOperation; + +type AuthenticatedOperation< + Entities extends _Entity[], + Input extends Payload, + Output extends Payload +> = ( + args: Input, + context: ContextWithUser +) => Output | Promise; + +export type AuthenticatedApi< + Entities extends _Entity[], + Params extends ExpressParams, + ResBody, + ReqBody, + ReqQuery extends ExpressQuery, + Locals extends Record +> = ( + req: Request, + res: Response, + context: ContextWithUser +) => void; + +type Operation = ( + args: Input, + context: Context +) => Output | Promise; + +export type Api< + Entities extends _Entity[], + Params extends ExpressParams, + ResBody, + ReqBody, + ReqQuery extends ExpressQuery, + Locals extends Record +> = ( + req: Request, + res: Response, + context: Context +) => void; + +type EntityMap = { + [EntityName in Entities[number]["_entityName"]]: PrismaDelegate[EntityName]; +}; + +type PrismaDelegate = { + User: typeof prisma.user; + Task: typeof prisma.task; +}; + +type Context = Expand<{ + entities: Expand>; +}>; + +type ContextWithUser = Expand< + Context & { user?: SanitizedUser } +>; + +// TODO: This type must match the logic in core/auth.js (if we remove the +// password field from the object there, we must do the same here). Ideally, +// these two things would live in the same place: +// https://github.com/wasp-lang/wasp/issues/965 +export type SanitizedUser = Omit; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/_types/serialization.ts b/waspc/examples/todo-typescript/node_modules/@wasp/server/_types/serialization.ts new file mode 100644 index 0000000000..5a55cadb2b --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/server/_types/serialization.ts @@ -0,0 +1,35 @@ +export type Payload = void | SuperJSONValue; + +// The part below was copied from SuperJSON and slightly modified: +// https://github.com/blitz-js/superjson/blob/ae7dbcefe5d3ece5b04be0c6afe6b40f3a44a22a/src/types.ts +// +// We couldn't use SuperJSON's types directly because: +// 1. They aren't exported publicly. +// 2. They have a werid quirk that turns `SuperJSONValue` into `any`. +// See why here: +// https://github.com/blitz-js/superjson/pull/36#issuecomment-669239876 +// +// We changed the code as little as possible to make future comparisons easier. + +type PrimitiveJSONValue = string | number | boolean | undefined | null; + +type JSONValue = PrimitiveJSONValue | JSONArray | JSONObject; + +interface JSONArray extends Array { +} + +interface JSONObject { + [key: string]: JSONValue; +} + +type SerializableJSONValue = Symbol | Set | Map | undefined | bigint | Date | RegExp; + +// Here's where we excluded `ClassInstance` (which was `any`) from the union. +type SuperJSONValue = JSONValue | SerializableJSONValue | SuperJSONArray | SuperJSONObject; + +interface SuperJSONArray extends Array { +} + +interface SuperJSONObject { + [key: string]: SuperJSONValue; +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/_types/taggedEntities.ts b/waspc/examples/todo-typescript/node_modules/@wasp/server/_types/taggedEntities.ts new file mode 100644 index 0000000000..fd0c82b272 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/server/_types/taggedEntities.ts @@ -0,0 +1,20 @@ +// Wasp internally uses the types defined in this file for typing entity maps in +// operation contexts. +// +// We must explicitly tag all entities with their name to avoid issues with +// structural typing. See https://github.com/wasp-lang/wasp/pull/982 for details. +import { + type Entity, + type EntityName, + type User, + type Task, +} from "../../entities"; + +export type _User = WithName; +export type _Task = WithName; + +export type _Entity = _User | _Task | never; + +type WithName = E & { + _entityName: Name; +}; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/index.ts b/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/index.ts new file mode 100644 index 0000000000..8e1f86e877 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/index.ts @@ -0,0 +1,27 @@ +import prisma from "../dbClient.js"; +import { + updateTask as updateTaskUser, + createTask as createTaskUser, +} from "../../ext-src/actions.js"; + +export type UpdateTask = typeof updateTask; + +export const updateTask = async (args, context) => { + return (updateTaskUser as any)(args, { + ...context, + entities: { + Task: prisma.task, + }, + }); +}; + +export type CreateTask = typeof createTask; + +export const createTask = async (args, context) => { + return (createTaskUser as any)(args, { + ...context, + entities: { + Task: prisma.task, + }, + }); +}; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/types.ts b/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/types.ts new file mode 100644 index 0000000000..38294c6207 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/types.ts @@ -0,0 +1,24 @@ +import { + type _Task, + type AuthenticatedAction, + type Payload, +} from '../_types' + +export type CreateTask = + AuthenticatedAction< + [ + _Task, + ], + Input, + Output + > + +export type UpdateTask = + AuthenticatedAction< + [ + _Task, + ], + Input, + Output + > + diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/core/AuthError.js b/waspc/examples/todo-typescript/node_modules/@wasp/server/core/AuthError.js new file mode 100644 index 0000000000..2d965c168e --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/server/core/AuthError.js @@ -0,0 +1,17 @@ +class AuthError extends Error { + constructor (message, data, ...params) { + super(message, ...params) + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, AuthError) + } + + this.name = this.constructor.name + + if (data) { + this.data = data + } + } +} + +export default AuthError diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/core/HttpError.js b/waspc/examples/todo-typescript/node_modules/@wasp/server/core/HttpError.js new file mode 100644 index 0000000000..8a2cb04db5 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/server/core/HttpError.js @@ -0,0 +1,22 @@ +class HttpError extends Error { + constructor (statusCode, message, data, ...params) { + super(message, ...params) + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, HttpError) + } + + this.name = this.constructor.name + + if (!(Number.isInteger(statusCode) && statusCode >= 400 && statusCode < 600)) { + throw new Error('statusCode has to be integer in range [400, 600).') + } + this.statusCode = statusCode + + if (data) { + this.data = data + } + } +} + +export default HttpError diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth.js b/waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth.js new file mode 100644 index 0000000000..11c884307d --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth.js @@ -0,0 +1,130 @@ +import jwt from 'jsonwebtoken' +import SecurePassword from 'secure-password' +import util from 'util' +import { randomInt } from 'node:crypto' + +import prisma from '../dbClient.js' +import { handleRejection } from '../utils.js' +import HttpError from '../core/HttpError.js' +import config from '../config.js' + +const jwtSign = util.promisify(jwt.sign) +const jwtVerify = util.promisify(jwt.verify) + +const JWT_SECRET = config.auth.jwtSecret + +export const sign = (id, options) => jwtSign({ id }, JWT_SECRET, options) +export const verify = (token) => jwtVerify(token, JWT_SECRET) + +const auth = handleRejection(async (req, res, next) => { + const authHeader = req.get('Authorization') + if (!authHeader) { + // NOTE(matija): for now we let tokenless requests through and make it operation's + // responsibility to verify whether the request is authenticated or not. In the future + // we will develop our own system at Wasp-level for that. + return next() + } + + if (authHeader.startsWith('Bearer ')) { + const token = authHeader.substring(7, authHeader.length) + req.user = await getUserFromToken(token) + } else { + throwInvalidCredentialsError() + } + + next() +}) + +export async function getUserFromToken(token) { + let userIdFromToken + try { + userIdFromToken = (await verify(token)).id + } catch (error) { + if (['TokenExpiredError', 'JsonWebTokenError', 'NotBeforeError'].includes(error.name)) { + throwInvalidCredentialsError() + } else { + throw error + } + } + + const user = await prisma.user.findUnique({ where: { id: userIdFromToken } }) + if (!user) { + throwInvalidCredentialsError() + } + + // TODO: This logic must match the type in types/index.ts (if we remove the + // password field from the object here, we must to do the same there). + // Ideally, these two things would live in the same place: + // https://github.com/wasp-lang/wasp/issues/965 + const { password, ...userView } = user + + return userView +} + +const SP = new SecurePassword() + +export const hashPassword = async (password) => { + const hashedPwdBuffer = await SP.hash(Buffer.from(password)) + return hashedPwdBuffer.toString("base64") +} + +export const verifyPassword = async (hashedPassword, password) => { + const result = await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64")) + if (result !== SecurePassword.VALID) { + throw new Error('Invalid password.') + } +} + +// Generates an unused username that looks similar to "quick-purple-sheep-91231". +// It generates several options and ensures it picks one that is not currently in use. +export function generateAvailableDictionaryUsername() { + const adjectives = ['fuzzy', 'tall', 'short', 'nice', 'happy', 'quick', 'slow', 'good', 'new', 'old', 'first', 'last', 'old', 'young'] + const colors = ['red', 'green', 'blue', 'white', 'black', 'brown', 'purple', 'orange', 'yellow'] + const nouns = ['wasp', 'cat', 'dog', 'lion', 'rabbit', 'duck', 'pig', 'bee', 'goat', 'crab', 'fish', 'chicken', 'horse', 'llama', 'camel', 'sheep'] + + const potentialUsernames = [] + for (let i = 0; i < 10; i++) { + const potentialUsername = `${adjectives[randomInt(adjectives.length)]}-${colors[randomInt(colors.length)]}-${nouns[randomInt(nouns.length)]}-${randomInt(100_000)}` + potentialUsernames.push(potentialUsername) + } + + return findAvailableUsername(potentialUsernames) +} + +// Generates an unused username based on an array of username segments and a separator. +// It generates several options and ensures it picks one that is not currently in use. +export function generateAvailableUsername(usernameSegments, config) { + const separator = config?.separator || '-' + const baseUsername = usernameSegments.join(separator) + + const potentialUsernames = [] + for (let i = 0; i < 10; i++) { + const potentialUsername = `${baseUsername}${separator}${randomInt(100_000)}` + potentialUsernames.push(potentialUsername) + } + + return findAvailableUsername(potentialUsernames) +} + +// Checks the database for an unused username from an array provided and returns first. +async function findAvailableUsername(potentialUsernames) { + const users = await prisma.user.findMany({ + where: { + username: { in: potentialUsernames }, + } + }) + const takenUsernames = users.map(user => user.username) + const availableUsernames = potentialUsernames.filter(username => !takenUsernames.includes(username)) + + if (availableUsernames.length === 0) { + throw new Error('Unable to generate a unique username. Please contact Wasp.') + } + + return availableUsernames[0] +} + +export function throwInvalidCredentialsError(message) { + throw new HttpError(401, 'Invalid credentials', { message }) +} + +export default auth diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth/prismaMiddleware.js b/waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth/prismaMiddleware.js new file mode 100644 index 0000000000..53e60c5e1a --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth/prismaMiddleware.js @@ -0,0 +1,84 @@ +import { hashPassword } from '../auth.js' +import AuthError from '../AuthError.js' + +const USERNAME_FIELD = 'username' +const PASSWORD_FIELD = 'password' + +// Allows flexible validation of a user entity. +// Users can skip default validations by passing _waspSkipDefaultValidations = true +// Users can also add custom validations by passing an array of _waspCustomValidations +// with the same format as our default validations. +// Throws an AuthError on the first validation that fails. +const registerUserEntityValidation = (prismaClient) => { + prismaClient.$use(async (params, next) => { + if (params.model === 'User') { + if (['create', 'update', 'updateMany'].includes(params.action)) { + validateUser(params.args.data, params.args, params.action) + } else if (params.action === 'upsert') { + validateUser(params.args.create, params.args, 'create') + validateUser(params.args.update, params.args, 'update') + } + + // Remove from downstream Prisma processing to avoid "Unknown arg" error + delete params.args._waspSkipDefaultValidations + delete params.args._waspCustomValidations + } + + return next(params) + }) +} + +// Make sure password is always hashed before storing to the database. +const registerPasswordHashing = (prismaClient) => { + prismaClient.$use(async (params, next) => { + if (params.model === 'User') { + if (['create', 'update', 'updateMany'].includes(params.action)) { + if (params.args.data.hasOwnProperty(PASSWORD_FIELD)) { + params.args.data[PASSWORD_FIELD] = await hashPassword(params.args.data[PASSWORD_FIELD]) + } + } else if (params.action === 'upsert') { + if (params.args.create.hasOwnProperty(PASSWORD_FIELD)) { + params.args.create[PASSWORD_FIELD] = + await hashPassword(params.args.create[PASSWORD_FIELD]) + } + if (params.args.update.hasOwnProperty(PASSWORD_FIELD)) { + params.args.update[PASSWORD_FIELD] = + await hashPassword(params.args.update[PASSWORD_FIELD]) + } + } + } + + return next(params) + }) +} + +export const registerAuthMiddleware = (prismaClient) => { + // NOTE: registerUserEntityValidation must come before registerPasswordHashing. + registerUserEntityValidation(prismaClient) + registerPasswordHashing(prismaClient) +} + +const userValidations = [] +userValidations.push({ validates: USERNAME_FIELD, message: 'username must be present', validator: username => !!username }) +userValidations.push({ validates: PASSWORD_FIELD, message: 'password must be present', validator: password => !!password }) +userValidations.push({ validates: PASSWORD_FIELD, message: 'password must be at least 8 characters', validator: password => password.length >= 8 }) +userValidations.push({ validates: PASSWORD_FIELD, message: 'password must contain a number', validator: password => /\d/.test(password) }) + +const validateUser = (user, args, action) => { + user = user || {} + + const validations = [ + ...(args._waspSkipDefaultValidations ? [] : userValidations), + ...(args._waspCustomValidations || []) + ] + + // On 'create' validations run always, otherwise (on updates) + // they run only when the field they are validating is present. + for (const v of validations) { + if (action === 'create' || user.hasOwnProperty(v.validates)) { + if (!v.validator(user[v.validates])) { + throw new AuthError(v.message) + } + } + } +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth/validators.ts b/waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth/validators.ts new file mode 100644 index 0000000000..bfefc5fb78 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth/validators.ts @@ -0,0 +1,5 @@ +const validEmailRegex = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/ + +export function isValidEmail(input: string): boolean { + return input.match(validEmailRegex) !== null +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/dbClient.ts b/waspc/examples/todo-typescript/node_modules/@wasp/server/dbClient.ts new file mode 100644 index 0000000000..c0720735ed --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/server/dbClient.ts @@ -0,0 +1,15 @@ +import Prisma from '@prisma/client' + +import { registerAuthMiddleware } from './core/auth/prismaMiddleware.js' + +const createDbClient = () => { + const prismaClient = new Prisma.PrismaClient() + + registerAuthMiddleware(prismaClient) + + return prismaClient +} + +const dbClient = createDbClient() + +export default dbClient diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/queries/index.ts b/waspc/examples/todo-typescript/node_modules/@wasp/server/queries/index.ts new file mode 100644 index 0000000000..f26278bda1 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/server/queries/index.ts @@ -0,0 +1,13 @@ +import prisma from "../dbClient.js"; +import { getTasks as getTasksUser } from "../../ext-src/queries.js"; + +export type GetTasks = typeof getTasks; + +export const getTasks = async (args, context) => { + return (getTasksUser as any)(args, { + ...context, + entities: { + Task: prisma.task, + }, + }); +}; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/queries/types.ts b/waspc/examples/todo-typescript/node_modules/@wasp/server/queries/types.ts new file mode 100644 index 0000000000..0617ad4559 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/server/queries/types.ts @@ -0,0 +1,6 @@ +import { type _Task, type AuthenticatedQuery, type Payload } from "../_types"; + +export type GetTasks< + Input extends Payload = never, + Output extends Payload = Payload +> = AuthenticatedQuery<[_Task], Input, Output>; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/types/index.ts b/waspc/examples/todo-typescript/node_modules/@wasp/types/index.ts new file mode 100644 index 0000000000..982b766e37 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/types/index.ts @@ -0,0 +1,9 @@ +// NOTE: This is enough to cover Operations and our APIs (src/Wasp/AppSpec/Api.hs). +export enum HttpMethod { + Get = 'GET', + Post = 'POST', + Put = 'PUT', + Delete = 'DELETE', +} + +export type Route = { method: HttpMethod; path: string } diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/types/package.json b/waspc/examples/todo-typescript/node_modules/@wasp/types/package.json new file mode 100644 index 0000000000..c14a10c91f --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/types/package.json @@ -0,0 +1,3 @@ +{ + "name": "types" +} \ No newline at end of file diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/universal/package.json b/waspc/examples/todo-typescript/node_modules/@wasp/universal/package.json new file mode 100644 index 0000000000..12d29660b8 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/universal/package.json @@ -0,0 +1,3 @@ +{ + "name": "universal" +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/universal/types.ts b/waspc/examples/todo-typescript/node_modules/@wasp/universal/types.ts new file mode 100644 index 0000000000..8cadbd740d --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/universal/types.ts @@ -0,0 +1,31 @@ +// This is a helper type used exclusively for DX purposes. It's a No-op for the +// compiler, but expands the type's representatoin in IDEs (i.e., inlines all +// type constructors) to make it more readable for the user. +// +// It expands this SO answer to functions: https://stackoverflow.com/a/57683652 +export type Expand = T extends (...args: infer A) => infer R + ? (...args: A) => R + : T extends infer O + ? { [K in keyof O]: O[K] } + : never + +// TypeScript's native Awaited type exhibits strange behavior in VS Code (see +// https://github.com/wasp-lang/wasp/pull/1090#discussion_r1159687537 for +// details). Until it's fixed, we're using our own type for this. +// +// TODO: investigate further. This most likely has something to do with an +// unsatisfied 'extends' constraints. A mismatch is probably happening with +// function parameter types and/or return types (check '_ReturnType' below for +// more). +export type _Awaited = T extends Promise + ? _Awaited + : T + +// TypeScript's native ReturnType does not work for functions of type '(...args: +// never[]) => unknown' (and that's what operations currently use). +// +// TODO: investigate how to properly specify the 'extends' constraint for function +// type (i.e., any vs never and unknown) and stick with that. Take DX into +// consideration. +export type _ReturnType unknown> = + T extends (...args: never[]) => infer R ? R : never diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/universal/url.ts b/waspc/examples/todo-typescript/node_modules/@wasp/universal/url.ts new file mode 100644 index 0000000000..d21c06c65c --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/@wasp/universal/url.ts @@ -0,0 +1,3 @@ +export function stripTrailingSlash(url?: string): string | undefined { + return url?.replace(/\/$/, ""); +} diff --git a/waspc/examples/todo-typescript/package-lock.json b/waspc/examples/todo-typescript/package-lock.json new file mode 100644 index 0000000000..83fb0f8781 --- /dev/null +++ b/waspc/examples/todo-typescript/package-lock.json @@ -0,0 +1,202 @@ +{ + "name": "todo-typescript", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "vite": "^4.5.0" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.15", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/node": { + "version": "18.15.13", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/esbuild": { + "version": "0.18.15", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.15", + "@esbuild/android-arm64": "0.18.15", + "@esbuild/android-x64": "0.18.15", + "@esbuild/darwin-arm64": "0.18.15", + "@esbuild/darwin-x64": "0.18.15", + "@esbuild/freebsd-arm64": "0.18.15", + "@esbuild/freebsd-x64": "0.18.15", + "@esbuild/linux-arm": "0.18.15", + "@esbuild/linux-arm64": "0.18.15", + "@esbuild/linux-ia32": "0.18.15", + "@esbuild/linux-loong64": "0.18.15", + "@esbuild/linux-mips64el": "0.18.15", + "@esbuild/linux-ppc64": "0.18.15", + "@esbuild/linux-riscv64": "0.18.15", + "@esbuild/linux-s390x": "0.18.15", + "@esbuild/linux-x64": "0.18.15", + "@esbuild/netbsd-x64": "0.18.15", + "@esbuild/openbsd-x64": "0.18.15", + "@esbuild/sunos-x64": "0.18.15", + "@esbuild/win32-arm64": "0.18.15", + "@esbuild/win32-ia32": "0.18.15", + "@esbuild/win32-x64": "0.18.15" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.6", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.27", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", + "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/waspc/examples/todo-typescript/package.json b/waspc/examples/todo-typescript/package.json new file mode 100644 index 0000000000..0c7717f7b6 --- /dev/null +++ b/waspc/examples/todo-typescript/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "vite": "^4.5.0" + } +} diff --git a/waspc/examples/todo-typescript/src/.waspignore b/waspc/examples/todo-typescript/src/.waspignore new file mode 100644 index 0000000000..1c432f30d9 --- /dev/null +++ b/waspc/examples/todo-typescript/src/.waspignore @@ -0,0 +1,3 @@ +# Ignore editor tmp files +**/*~ +**/#*# diff --git a/waspc/examples/todo-typescript/src/client/LoginPage.tsx b/waspc/examples/todo-typescript/src/client/LoginPage.tsx new file mode 100644 index 0000000000..c203946d81 --- /dev/null +++ b/waspc/examples/todo-typescript/src/client/LoginPage.tsx @@ -0,0 +1,16 @@ +import { Link } from "react-router-dom"; +import { LoginForm } from "@wasp/auth/forms/Login"; + +export function LoginPage() { + return ( +
+

Login

+ {/** Wasp has built-in auth forms & flows, which you can also opt-out of, if you wish :) */} + +
+ + I don't have an account yet (go to signup). + +
+ ); +} diff --git a/waspc/examples/todo-typescript/src/client/Main.css b/waspc/examples/todo-typescript/src/client/Main.css new file mode 100644 index 0000000000..5c086b1dc7 --- /dev/null +++ b/waspc/examples/todo-typescript/src/client/Main.css @@ -0,0 +1,53 @@ +* { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + box-sizing: border-box; +} + +main { + padding: 1rem 0; + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +h1 { + padding: 0; + margin: 1rem 0; +} + +main p { + font-size: 1rem; +} + +img { + max-height: 100px; +} + +button { + margin-top: 1rem; +} + +code { + border-radius: 5px; + padding: 0.2rem; + background: #efefef; + font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, + Bitstream Vera Sans Mono, Courier New, monospace; +} + +.auth-form h2 { + margin-top: 0.5rem; + font-size: 1.2rem; +} + +.tasklist { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + width: 300px; + margin-top: 1rem; +} diff --git a/waspc/examples/todo-typescript/src/client/MainPage.tsx b/waspc/examples/todo-typescript/src/client/MainPage.tsx new file mode 100644 index 0000000000..817fbd4878 --- /dev/null +++ b/waspc/examples/todo-typescript/src/client/MainPage.tsx @@ -0,0 +1,94 @@ +import "./Main.css"; +import React, { useEffect, FormEventHandler, FormEvent } from "react"; +import logout from "@wasp/auth/logout"; +import useAuth from "@wasp/auth/useAuth"; +import { useQuery, useAction } from "@wasp/rpc"; // Wasp uses a thin wrapper around react-query +import { getTasks } from "@wasp/rpc/queries"; +import { createTask, updateTask } from "@wasp/rpc/actions"; +import waspLogo from "./waspLogo.png"; +import { Task } from "./types"; + +export function MainPage() { + const { data: user } = useAuth(); + const { data: tasks, isLoading, error } = useQuery(getTasks); + + useEffect(() => { + console.log(user); + }, [user]); + + if (isLoading) return "Loading..."; + if (error) return "Error: " + error; + + return ( +
+ wasp logo +

+ {user.username} + {`'s tasks :)`} +

+ + {tasks && } + +
+ ); +} + +function Todo({ id, isDone, description }: Task) { + const handleIsDoneChange: FormEventHandler = async ( + event + ) => { + try { + await updateTask({ + id, + isDone: event.currentTarget.checked, + }); + } catch (err: any) { + window.alert("Error while updating task " + err?.message); + } + }; + + return ( +
  • + + {description} +
  • + ); +} + +function TasksList({ tasks }: { tasks: Task[] }) { + if (tasks.length === 0) return

    No tasks yet.

    ; + return ( +
      + {tasks.map((task, idx) => ( + + ))} +
    + ); +} + +function NewTaskForm() { + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + + try { + const description = event.currentTarget.description.value; + console.log(description); + event.currentTarget.reset(); + await createTask({ description }); + } catch (err: any) { + window.alert("Error: " + err?.message); + } + }; + + return ( +
    + + +
    + ); +} diff --git a/waspc/examples/todo-typescript/src/client/SignupPage.tsx b/waspc/examples/todo-typescript/src/client/SignupPage.tsx new file mode 100644 index 0000000000..11d01c9429 --- /dev/null +++ b/waspc/examples/todo-typescript/src/client/SignupPage.tsx @@ -0,0 +1,16 @@ +import { Link } from 'react-router-dom'; +import { SignupForm } from '@wasp/auth/forms/Signup'; + +export function SignupPage() { + return ( +
    +

    Sign Up

    + {/** Wasp has built-in auth forms & flows, which you can also opt-out of, if you wish :) */} + +
    + + I already have an account (go to login). + +
    + ); +}; diff --git a/waspc/examples/todo-typescript/src/client/opcije.md b/waspc/examples/todo-typescript/src/client/opcije.md new file mode 100644 index 0000000000..5dfd11e599 --- /dev/null +++ b/waspc/examples/todo-typescript/src/client/opcije.md @@ -0,0 +1,132 @@ +korisnikova definicija queryja: src/queries.js + +Ciljevi: + +- Zelimo li da gtd na frontendu i backendu vodi na implementaciju koju je definirao korisnik (ovo je laz)? +- Zelimo li da tip na frontendu bude tocan tip (tip generiranog rpc poziva, tip koji tocno opisuje runtime fenkcije)? +- Zelimo li da tip na backendu bude tocan tip (tip generiranog dekoratora na backendu, tip koji tocno opisuje runtime funkcije)? +- Zelimo li da su importi isti cak i ako API bude razlicit? + +## Rjesenje 1 + +To mozemo postici tako da: + +- Kazemo korisniku da omota svoju implementaciju queryja u nas hook koji vraca tocan tip +- Ucinimo tip na frontendu istim ko na serveru (izbacimo query cache key) zato sto imamo samo jedno mjesto na kojem mozemo zakacit tip na query, ali dva mjesta koja ga moraju koristit +- note: korisnik bi sada _morao_ omotavati svoje queryje, assignement tipa vise ne bi bio izborna stvar + +## Opcija 1 + +GTD na klijentu pokazuje na generirani rpc poziv na klijentu +GTD na serveru pokazuje na generirani poziv queryja (dekorator) na serveru + +properties: + +- nije potreban post processing da se projekt uspjesno koristi +- import za server i klijent ne smije biti isti string + +## Opcija 2 + +GTD na klijentu pokazuje na korisnikov query u src/queries.js +GTD na serveru pokazuje na korisnikov query u src/queries.js + +properties: + +- potrebno je mijenjanje importa da se projekt koristi + +## Martinova ideja + +Generirani library kod u @wasp/queries/getSomething u runtimeu odlucuje hoce li koristit klijentsku ili serversku implementaciju queryja. +Korisnikov kod i dalje na gtd pokazuje na definiciju queryja koju je korisnik napisao (omotanu u hook) - vjerojatno bismo to postigli pomocu tsconfiga. +U tom bismo slucaju trebali prilagoditi tsconfig prije pokretanja korisnikovog koda u Wasp datoteci. +Mozemo izbjec runtime odlucivanje o implementaciji koju koristimo ako znamo koji fileovi su na serveru a koji na klijentu + +file://./MainPage.tsx +http://google.com + +## Najgluplje rjesenje + +- dva importa koji se resolveaju u razlicite lokacije za server i za klijent (tipovi su razliciti) +- svaki ide na svoju implementaciju u node_modules SDK kodu +- jedini nacin da ovdje dobijemo GTD je pisanje TS LS plugina koji ce importe + resolveat na Wasp file uz tocne tipove, sto je cudno jer iz typescripta vodimo + na Wasp (al ok, radili smo mi i gore stvar) +- istrazit jel se moze napravit typescript lsp plugin + +## Sljedece najgluplje rjesenje + +- Importi se resolveaju u user kod na isti file koji je napisao korisnik, to postignemo s konfiguarcijom projekta (path resolution/aliases) +- Mozemo odlucit zelimo li iste ili razlicite import pathove (vjerojatno razlicite jer ljude zbunjuje) +- Mogli bismo imat tocne relativne import pathove (import getTask from + '../queries.ts'). Ovo zadnje bi znacilo da svoj _pravi_ kod ne moze nikak + importat, a to nam je predivlje. +- Tipovi moraju bit jednaki. Odaberemo tip tako da wrapper (koji bismo morali dodat) njega ima kao povratnu vrijednost. +- Problem kod servera je sto operacije nekad trebaju korisnika kojeg ne mogu same znati. +- U tom slucaju, bolje je da na serveru imamo pogresan runtime tip nego na + klijentu. Normalan poziv operacije koja ovisi o Useru na serveru bi bacio runtime + exception, a mogli bismo ju pozvati s nekim dodatnim "hookom" + ```typescript + getTask.callQuery(user); + callQuery(getTask, user); + getTask.withUser(user); + getTask.withContext(context); + ``` + +```typescript +const getTask = defineQuery("getTask", () => {}); +``` + +## Ideje + +- Treba li nam znanje o tome koji su fileovi na serveru ili klijentu +- mozda ne trebamo uopce generirati istinite (generirane) implementacije queryja + i akcija u library kodu (ako gtd pokazuje na korisnikov), a u generiranom kodu + (.wasp/out) presretnemo rezoluciju drugim node_modules/@wasp folderom koji + sadrzi tocne implementacije (jedan u .wasp/out/server, drugi u + .wasp/out/web-app) + +## Sastanak u srijedu + +```typescript +type Args = string; +type Context = object; // sadrzi usera +type Task = object; + +// client + +import getTask from "@wasp/queries/getTask"; + +// opcija 1 +// ovo ne odgovara stvarnom tipu u runtimeu +getTask: (args: Args, context: Context) => Task; + +// opcija 2 +// ovo odgovara stvarnom tipu u runtimeu +getTask: (args: Args) => Task; + +// opcija 3 +// ovo odgovara stvarnom tipu u runtimeu +getTask: (args: Args) => Task; + +// server + +import getTask from "@wasp/queries/getTask"; + +// opcija 1 +// ovo odgovara stvarnom tipu u runtimeu +getTask: (args: Args, context: Context) => Task; + +// opcija 2 +// ovo ne odgovara stvarnom tipu u runtimeu +getTask: (args: Args) => Task; + +// opcija 3 +// ovo ne odgovara stvarnom tipu u runtimeu +// ali baca exception jer nema konteksta +getTask: (args: Args) => Task; + +// kak cemo ju koristit +getTask.callQuery(); +callQuery(getTask); +getTask.withUser(); +``` diff --git a/waspc/examples/todo-typescript/src/client/tsconfig.json b/waspc/examples/todo-typescript/src/client/tsconfig.json new file mode 100644 index 0000000000..93c79bf3d8 --- /dev/null +++ b/waspc/examples/todo-typescript/src/client/tsconfig.json @@ -0,0 +1,28 @@ +// =============================== IMPORTANT ================================= +// +// This file is only used for Wasp IDE support. You can change it to configure +// your IDE checks, but none of these options will affect the TypeScript +// compiler. Proper TS compiler configuration in Wasp is coming soon :) +{ + "compilerOptions": { + // JSX support + "jsx": "preserve", + "strict": true, + // Allow default imports. + "esModuleInterop": true, + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + // Since this TS config is used only for IDE support and not for + // compilation, the following directory doesn't exist. We need to specify + // it to prevent this error: + // https://stackoverflow.com/questions/42609768/typescript-error-cannot-write-file-because-it-would-overwrite-input-file + "outDir": "phantom" + }, + "exclude": [ + "phantom" + ], +} \ No newline at end of file diff --git a/waspc/examples/todo-typescript/src/client/types.ts b/waspc/examples/todo-typescript/src/client/types.ts new file mode 100644 index 0000000000..7739835bb7 --- /dev/null +++ b/waspc/examples/todo-typescript/src/client/types.ts @@ -0,0 +1,6 @@ +export type Task = { + id: number; + description: string; + isDone: boolean; + userId: number | null; +}; diff --git a/waspc/examples/todo-typescript/src/client/vite-env.d.ts b/waspc/examples/todo-typescript/src/client/vite-env.d.ts new file mode 100644 index 0000000000..1623b9c79c --- /dev/null +++ b/waspc/examples/todo-typescript/src/client/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/waspc/examples/todo-typescript/src/client/waspLogo.png b/waspc/examples/todo-typescript/src/client/waspLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..d39a9443a8153b158b76f51dda2e42f3b34a9169 GIT binary patch literal 24877 zcmYg&bwHEf_dkq*gusvzkd_XWZiaMsk5K8HN=eC7L}^I@=>{1+8fKsfBHazrDJlJX zHhh1+zdt}^&vWj%_q4D&@u0Q`pkmVX8C5AIU~ z)%!TeU$<6)|0vm;sXJ(EgFMLG9itLx;iO8y!!UFN2mo zq)jePR{z?S5zd_RmRQaZ-s9hvY*-G+Oxe@2<0oVZfp&r(y9J>p3&!au#tGc~p9srF zAB;|^ifJFEt%%)gXctrOY4{#rqqEt1xTkb3!(+}xznhxTyW5O1&mGAeSrGMyHN_nG z?TGqr2QvkBmpLAO$gs>l{eEyp`6DQEYsx(*Bh8sFS53e{0S6asK=sCeKOzSL&H4%= z)_*5Z$w+15V|*(m`!s!DO8{#AMBb{$UEsy4e{c!Y5&n<4&n9ksLx)_0G2i0@jz;lo zB%W>I9&9kR_>^~7hEe^RXE{~RrGT=S;(|<(lSZ#YWPc4C`Mv%#uc0)n(9$sh;zydf=06^E^RE}Ufgw9; zA?U@vUs*hac|4FiHAK9g1#&w_!!1puuRk&ldA*ftbnx_2@R~?Ec#;R@zv$7QA3ERp*U@$*=Gv)1J{Q8RU{AVR)UMk z^rvrZ#=okD`zvTtKy4Fn5Ux~2x-7GGAu32QV-g!A`8*zV#iYB<`9|x(cLQq#HL;EO zs!!RAgW9#FD0(7G{^3CZ%H?$g$>Z5gb0Byx#-}&D-@Axe* z&I+BmbqDt+Wn1Na=iYrX0Y%kpW(>ICT`Cs%VzIUrVGTrWsocV@`gNo=W0AH)&1hsY z1st@M7AF`or1EKLLP*>EQ{cm9ambDma5O)nv*?DX)Rs_db5_zm+JW=HL6lnY3@fs5ApZJjUvC_@F0Twz+(-Dm zkptCf7dwS3*t#J)n1%v2lj&v8^Eof;*~W!aetK^lJi`aTM$H1x?^J(47)hf>Mk!c{ zkD4YMDh*pfmCOD8{2?^R>&TB&2{E4iEp2e$W?Z64OL*zd@0~|)q~pZGZbN5@ojv=8 zZNXkSr((p$*VdQr53hSaO>TamfHuYTCCcR!nfFieNL~k@eGhc!-n`FZ1c0z8AlfH@ zD=Su1HH>JcfSjMUYKhP7&vsW+i_%LA;=x~g7I{b8_Vi)r#h*vwk|ruZAi^4|S-flx z3O<&*s1VLu>Gtaz++}kf0g)}7_?L zQvC+IXM^*;l}Gn;qcy**;|i!`X(Bp21_CYu-GdOE*4`tbb_zHMF?>Rp`~6CbTKsus zR1-LQusf9BckC7oxI{p&=}b=`u)D6QBuZhM!bt!+>*D^Eo|^hiveW0j17dSL_1-5> z?JEI8@dy!|zU_@@{o|(~qDom&#S((%#s!UV-^eJc3L&FyR9B4|-jtY{6eTtX)LR%= zeCn%YV|wA2XkBjG#I?8u78vsfd=?SY(rCLTA)G;r@Yc89Q9f4Kend~t2jX2_>(^l9URwnQ=X>G zwMtT#Cz*zzji-Y&d*}%V%dP&pw0OPnP~~>gcB!D(WetJSfZoZLKfE)zInjt(NQ6z? zBeCn;HwJ*Yo{?=;O#HRWEx@X6M}peF*`q1nLLwA+MI7Yonp^E{_5Ep%`5{~!zk(FR zN(8CDKW^QCg(z9z_9~j6 zo)LYm8;x))3xX-E2bIM0Fz&u6WCB;w!VNV8M>t1Kb6PgZ$4pg83GD#w@BEb%Crs13 z=yPaD&9%1F>OPwM*qbT_2c%4-l*&`%c0KgjuxY9Lur`YX3y zC(dUZWRS@GJ^-%>f(s6(sTY&%am}#Gquj89JW^o6Y#Gw@R^{8#id!_-M$3uvDN(3^ zFRL>R7*bRP%_DL6epoIx)k@plJC?>z=qCuB-3`2E-11C{A~Z!LGUpJ>y6fKsPFX|KaeOj4)3l&zHj$0tQ=Rx;}yUJwtb z?RHSex(hJ!VBhH0of>hqIg-Y1MLb+Eq{A%BW-;@YK!uGRjJ%z_S1$xT6dTG_O{LfC zA9^t_qnK;qF>e5zA(TZPp+;Hw?T`n*+;z7!Ar*2rXrq!PHa_IV@Gt-G=MlkUjERPc zMlW$dgd#L=HUiHDPikI_oJX0?ngf^vtm3-5GbwnU1C@gG{!7>LTkwl;!zW&uP~dqg z)(VTnjN*w^V(ZdyCme4RRY0xMgVF4*HA?hmIlKJNYk_hlDQ#X+cmYF$M#iu#b5BK|4M5GTNR3?Zr3qi*!pk^A;@JZvk@ z^8Q;y`wRq7=8lKjC6D@@>WO@3;x?>X(F_I!9UCia+f%@2m-g|SCMNz$!AA}q zTXC(NMAjuY#P17O0mx{~$QKZ>cA#{x)p--M@st=wsX3u9^%{^)4Lf9v{#@`ZuHcEB zY-;{@e!vT}z-w{l?S-RUHVbX|9lIXVFadONKwrQ~tK@wPqT}9QDjbh-5fp$`pq`4ZcGaq#Q{z44fg^qc+;3nVi4ZF7P6A{RgA zHR0EY&AXPKj;qD#VKUu{00waomH1nAj}JdNA+4kid7BN40nUw&&TgI$7Y&OID-#8<&C&~-l}TScdFSM5ITk2tT^0v8c8!20Wt#)e@Ig||JT9eTcuYNK{d(g>kI8f^YLl*-jFD-#|1;_LNy zM|;!#lLhveiwTu{Dua(&ZE~$*3JsV-!R$cf;9#0Rpm!eXU2##FncI)jEmYFLM8A*~ zz1at4mrS8ZKB>v*Av_uuIDYe~!0#OnCcHdX!TcKU3kAi!`e3ebSB4mQ;S zn|Ukkhk@hmh8fR6fjcL4x(1&5&0i1ujkkIXKUEvv`2eDT0_6QE%v>lpR-B@z#x+^p z*4uEnx=-6;{9PVQdVc41o65USXep~+bq_=OE!yo!TI$#;UHuQ$S_BR|taamU!bCRZ z0@n(9&Q!$dd@doPKmsdeelVL=%so`h`XsW}oJu#PofuLde$bQWbDm%G@}lpe&f}V$ z0d6Q8069WC0G{3_kB%9EaFIKWn7bB;5KQ%V>4r)@S(y@E2~=+imArU}Wwwgi?>3Ru z-sYNHY4*MhzC;+G?q=k=!RX2R&%>U^q!0KWxrSq8Aw)I$>s(MbxX{szNO_qLO=#LW zoibRkS1V@x$#MPgmlki99+uA>aPY_Qn2~2zrQt3(2L*r_G`?3>80^#bZk=eZIJ)4> z+~;?@qlevoG1`FfVW6wX%vNU&4RMcAh+=rku7hR$=ixbGOA=#}2Yi+;8kp6?sork} zk8ri^MY!R69|1w(+S~H?Q46CdUN`@cl$^6o6;KTjyC`F1+o6v>N49mGZ~128N)Brp z^AN$+?s0T9hrhjair>^feRnY~$yP2^d<}ROie^W22-di7zF!GNjpv&5CC~scr-90~ zeAL``D_z|Y)K5D+D7LD>7&a)2FCtB(7?0U{l%HCsCmb@m$ zemP!DfhDVm2pBB*vQPSLkqH&W83mb@*dc^4KaZrHDBUMbTptwsoD?JdMvN5@Ry*NO zcqZu&N2PlQhOXCu<;g0-&5V;*pVg6$E=oDNx=V%6>(9;_l}6Dj$mUHHlKr#c>m4)0f? zl7XZnW2jE+u=2nsCAGSROFM@S`{V->8MlRvUMJ+#Eg$jDK7Ua1wNw2W!7<(LdM$QK z%V)i}QZZm=(W60hWEh!}4TY9_a%(2v-@GHw5)qf{h)M~QQ-g}2c^SzdERju z-q096(*VS6M~GizQbu7EQ<9cxw4>KcR4~}!t`0ek>~qxKO)*>w_Ma99nJP%Dd7jnr zIGw|WbOnouj3TfLG9rF#33DB96zgQRtnna9L#DFDo6}@30$Yw#dTVf=mtaB(UY=RG zr~6~^fds}kvPN|%>z^EsFr4ENTg;f&E<8b|t^u|gSo7wx+{?HgHvT7ozhCSqQ`eR+ zZWH{~pO)?!wzQ&Sj6DfO@T6m`Ttr+?9;@!&T}wb&QLPLbBZK6HD`CRoGN14;ngwV9 zwM6yBn2{SFuYokXfaHc#OLk9h(ePVIBV}qV9YQZo$XH5h;kseE``7Vwz(Pn-ufu~v z%RIT=g6h9?n=xX9j)OP@vz@-aKcc!TAO?|uvO zh30VQR01xdQnROLEh>OGj3U4x4+l%48SJ&DxjU9<8bnD2ow7?$lPK=YoB3>2%ZsWEvj8kU_81`8AKIQPW83-qq6uX*27f2HwfJPrQDb&zRH zlc2R*dDVeWad^t)BYn4MjPOH3VnC(d2Qy!C8hjFCqDbngN^cP~Q)71~i)BBrH{Y{60 zW3oljs@g=A+DS>vS7l_IS5jp0Lu^ht+U}a)3^d){1cZd=+07!az-}o&l(w1A==QDT z3K9vb=pcf%>bu8U{JP~r+eC;`;dxI!x0ZrxZA=*f1BW=GK{=8(t14^zaYm47TC>J3 zm+R0`O@mbF74v#Nh@(MyhOX%zqm%FId+v8RlQ-xpo=%u08ed_&C<&U$*u@cIvHb*O zN|I~$Z~(Un7Iq^=xiyp;8=?~$>7lz-W*n~x-*edu(w%jg&&}>nY0UfNO>AlSF9ZWN zZ)C|*m3Z<|hQ-3RtCaMf&$eWk>-99hx9+9Mf*fv9xL*gZ_H>vD?mJ^f4T##_dqkq)cY4xOO~U)fdUx_kbk^*ZGAx|Lm~=c8_o zvd`*!iax&09b_Xhv^?-Rp|7D~SC4t!hOEX9ZglsZ{oel&Ehezu3640)FdiMZ@{9>R zd8}dXD|U_vnE-#HiYOEbtb}H-BX6zmqj<@9Tt-$bsndAetct>333$$wh+M}$cNf94 zRV{iPN&0clTm&$uMkA?BOoP_uIEI`Uz~X?YX+}#g=qw++uo&TEuJ_{p^C-v9adCzS z=L%A>QE2J)%-XR8Cbi;3xiHNUwKEciiWw)HC~~{!eOed{;DYZ{nH8!hTbH^DUI>2u zR@k@HNgxel>-N)jnAaNl3Lh%pf^yfaq^IZoM(;xEu|ORjBbrC zktIjaqX7Xd!EG?=X=ck0#|mHVu2zM_>#&p5HPhOGqn?ap`YY5xZC{3*Zg^*YqK5JM zs_BQ_h#AWMx@`)#8L+=%=?t<`eYDa$-w-UqnQ3Cg3wIWT932d%DM?E*Tf1CY5*!2p zpsr7PTKjG_)qZl#4=fQ1qgs%5kNZ6QY&_ysnL%sf>XkWxg5swfpX`p^(#s8ScCiN~ zpO0Ob>s1GQdT4pI97Ijq*5|xhvKAC1Sac(dN_1}g(>PJ`s?xE>{>ZZUEq6t%*lb@y zH`zwQ*8C%qJ^he*(oE#+9AS*{*Lv50%U&c)VKs3lk=z? zGO|KY7A=&1|LpKXFlyuv*O9r^TK*tH(-0a64WpPbT9Z{4@aMsbEp1` zekOy#?5`PPvC5WQb5ssL{i0M}{dlXwC~uc#d@vT0MkZ*|5A{9!Hw5zO=E7+w9-yX! z^>cz0O|SM{ghERMD2j{-3gF%60VR`hcpz^#Yf0NJ3vE!HV67zZ?JfFTY|LS`cV0L< zAYmbP-sMzvlkl_2%C^clP(1K^*%i`M?g^h!KQjlS85nc^s_&^J^swaz_Ep?SR{rufq!cn{>KRkP~oX|786c=&||M3IBx%1_;YQ)&TY5+ zmQx;bEHelw{v=Lh7XgA_ArkI93cnUwChaxB4>3)B`_HJDlPDd2w=ynY z47>N4dSdlenYFLz1g6FVM1exM;*bTQ`#2Ft?65!g0~-3*4?qX4Ww;xjsc*1kpbcO) zj#s0HF*Ku5a=t&;DUfzzV;!v;qr1lfN)!K%Uk^>%!Oizq4zFbdp;`TdDYv552GR7c z5$# z{(Fr(ETk&*PV(6wHu%^>X^j9MAIU?(7A(?%Z@OTI^*DLelisAyg`s-8w;UrjOdkB) z$9*M+u>vna%dm%s(_-JIzsCrt-3|WhzVbAlGu6Ge!Nbz6S(|a!?}Vy4y-qw}&c|V( zj{akM02rdCZrjjZ*OJpQTxeu%+P-3{^r>t9_rv&CWiNd3hJe!V5omqib#lr?ekO?S zrKLxsx^wU41+^#j0p6JxoY!o>H|gZ9MQnPdfzgL}H5ZBnVt-^OOad z^G{XC_SBRXnA5DmsFaTEgh2JQ{#^`e*%0#bu0ES_U z`&aK#jmi4gKpG%Nf=ooK6Yym1HCz)6(#+D$wPe1NRc?gQdn1e4w}n6D&OIHP+S)h5 z8F;l7mP`4wuE*KE`#}1H9G3d$pqqYbKH{h3wo?g49t8MI{*xQAG4F#^=3Lz$*u*ir zkscBL=ARXM)5Ruv=lGgnHRaPS_93k8iiE(I{mvuE?zGOII~>W&%qs^gWu1W<9N)q4 zOeZ~0UA@*rcDGgiF{a}cD}118ypE39pFi@jUjuAFp+}=Gq|O^!YpRAsXg*kv-xQ6g z#Tt;^8-KsNOqDoz=@28IVj4C0GUOUa(lKrlWZHZ;MgM}3{EaivO9FfkA!1@%G~hg} zJ7ZX!-W*c&22F9`7N_a^lmzxeoN`K>3h2%NdupaKI}50e2^#Av=;H?W({?-?egy-? z3N8q1hf&U?wbF24H8+oEXDXswu!Xbx1`i2t41R)kTx6UgwCs)h!x&6O=YAtg%$qEdy`&9lssr5hSvgp@)m%5XuYk$Uy1_2_)ul zgK;nMAD*hk84GzhFCn@zP`<_F6ZVtj*vbz?O&@Kwe=;3rz#p3f*)g>}^*8xZ6PiLG zsGZIXm>9~G!;sHd^Jze9RyVzo*_P>FN%bZq6V%D8?(n0+dqJ3gLd5&x6EW;gcMTaa zwwiQfG_Y#y*F>EltgR?s^LKsM@9WdUA4M(Yd;KjlNB(B$WL-Z0p0-&L)l-(|PMF6s zTNU2L@6e`|UGrL5i8NDt^@rcPb1g(fL!H5H9*_tUYNETHYYn(E%X^h`r&`jw&s*h( z$cCm$PbV&H53`nqo1f3$tiJPHhTqpB0%{6+XLYl?uFj{{oq@rgrhZ?vYty~O^(z-HZ^o@)3s5w zD%Je)r4bIkWFs!pIs|8w+S^f3bwKy;Bisv982xQGd#6Na?gea&I4q|-9F8Za=)4dJ zes(|iXZZ7lz1q4c+G~huJ_VTEI6o=yzr} zvbEv;lGfPd21O`Roid_0(}JSIn5AlD4ThX4uyQncE}kCiqlk6ZMJ4FzJ7P_{^4%AQ z<>3^5YboQ$_U(f@WrV*4W7S`)%fj31CcOG~y1!1&FdG5NV^4Ea{MUvnS8Do_V1fzy zNjoXNkwzjR6+2u>wkyvfU=7h_GFB<|L4s$<#NZ|tcx*cjM`?@)$;XdihP`t?&jK`} z5^3Rd-|SNok_UU{`j0KU-fxI3wkSQuC=vwWXp*v-9lv^+=gPEHyT3Z{uszXXD^QRI ztZ@f2<}MZEYLa@c<^77!+U?p6RR5%G0;YcwNi!Naa7Hdb<|KB$?nyUAlcA8607`u( z_d|roM{o#jzb+6@*=?|KM;T)-@*xsi@amMNr3q>>zNK|-cYKoTW+elb`84(Op#{fp zHW~5v^kf0<^@tF1=Z8K|y%rl_-w91KCI^jcpl+(e)!MuQ>H_d$QPuSVgvXUhlmSNv zyZD>Yfm3|N8KkS-CyiM*;j+IqGCPhNo>2*!^hP~d{q`p>M_YHD40`DAm} z8y^yzL{%s7PG2s+03LxM+y zEfJj`zZj#HEkK1$)_Switk13?Po>>hO4%WePz^e+bhN0gYUq~wDWI$g@1?8jHl@c-Vq(0i#MasL=n)y!I@<_7PI%(drjja<@2VkW zRO^{6>26a*sbq;GsH${y5t@`Q8rn)7j)qB4*w|@B*)}FUV63s)od}lxtkTCB>@Xs% zJHoie12_M=VluMphOyXrw=4Dzd8h|f?s+gFz1PRh-Wfx<)1Waag>`6qt!w(IBg%82 z;l2YFOR+;u)dY^0xtdKZ=q?%3*0Ri|Ky?jmy&3C8C`PSQH}>;09`xP<#E=ySYWgi9 z`t=bQsnPnKcgER1||Lw37Z)#yIko09XhIltQF6#a5l8O06|Zul$+P_{NG5o;QFS z=^72xKBjuRV`babz3V!+2lO5zQXxA!SzXr$AJ2xLx-)nOxBB9U5CRVsRiM8T9eW3p z?!gzsw6*1kf0jFDM~@Fbp6xe+TwAB{HX;E!<^V@_ymuc27POP|eNYVlBJnrd1tf^q zz&|+GuRS{te;C&B5mOzr6Np_Vqz$90o=!{HZh=l77aKF;BPGP@F4kMZvJjX9Y6st( zD;Xs#Y*Hp^Zq0Z*?xCuR=t>#|B{q9c zyi^+Tm3uYpqiKe%%VfDC-Uz!kp&luM8*i-KrSzt$ZZFeN z%!Yf`txDawv`?q{)rdVl3@_^VhuT&8Y3Z)g@G+Nkej91$*6tkhtArS!z+D{Dr6kBu z+fc(?FY#fNljmQ(D+I~`mg-P(#}^Db=u3|t`*YhS5nT}fkF1_C;^jJBvHj%)F>Kg# z&r9O*L{few*6RZ&#G*%yn;ZpGae3r?2Y#<{h=8LM>z4s;@bSl`K%*YBQbjL9y#7nm zAWj^;CMqs;^>M}Pk0#oT-J7qfkQj{UK$YRN^k|97~W%vby z)dmBime-J@WNsFqJ69n-@QE>v6FXlm8H@r^+PTeo@*h0`@OXzt8D26)y~W)l|Y{ptUN&Viq5SLlFnozZyM=ll;Q&@2YT=(nFbq{_sfCD2u2%-aiQMb}QMAzTf^LJT;uYM2MTq(+%-e#5h zvo3xF%V*jzMXO7S}4@Vo19f24z(H4fQNboo@Ecf*%A(!jzSfqftA>VPuQaRaU z%h5#ueZYSfbRB<mBp4cwb~llga=d)!#5rf>Ec`$y)U3jGNe_ z??`dSFPxxfK&ovulE>Qo3X?v55gO`1*RIc6OtQy@p_5zFR0Rg-BGBuH9B}*EJG<9m zJs*FS&?ZDci@`pO<7w~0sKQz#yp)-+I`o$>)^XPVO#L~d6CG%p@GEM)7v)9>!^|z- z(PBDbi@^d7*g~^<@Mho(bRx5uXej^PhK#Z}IpDd}8xKb-zr_ZqO<-JU36WdvvtZfa z`Qo#+98T~HnwxQDVgyL1zrCitjCp?mC;@h{;P*`c6lRs;v{RebwrZ&HoAm@Y=hmzf z1TZ8X;sXqC{%gC^D1@sTF$wCC2qvuu zyn@ce(#SKFf1&|CzYTA%R9^cfi0k{d0CPZzv>n25fwsEcNgS5mXtdx#qp;u8uh!TrR+Aj4&Sk z(N>=JCamGm>oKcEi~hd($daKNYfwUbK~0+pHt`RQ51vOpS-hsBJx4q!Y$5<}Pg;;4 zfOk7dV!&99{y4SkJ~br1Y{^{93dw`YJ{kfgj}0sMyimbP)D1Ff^;~LMl6Iv1vMJkz z2?!oXLF(V=W%49~HV*u*dbnW3pVYrzF}K=0TYu|RpI%VCxbCi$1x0kkMT$y%R-WaB z+vte`%f%+4KhPE$8w4!g?HtOWbTziHHJhD}eV9K(;>83c*KF`vx#Pp%O|)0sWT9gf z!ymiX^PzQm?XU{R3c9vDuP?e7OvD0Xn&F0Z z9d)ZV^71n$MWhHc*o7UI`lMBt_VCA_xI)CeJ{Uj^h$+-)T>=)*0vXP#XHutfC|5AU zeTWdDq%MmfR7b&TmE22qE7IZLJlM5k!O?!ktz}m7wug0&zb(0d!(KlqoN}PGya!2`*+y7B?AW83ZbpBl~*(Xo-Qg z1LDWZJ$@E!RB8mR(N)(4_HTUKPIGTvHFNzV1EQvSc-+T_1i)ZdXFnI!D4G(1Obn?fp7ExaTE!JIU6KeI^2VX3e zwbvY`rgf7^O>cZs#pXCWdbaCwWIX=$mQEO$#lAqRMh|iiEb&kcDpS-=yNew;RbO6s z(=t>Of$S-JS1morkk0n&v_RwtoX*`eBpr?X(Z&32rhxv9j`*L`f9R@DnFW!IgOSiV zP^ZOK>^x_v_$vaeo3LZs>C63TUGs=jxz1`uH~10i)beNaWzN_Xc})u)ADbR0LK`-l zYE;0~M~f9v6bLAFrSCnfe!Nu|gou!Sdnrwu(xSAS$atb(&3yKGW7Uzw4PuP3&UueB zFI}Zoynp?yvv$GuAUZT{lWE_RI*Iv-OFH*O2tI!3>zuIux%6l2$)oDDq}8;9n3RV2 zv$_Ey!NvK{f>cy{$FP)>(CxGz;;uK1=w&{&1X5rJtgNtyt?1rYLRwZ_v zCXeQZFWD2$D7Aw3JHBmwRVc8a_a6M*V#X-Y-LmvbVWAbE(hT8%scYRx8+vQnNy1UF zlN)FhJ^qAS)!0`eDduleULAw7!=g==g3FoHPpD0xEDVT_;04eKFj+&^SwNtH?V#?c z+b(8yTbK6M{ymbb<;_H`ydfh2CL)x4YSY3Z;+G=k3QaKZ_r>*1Z=VUWTz+@!eZY{> z8>lyaLha=0Tl!dgtgMQ{Lz;fgwO_g{6+{9ZW&{k(+4xo$$Z6VBH82kNIrv+Y@M)cH z%6X5e>kQd`p?wV)af6uKwLvjF5}pX3>E{5J$znu^H-Fw3CixjHnb$(uGr<$@4T0Le z3aO%@%#6M}r;2&T1!TEC@myX;`{~1U*Tc?d#_O`@$}F_FQE=r*_~LzS4nVI%mO&)e zE;HCe03xoCZG)xNULMehc5w_eydEMInRye$-y zV_st~2qpCHNz9shC%fv9YLkz2qL=*FK2Ra~IN%L=9uzg;}6RO%l@n zD^eex(N}w(f266|0;V;Ax~?+Ryo=FbQ2x$U?p#6w%UWgDIccO*C|%nIpJXCJRO$&~ zboB~&<&6x+JK%@i|$u9>UC||u#O#I=1vYOHxcip(hxMI(X z4w!j}dkVPU_skNamIji{JOER6{E(*jCD(!bb}-8Js|bN@r-RbL z^X&cD{iVKL`{dr}T@ttVG&yHh`B%Y@&*c|~a+yyX)A9%);cg(hJ#hc-=fpH|YRm%4 z#4>;K#y&9w$StQSxn59e$R zgXrh_AGZ&$R}uib41sn4txbco2hSZ*XV#bg_jDdOZ)yIt7M&2^ytH7)6QU`aq+IAFcBd-{)5^~5off^iOcsdTx@^oAH^qV80 zW&4U@X)=INu;87e>rt4{)t^#gJK2FRbE_=U10}b1r(5S#Pu#MK6qHY+h6(M>)`GNv zCT&(YNIBA@+yC8|uMRRtEJ!6Q6q?B}*MDj8@mm*Gv2clGKFa5ow{gr6G->|%aEdSN zJH`lL^d@U882R>ti2RFCW{l&@!VeBkTqRjK7W~fWA?E4O>TSRP`b5a>dN8JCSxmWn z?S-=lDb{E@@y>!wGrE@OhfcneFPpP_vqFHf62`fobZ&)QQ1QYonnQY>cmWv9HlQ25 z9Y!WUc(PBe4Eqf1mBo)k2vwkTUG`U3B*MA;xh>vDV;GnPKCnk(NRiS&*L~| zl{&HBJq-72bgnyj$G0vy>6^5hUts)OZwuP}`AYDF2TI0v+XMBG0nEz@F$ZWNN_)17 z&5l+vvz>ZU2pMv&=z+rTXR?~AYGDvH8(>oes`XpiZ3G%ugG`3ApT6&wc3Tytu5&Ie zV^S3*pK{kA0XT5=$?I9>GTs)MHYUsBy|aU6Kc;AQGYR(Arv!Z>u6J7$N=mv7ckX=V zn?7**auY&|&BiLOF?Ewah)LhWRBZ^&W-t7|mBhdsGQNKJPQ62xzl!l7vhN?wj=OWA zfeTUj=vX&n>AV$G5%VLrhI@QibM41dG2e463UuT$MMd%X@7}g_zYbG0Fhg7AV%~>x z;AdF5qdbCwt^UieuKzG$z%#Yk6U$elF5{6Q&oP-AMg7^tWle`6kQ1zRy;cnGb9*XW|B*fr0rc^J<>BCrfzZ-D z3qsg4bwk}*Q0dwj@9MYL=)^Os6v3cQtPmlo=yC7kTE7#M;~`~jLNMQ5HbYW!nBuPj za@5PKOePDOfrxnehxac*ZdW=-6^I;gn+S0 zsOvMRRdz8}KfMv&l1v7-;eSwyS)_=L*Y+iIh^~z!v2a^ztjp^G?|#@%KrfE{DwJ#` zzTSl)f7^1+wbYpwz)wgBGl)`5hx@ncODBAg95H5jkf}(Q$x3QC`N^oqI5yOp4MYEZ z99;9#gk`quUmguNn=O%-cO!J`Xq34cF+k@K0u*1No#qQtOUCT`gsR~7^#PD&u5 zFVUZ+PwY&k>cGsaV-OB}`+~1xU)+b_TS^Q6(*5^;Rh1QJmO>IeV?BS0_(S(=?Nu0C zaPSJP!%8Pgb;;Qnh;$6s}W#Q0=A0gjQrPSLcDN}a+O;qw+gT>zPsK;NG9-Vi{*NsjP%fB_L0CG9v7X@Dw389O%zu+gghp0b>OS<+@RTX;I&b5; z+{^#ofii#ug&Q^o;dDUNDQVD{W~`gEZeATLaO{?%K%H9ZKI4CXR;WHAK%Q7UVv4wB zLM;D3R)%PtQZePP_j~mv=hNB$mCSmRpmrl7mgUc8*Vy5rUlv6FqnITKw-SB}l-Yp! z2Iqx_GR`*ovf%|^m$HgIy+W*aGU`AImsw_-gENr^a)XHL6M0(|(7R3Kw zxsXavMWf0fqP6Zi>|nFpL!azwWDmoGF_jY^do4q~V0jKp(7)LRT(AXUP2#(864>5H zn=oo-cYW6XdtrzIKGtEMCzgZ)B}Z$2JK0WlPx#LJzrBir6QbfG2UR?Q+Nru^5Ev;Y zV}2E>0`(xaUIj51dAK%;&&%v_7VqnQpox72vYUozd$#1Lao9K+tS{s6RcCW?_mlsx zj$4h^K5!c}VV)p^9rR1YlnR)b{D-wT60d}}QY5g2jjFhXK3RYu(1ws8s~QubN5Ju= zek!HoBPCIKLNQ^bid`71_=NbQWu{HrNe;G>BSY;s>5@{kx#Ue%zh*d67xcnxqRdtq z3z7`*Fml~_FIp@^q%wENoDLyG4D0)~82H+!ITxMicJr~>V{R5~8HpJT)OIMj{`Ni^ z4*{-~S=nWKfJPQmnO;=m6HoRSVXkIGQgw*JqnOR5<=OREagz~juIo?a6pf{T4a)>* z;q%j55MEWJ!m0g&-GdLFs@;=!b=ftr)t4pl9;}bXGd1x4`Jg>I!5mXr?l7DEQ7DYG zRr_zN*LJ;_7*=~blvsKc z!wtoH@<^z{p@bkSN1!h<;_yDymfTqAgyrcP`)&Fs%`TV4NBXyX zkeT^HW@`wrm}QDcd-@^IkpW~{-W0LUzosBTSnIxKVL$%3HR#7p#0D191OAlBfUIu|^O9gctQkONd0fjg>;;=d>M zWqp3t$+;+I!4U~UI=q0zd9)=hBu*WD9}jD*iC{kN!HmOHuy2H>!PA4kLjl!^9C-T3 zfCzSA@?qeqD>v2>XSoA$_q`?;{fW|}=K1J}RQE>po_0MP;O6Crc#}s1*I~=W`l;LR zuZn8qh;5i1DwriE)x~?G))E{4FcH>kEmUdqYPw*0e6sm8=f1pU|N5N;pq}!3(x=PX zrt0VIc*}X#xAEC#%b|9(gFqkET}uGYQAX!Dqmyq;brs4P&6b`tog!4zkZ~sw<)d5& zY&;O$=lCZZ8zKzLzYtp9Z)wMbA~y4jX*3>SZ4CpMsZRtF&^)x!)8paCyLXN9Tbk@M z>See+nKphfP+v$}nodo0c+}50S5nJAr1ajw5tyq0#t5uzH(wKdqr<^riNO3V0I6zr z-A-{g$?9jqM3&iYzE00oLB)7 z5Iz}KfsO{EmAL-;LX}ph<{E-;d|>j!ciMtUop4MMhgV>yhK#xOn9^Y@?X5C@7?Yk- z+2C6!D~NWuTB{_nkg0yHY~lB@kuN3Y4s0U6U}L9SW^VVza_U1WRJzMy$#MIjb{QcR z3(%JDbB?9s-`mizwDT>sb8!C=lFSAKj%T2)ow+;Z!l%3B-N8*2qZh5`wGIp4s3!&h4(EPb|ZYXVO-}cLf z@k0>fU`PlT$g$~qyt1W1Ce6c&cYlN?rcEZ851w*ndsHw17x-}ydiWWAGQ;9C`XuI; zbw4*u#ZzUgK9^5#)JKgm-w_=Mg8&67S~JJ^&Covn0Evib2b$Ngm!e!F9G!1f?fT%I zn0Tm)1|D`WDRO<2-=ig4T%7VOzUO^euc(BHT-iisHTp)0jU6wOwuoTf*S7%c&yRTe{K4V0;TM;u zsby0SftiZ)QW4EOesD9*-yVL;5J$G-EaE=_w%D6NWz z8n+w_Gf8nYwHqtY!ZUp`Z*FJvs|qgxd%-TY75!V?Ayb0${0R>Q?j8ID<0>ih;EJ{s z0%TsCz=mpL&u?(eCmlF8M2bbC!)6|!ruMzM@j2;Bszj8De0bnzz(G!HC4&$V?ESX;Tf$)<&%8Bc& z2Go2gspY4uE@>$7hIH4)ji7zE98az)7Cj0CNiJ~d`PqSJ9ol%iH|3dkr2iGxDBrg< zjoDv1pxPZa%X%yb6O>p+wBlT6!{R*fn8ljrXSz`-sVZ+15Kt%x-|pQR z-iS<-8duK@b*~3D0l>6|W`1lu_s*Z_X-y_EOm_L+pK=I>5j-}(z07fh&M)4(#*cd5 z-H>U7X$cxx+lG~O|C0K#fy#W(c(-u(7LJiIl(iA(g z#>)egN*o-|(#Ndi#ZN;7)np$8a?q`afav>;58-rqA^EY-efq8k2wFSY7(8z?hh`Sv z0L&0~3tHk_^8DodXSDR>Mt8YLnn>loRl!Tb8k#qonu0HDtnZzR5);AH#W^Q@2N!!* zcIE|FeIssQw9kcjql||s8W1?S=@IA+!A{$H0$HUSYVVN2vuVo~m3(pAo3%@}2f&&Y;r>{$;8U+kdzUmS z`uaHQpS61;od-w$GQzdi`Vb(Jh|LX2QyL7~^W2%1W%8d;;`*pNHtEI}rKder<(Qq=5sbP{FhDd{t?%^k8A(twx-{(=r+i7o^qnlyKP*U4?Bl~ zc{!Nmok2*B2^=&5_ z`Q8eQ#wPNZAU!J^YDYfq7oaZkJ;VL4|0i94$gYqng}8;S@2HV=7?R&RYR@O^oc zFxeHhA$UCCIdV_?)xf_iVVIfeRD$~h1`1yvvXc0wn*@F_c30^dNU6jOto3=4Siq z0ojxo!~Zc|R1eLg{LMarnK?feoI58>#?xY9m!9&~8vn2RFG&$1E^T!Cm z_=cZ_aAy-gWbg)Twx7R>T6gQy4c-`Ywmp2y8ZtD$3%%jAtBL%5Gq^78X6@bg_5n@_I=MXwvxzJNXWisnX&K7 zP(qTi4l@QNS;m@%%ox7c40reM@%v-`n(@B2bDj5fopWAK&Y13XbN9bcaF$E$h` zwI}%YuV!>|A;ZDWo#Cfy#+{EqUF%tFpR66^Gv4;u+&fj$`jgrB=SQXTsxt%ph8x-} z{Dd4PtHX-qJKHu}w;#0#nPOPRWPNjw1uk;5s2$e|BZ=mg@NRC!7V7-IdORa zp2tg_K~fA9#%kodJCaQ;v!WWt6jjl4+3Ltcgh$>H6NDvCj0wV{UisT;0CJ%j4#-21 zzUU}7D!EwK_KMA%>Bu^{cDC_`RDY}8oQLAS*ZElBTJBeNw~pnU$eTzQh;em_i(I1k zZLj`62B-|8*dfPgLZ#n8@)I9RK!0~@=*XLh_uR9c5H#parE*Su-vSyt@yP!cf8D$A z=OU6AJO*0z=-*P8F2gAYV&9yj!&>I=fs79_wx=fT;ul58DilTkRe9>+k>*S;h2+V0 zn4M@IO_wZTkT*1mI3_Whyof&RU3D7qwLhtdW@8{TSB0`4v~f3Re?tfaaxQ|Sbf>dH ztjg5i@vB~gkS^*d$1@kTys#JJtUKi@-S>6)u-~p3y{y-s-_`S_gC7*m=-_->Xfma> z;o`_|G}nN5Oio$e|Gwm}M9pJhy9>^{iDQwqp!fwgzCx}^?YqcaiPaZ6gye$$(r|rA z`Cv$~e0v^W{Nk#zf<7yTp)A9eR`mw%P9S^tD$nPhjAK2Y(HLq5isai>Xr@!1`LD+? z2&rT^;GvPtJwLV%!;QG_=w^1ws`cON3gOC~mV3JkB+I5cQ*KV&?zuOgH*n5GePesg zugSBkZ$FawzmN%rahqatROxm*qY*`%O|k+{?2Vv`4`(Jh_cej&M$}!XlT8SV;KZ>1 zoo`7%&=2Pqa(U4;Q7U(ctDa>Sew&px&ygI3rU@kcv(jjV5L9)4yXgL| zxlYsa0qc%BQP&Cg7Rt!VF)!W*>Cej(kcP;u9KZQXX&SBWQ3ijD$lj?gRsJd?*$4|A zrDrrbs&_DBK*OoW3x;_}gc(cyrxd-Ufr7&pDz9vPIrU4LHJq8P?zw*yNhHO=NW}HB z9qs~m`kr;FLP$~C%3^J{qJYu#`L6#RjLCM$>9m1QMJ0Rh1l2@(o%ca7!}oor#6-I@ zP>VqsuDl7 zJY3NE|DU9o1p6Kcbs@CqDd^p6l+PUv3-|@&wF$S+C3H*3+OY4CHfWA-P!j3y#Ws?BGDiq2p*7kar1 z?w#-0^LKht!Q_jBHpaik47HiHjKo63I!ue?ezJ1ZDJMLB#X+qK2dC8?;1g~s5B@^d zSIHc&)1RpJd%6=Isd#zjvB}VUKU)*m@k0y#IC;yZhC$61UP}gkZUiyrFn@w|L`o~`^YTu@;zE9A~dArAavD+;~ z6TQL^vAL!aGkaBpaVe_*OzMr2Z0%?6pfAvY&2#qVX2g%9C9OOI;)w%$+u_E|OxcR- zqgoLjC65_REW3061YFVRkvA2I+!xA=LYCi&vz*IU8!KXC5ve(@wMKlB1=sJmj7`iI zxlU`zQ{45eaF>r0X6-pWBBt99C$L=0I`4fVBh*{z1%9!{C0)ee!L% z{iRrEVXrEqEVz-)YYF>8rL;`#5lBRF!d&vvUWKrRao!;Jklsvt>THJm0>{s;5uEhX zZL67;q70?;uAEKg-#a!$!w|Qm2daJBZfE(?et3mRV+re9lTCeS2QvDKZquR;M`T!84ET91&J;#`zDNPod=q z*lJK8NcJOHJYx;=i_Qr-MD2iu?m+FGYrpMs#VT-$cE@{Zd!Eq%1%ERq6;?LW$8z3v zt5Q;1SZbo+L*$_go|KUs>wcPsE+h`T0dHjRy$#5JHnjY@HWB2ESVl@(bwxb+vaR8h zmHGXrXx6jK(6XYeE?qJ675{kcMzBYnB9{AHYW6LE8)E z*teblupknw-fERXbG%V21BA(b^NZSX*dvh`>iiWk4GBprnT4|@D@{yqQlTJeQlnKQ z6H}GDb1Bfj4sPznE0)<@SbOu-8;vmLOX44G*^wDHcS3qA zYl~kM%#Xqsy)5lc(Ke|cq!!qeDxmE7k3H4?q4gWtIQ*v_}<^3(N}7b z@DT8LW+qy1uDlNFy1Qrv{rE=)|2C}!LVhQ4mRjVr>{CuoE1B$lCf>Jcpw(;nOiZv# zFS38=*dap3mc0GSMpjj_H`P`I*`M5hM7Vl;%hk7=bloR~@lyLReL3JNHBy9)2WVDj4cWw91w<5#gj3CNq`_Uly0Ty1eABEG z_(;3vf$EB>auhqKV~|{LUy>uN=c~PSV)wf`*_PSQAU(r30pb+9nE0AkrFEVAamZD+k~! z+5p;?%;j)Pz&pugsg_m5lM){|4xxI*%(D<(LhYoD%*1Z*3twijik?A&P>7gP&*wxA z$?`PEw3hCZI9@S~FBFIfhQTW{$g!y<XLc`8^Cu2ZEsyZ)b5`*ae;~j>O0s@lu zBc7mi{4>z(!+D0GcQC5kH@O=%GkyW`;*5>yLdti4>E1~#g*#b<^LSY?FPnMbPZG?j zM7k;(kxu$Ux>!*7{G$=@=#nro!xmVhx84-g6d#zh>>Fg@xI1V{AIepjmP%BoY(Q`3 z7_%}=@f z$xd6>j40$)!Xw@$)6vIuj>fAEDwtS5z8iees z*Qp8HaVR+Dpn0BGeeDX zQD5gtQJS_@gnQNA-Y3QL6G8GR4y7Mmn2}95ZFW*G)z1DfAchK#zdz$GvIU!doQ9LJ zC@7>zsV^RI0=G^i{hNmwJ8`KFv+KR;$ckQm#}_t#9Y-B7?|;S zOnzu-d! z9Aa`}PCSPWp<;V38WL}T(-?)bLHWI5y`3Ow+n%o!#4U&D?b{aAqFsaJQ*1B=h7t^C z;s6CuOw+OGGfb7eaM@60=#SZOnVoF49;ol&5Gd2yt!bTQ;j?caXk;$T5IuI|b}%ZD zN05P2Jd=H~V9aCV8pVC55L;o|zmydSAb7@8#~4bHIFLTjd1l`4CF1xZSQ8vHV!aRE zzNsnDGAJ}QxFNxS)ScTH@YKo!+qnzmRB#+sJHs_){4mLySt(>J6X30ZR?f*9%!tK( zF`INbR6NpZxC%AdoL+aLs|J}XUI>AsLZaVqav)+gNlGk$_i@SbTriFZ^bHsC7dJ4mFog1HS`Vl|F0SA`D|hKqjE zpsT}zwGot z7^$C4*AU>N^KpK|+lw9g8of~^JRp*w5A?&qD9(|c>}u}AfUu})`g0`OuUTC#|4mm{ z@?$Z)I3b+t*h%rs4>3~2Ytx~aSMMBEu;DgZ7xfgjD3IorKFqiF8Np={ zQ*{9^fEioJ<+v|M$LBrXGa~xr!Ew|^q5kB&gBAUGTpz^PS<3E}w0NQv{KM)`R|QZ{v8ox& zN*}brATIJsb=(CvcYt7lH~Hh&?EEmP(BCsJ01{h#%V{BJ$;LMxcaY zH0dDS^=F`$ZiHuumotF2qz_xX%1?FeD;Lm9P|aPL_>|Jp#!%wc&|4msLWE}){?wC% z7=R@)cqBQfiKKtcy=>$#R4hxHfwk2Ti$bLH-vRD$QH*K&vyyIa2C0@L54l0HAkfgx zGOsZJl##0WX5SYdB6!EOV|E&R;*k{s=wj4eCL>I7T(MW92}WXZ78}<5xpn9P`!<9W@RMoOdj?Ks?PTt-)|5$$>agZ%9WlWMO~fA~%@h z3Yz=2tYB<*j4my>_32jxHxFv#4yMy6m+V2V1@wH0x|0LzJR}$nW);N9nO!r5YTb+k9&@Vlqb$WWu=JR-B6w@ zTxOiLX~sG0yg9e#j(>yIXcSDb68n7$RAS!)oN|L#e7EI}*=h=y+2cf_di5@h8iWfu zY`Re<9J#_OB9;>FCVQQ=OU4T)`CEDGx=$LlX4r0OR4%>xd>?%WM388~nGY`kV&j5d z#^qgx8fSGj+)boYY360q&E=YEh~|=VMd|OWU&ekrRFg9lddgp;bId&b@!*HIjj^v?44kvS z3yU<>-KmW5bJlm~$E@J{sxd; + +export async function createTask( + { description }: CreateArgs, + context: Context +) { + if (!context.user) { + throw new HttpError(401); + } + + return context.entities.Task.create({ + data: { + description, + user: { connect: { id: context.user.id } }, + }, + }); +} + +type UpdateArgs = Pick; + +export async function updateTask({ id, isDone }: UpdateArgs, context: Context) { + if (!context.user) { + throw new HttpError(401); + } + + return context.entities.Task.updateMany({ + where: { + id, + user: { id: context.user.id }, + }, + data: { isDone }, + }); +} diff --git a/waspc/examples/todo-typescript/src/server/queries.ts b/waspc/examples/todo-typescript/src/server/queries.ts new file mode 100644 index 0000000000..47a90cdf0c --- /dev/null +++ b/waspc/examples/todo-typescript/src/server/queries.ts @@ -0,0 +1,9 @@ +import HttpError from '@wasp/core/HttpError.js'; +import { Context, Task } from './serverTypes' + +export async function getTasks(args: unknown, context: Context): Promise { + if (!context.user) { + throw new HttpError(401); + } + return context.entities.Task.findMany({ where: { user: { id: context.user.id } } }); +}; diff --git a/waspc/examples/todo-typescript/src/server/serverTypes.ts b/waspc/examples/todo-typescript/src/server/serverTypes.ts new file mode 100644 index 0000000000..3f1b705b66 --- /dev/null +++ b/waspc/examples/todo-typescript/src/server/serverTypes.ts @@ -0,0 +1,11 @@ +import { User, Prisma } from '@prisma/client'; + +export { Task } from '@prisma/client'; + +export type Context = { + user: User; + entities: { + Task: Prisma.TaskDelegate<{}>; + User: Prisma.UserDelegate<{}>; + }; +}; diff --git a/waspc/examples/todo-typescript/src/server/tsconfig.json b/waspc/examples/todo-typescript/src/server/tsconfig.json new file mode 100644 index 0000000000..70a79b44ee --- /dev/null +++ b/waspc/examples/todo-typescript/src/server/tsconfig.json @@ -0,0 +1,48 @@ +// =============================== IMPORTANT ================================= +// +// This file is only used for Wasp IDE support. You can change it to configure +// your IDE checks, but none of these options will affect the TypeScript +// compiler. Proper TS compiler configuration in Wasp is coming soon :) +{ + "compilerOptions": { + // Allows default imports. + "esModuleInterop": true, + "allowJs": true, + "strict": true, + // Wasp needs the following settings enable IDE support in your source + // files. Editing them might break features like import autocompletion and + // definition lookup. Don't change them unless you know what you're doing. + // + // The relative path to the generated web app's root directory. This must be + // set to define the "paths" option. + "baseUrl": "../../.wasp/out/server/", + "paths": { + // Resolve all "@wasp" imports to the generated source code. + "@wasp/*": [ + "src/*" + ], + // Resolve all non-relative imports to the correct node module. Source: + // https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping + "*": [ + // Start by looking for the definiton inside the node modules root + // directory... + "node_modules/*", + // ... If that fails, try to find it inside definitely-typed type + // definitions. + "node_modules/@types/*" + ] + }, + // Correctly resolve types: https://www.typescriptlang.org/tsconfig#typeRoots + "typeRoots": [ + "../../.wasp/out/server/node_modules/@types" + ], + // Since this TS config is used only for IDE support and not for + // compilation, the following directory doesn't exist. We need to specify + // it to prevent this error: + // https://stackoverflow.com/questions/42609768/typescript-error-cannot-write-file-because-it-would-overwrite-input-file + "outDir": "phantom", + }, + "exclude": [ + "phantom" + ], +} \ No newline at end of file diff --git a/waspc/examples/todo-typescript/src/shared/tsconfig.json b/waspc/examples/todo-typescript/src/shared/tsconfig.json new file mode 100644 index 0000000000..f78b58a772 --- /dev/null +++ b/waspc/examples/todo-typescript/src/shared/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + // Enable default imports in TypeScript. + "esModuleInterop": true, + "allowJs": true, + // The following settings enable IDE support in user-provided source files. + // Editing them might break features like import autocompletion and + // definition lookup. Don't change them unless you know what you're doing. + // + // The relative path to the generated web app's root directory. This must be + // set to define the "paths" option. + "baseUrl": "../../.wasp/out/server/", + "paths": { + // Resolve all non-relative imports to the correct node module. Source: + // https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping + "*": [ + // Start by looking for the definiton inside the node modules root + // directory... + "node_modules/*", + // ... If that fails, try to find it inside definitely-typed type + // definitions. + "node_modules/@types/*" + ] + }, + // Correctly resolve types: https://www.typescriptlang.org/tsconfig#typeRoots + "typeRoots": ["../../.wasp/out/server/node_modules/@types"], + } +} diff --git a/waspc/examples/todo-typescript/tsconfig.json b/waspc/examples/todo-typescript/tsconfig.json new file mode 100644 index 0000000000..93c79bf3d8 --- /dev/null +++ b/waspc/examples/todo-typescript/tsconfig.json @@ -0,0 +1,28 @@ +// =============================== IMPORTANT ================================= +// +// This file is only used for Wasp IDE support. You can change it to configure +// your IDE checks, but none of these options will affect the TypeScript +// compiler. Proper TS compiler configuration in Wasp is coming soon :) +{ + "compilerOptions": { + // JSX support + "jsx": "preserve", + "strict": true, + // Allow default imports. + "esModuleInterop": true, + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + // Since this TS config is used only for IDE support and not for + // compilation, the following directory doesn't exist. We need to specify + // it to prevent this error: + // https://stackoverflow.com/questions/42609768/typescript-error-cannot-write-file-because-it-would-overwrite-input-file + "outDir": "phantom" + }, + "exclude": [ + "phantom" + ], +} \ No newline at end of file diff --git a/waspc/src/Wasp/Generator/ExternalCodeGenerator/Js.hs b/waspc/src/Wasp/Generator/ExternalCodeGenerator/Js.hs index 3378ca1b01..1cd956a56c 100644 --- a/waspc/src/Wasp/Generator/ExternalCodeGenerator/Js.hs +++ b/waspc/src/Wasp/Generator/ExternalCodeGenerator/Js.hs @@ -18,15 +18,10 @@ import qualified Wasp.Generator.FileDraft as FD import Wasp.Generator.Monad (Generator) genSourceFile :: C.ExternalCodeGeneratorStrategy -> EC.File -> Generator FD.FileDraft -genSourceFile strategy file = return $ FD.createTextFileDraft dstPath text' +genSourceFile strategy file = return $ FD.createTextFileDraft dstPath text where filePathInSrcExtCodeDir = EC.filePathInExtCodeDir file - - filePathInGenExtCodeDir :: Path' (Rel C.GeneratedExternalCodeDir) File' - filePathInGenExtCodeDir = C.castRelPathFromSrcToGenExtCodeDir filePathInSrcExtCodeDir - text = EC.fileText file - text' = C._resolveJsFileWaspImports strategy filePathInGenExtCodeDir text dstPath = C._resolveDstFilePath strategy filePathInSrcExtCodeDir -- | Replaces imports that start with "@wasp/" with imports that start from the src dir of the app. diff --git a/waspc/src/Wasp/Generator/Setup.hs b/waspc/src/Wasp/Generator/Setup.hs index 05ba4912e2..435e31acd0 100644 --- a/waspc/src/Wasp/Generator/Setup.hs +++ b/waspc/src/Wasp/Generator/Setup.hs @@ -28,11 +28,10 @@ runNpmInstallIfNeeded spec dstDir sendMessage = do Right maybeFullStackDeps -> case maybeFullStackDeps of Nothing -> return ([], []) Just fullStackDeps -> do - sendMessage $ Msg.Start "Starting npm install..." - (Left (npmInstallWarnings, npmInstallErrors)) <- - installNpmDependenciesWithInstallRecord fullStackDeps dstDir - `race` reportInstallationProgress reportInstallationProgressMessages + sendMessage $ Msg.Start "Skipping npm install..." + (Left (npmInstallWarnings, npmInstallErrors)) <- installNpmDependenciesWithInstallRecord fullStackDeps dstDir `race` reportInstallationProgress reportInstallationProgressMessages when (null npmInstallErrors) (sendMessage $ Msg.Success "Successfully completed npm install.") + return ([], []) return (npmInstallWarnings, npmInstallErrors) where reportInstallationProgress :: [String] -> IO () From 0d7087f4d6b43675a532f08a9eed21f09c65c7b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Wed, 15 Nov 2023 17:55:25 +0100 Subject: [PATCH 02/54] Add prototype migration script and fix some issues --- .../templates/react-app/vite.config.ts | 3 + waspc/examples/todo-typescript/migrate | 6 + .../node_modules/@wasp/.queries/core.d.ts | 16 +- .../node_modules/@wasp/.queries/core.js | 29 +-- .../node_modules/@wasp/.queries/getTasks.ts | 12 +- .../node_modules/@wasp/api/index.ts | 1 + .../forms/internal/common/LoginSignupForm.tsx | 2 +- .../node_modules/@wasp/auth/helpers/user.ts | 20 +- .../node_modules/@wasp/auth/package.json | 5 +- .../auth/pages/createAuthRequiredPage.jsx | 28 ++- .../node_modules/@wasp/auth/useAuth.ts | 3 + .../node_modules/@wasp/ext-src/serverTypes.ts | 4 +- .../node_modules/@wasp/operations/index.ts | 29 +-- .../node_modules/@wasp/rpc/queryClient.ts | 31 +-- .../node_modules/@wasp/server/_types/index.ts | 6 +- .../@wasp/server/actions/index.ts | 4 +- .../node_modules/@wasp/server/core/auth.js | 179 ++++++++++++------ .../@wasp/server/queries/index.ts | 4 +- .../todo-typescript/package-lock.json | 53 ++++++ waspc/examples/todo-typescript/package.json | 4 + .../todo-typescript/src/client/vite.config.ts | 7 + 21 files changed, 295 insertions(+), 151 deletions(-) create mode 100755 waspc/examples/todo-typescript/migrate create mode 100644 waspc/examples/todo-typescript/src/client/vite.config.ts diff --git a/waspc/data/Generator/templates/react-app/vite.config.ts b/waspc/data/Generator/templates/react-app/vite.config.ts index 8b3650d212..c029d635c0 100644 --- a/waspc/data/Generator/templates/react-app/vite.config.ts +++ b/waspc/data/Generator/templates/react-app/vite.config.ts @@ -5,6 +5,9 @@ import react from '@vitejs/plugin-react-swc' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + optimizeDeps: { + exclude: ['@wasp'] + }, server: { port: 3000, host: '0.0.0.0', diff --git a/waspc/examples/todo-typescript/migrate b/waspc/examples/todo-typescript/migrate new file mode 100755 index 0000000000..01bdfb532a --- /dev/null +++ b/waspc/examples/todo-typescript/migrate @@ -0,0 +1,6 @@ +rsync -a .wasp/out/web-app/node_modules/ node_modules/ +rsync -a .wasp/out/server/node_modules/ node_modules/ +rsync -a node_modules_wasp/ node_modules +rw db migrate-dev +fd . '.wasp/out/server/node_modules' | grep -v prisma -i | xargs rm -r +rm -r .wasp/out/web-app/node_modules diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.d.ts b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.d.ts index e1bdbe4783..0b6da79944 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.d.ts +++ b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.d.ts @@ -1,11 +1,11 @@ -import { type Query } from '.' -import { Route } from '../types'; -import type { Expand, _Awaited, _ReturnType } from '../universal/types' +import { type Query } from "."; +import { Route } from "@wasp/types"; +import type { Expand, _Awaited, _ReturnType } from "@wasp/universal/types"; export function createQuery( queryRoute: string, entitiesUsed: any[] -): QueryFor +): QueryFor; export function addMetadataToQuery( query: (...args: any[]) => Promise, @@ -13,11 +13,11 @@ export function addMetadataToQuery( relativeQueryPath: string; queryRoute: Route; entitiesUsed: string[]; - }, -): void + } +): void; type QueryFor = Expand< Query[0], _Awaited<_ReturnType>> -> +>; -type GenericBackendQuery = (args: never, context: any) => unknown +type GenericBackendQuery = (args: never, context: any) => unknown; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.js b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.js index 5103db1d8b..00974ffa08 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.js +++ b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.js @@ -1,27 +1,30 @@ -import { callOperation, makeOperationRoute } from '../operations' +import { callOperation, makeOperationRoute } from "@wasp/operations"; import { addResourcesUsedByQuery, getActiveOptimisticUpdates, -} from '../operations/resources' +} from "@wasp/operations/resources"; export function createQuery(relativeQueryPath, entitiesUsed) { - const queryRoute = makeOperationRoute(relativeQueryPath) + const queryRoute = makeOperationRoute(relativeQueryPath); async function query(queryKey, queryArgs) { - const serverResult = await callOperation(queryRoute, queryArgs) + const serverResult = await callOperation(queryRoute, queryArgs); return getActiveOptimisticUpdates(queryKey).reduce( - (result, update) => update(result), - serverResult, - ) + (result, update) => update(result), + serverResult + ); } - addMetadataToQuery(query, { relativeQueryPath, queryRoute, entitiesUsed }) + addMetadataToQuery(query, { relativeQueryPath, queryRoute, entitiesUsed }); - return query + return query; } -export function addMetadataToQuery(query, { relativeQueryPath, queryRoute, entitiesUsed }) { - query.queryCacheKey = [relativeQueryPath] - query.route = queryRoute - addResourcesUsedByQuery(query.queryCacheKey, entitiesUsed) +export function addMetadataToQuery( + query, + { relativeQueryPath, queryRoute, entitiesUsed } +) { + query.queryCacheKey = [relativeQueryPath]; + query.route = queryRoute; + addResourcesUsedByQuery(query.queryCacheKey, entitiesUsed); } diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/getTasks.ts b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/getTasks.ts index 7c1d542ccc..25fc1f2859 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/getTasks.ts +++ b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/getTasks.ts @@ -1,10 +1,6 @@ -import { createQuery } from './core' -import { GetTasks } from '../../../server/src/queries/getTasks' +import { createQuery } from "./core"; +import { GetTasks } from "@wasp/server/queries"; +const query = createQuery("operations/get-tasks", ["Task"]); -const query = createQuery( - 'operations/get-tasks', - ['Task'], -) - -export default query +export default query; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/api/index.ts b/waspc/examples/todo-typescript/node_modules/@wasp/api/index.ts index b0a2dd7f44..56ed992ec5 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/api/index.ts +++ b/waspc/examples/todo-typescript/node_modules/@wasp/api/index.ts @@ -4,6 +4,7 @@ import config from "@wasp/core/config"; import { storage } from "@wasp/core/storage"; import { apiEventsEmitter } from "@wasp/api/events"; +console.log("Top level node module"); const api = axios.create({ baseURL: config.apiUrl, }); diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/common/LoginSignupForm.tsx b/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/common/LoginSignupForm.tsx index 02ee07ec5f..5bfe0cc83c 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/common/LoginSignupForm.tsx +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/common/LoginSignupForm.tsx @@ -1,6 +1,6 @@ import { useContext, type FormEvent } from "react"; import { styled } from "@wasp/core/stitches.config"; -import config from "@wasp/core/config.js"; +import config from "@wasp/core/config"; import { AuthContext } from "../../Auth"; import { diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/helpers/user.ts b/waspc/examples/todo-typescript/node_modules/@wasp/auth/helpers/user.ts index 6c4a5ec2b4..6e2315eccc 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/auth/helpers/user.ts +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/helpers/user.ts @@ -1,14 +1,14 @@ -import { setAuthToken } from '../../api' -import { invalidateAndRemoveQueries } from '@wasp/operations/resources' +import { setAuthToken } from "@wasp/api"; +import { invalidateAndRemoveQueries } from "@wasp/operations/resources"; export async function initSession(token: string): Promise { - setAuthToken(token) - // We need to invalidate queries after login in order to get the correct user - // data in the React components (using `useAuth`). - // Redirects after login won't work properly without this. + setAuthToken(token); + // We need to invalidate queries after login in order to get the correct user + // data in the React components (using `useAuth`). + // Redirects after login won't work properly without this. - // TODO(filip): We are currently removing all the queries, but we should - // remove only non-public, user-dependent queries - public queries are - // expected not to change in respect to the currently logged in user. - await invalidateAndRemoveQueries() + // TODO(filip): We are currently removing all the queries, but we should + // remove only non-public, user-dependent queries - public queries are + // expected not to change in respect to the currently logged in user. + await invalidateAndRemoveQueries(); } diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/package.json b/waspc/examples/todo-typescript/node_modules/@wasp/auth/package.json index 2c943a0ed6..2a1df894d8 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/auth/package.json +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/package.json @@ -1,3 +1,6 @@ { - "name": "auth" + "name": "auth", + "dependencies": { + "@wasp/api": "*" + } } diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/pages/createAuthRequiredPage.jsx b/waspc/examples/todo-typescript/node_modules/@wasp/auth/pages/createAuthRequiredPage.jsx index 621ef393d9..33cb648ed1 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/auth/pages/createAuthRequiredPage.jsx +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/pages/createAuthRequiredPage.jsx @@ -1,30 +1,26 @@ -import React from 'react' - -import { Redirect } from 'react-router-dom' -import useAuth from '../useAuth' +import React from "react"; +import { Redirect } from "react-router-dom"; +import useAuth from "../useAuth"; const createAuthRequiredPage = (Page) => { return (props) => { - const { data: user, isError, isSuccess, isLoading } = useAuth() + const { data: user, isError, isSuccess, isLoading } = useAuth(); if (isSuccess) { if (user) { - return ( - - ) + return ; } else { - return + return ; } } else if (isLoading) { - return Loading... + return Loading...; } else if (isError) { - return An error ocurred. Please refresh the page. + return An error ocurred. Please refresh the page.; } else { - return An unknown error ocurred. Please refresh the page. + return An unknown error ocurred. Please refresh the page.; } - } -} - -export default createAuthRequiredPage + }; +}; +export default createAuthRequiredPage; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/useAuth.ts b/waspc/examples/todo-typescript/node_modules/@wasp/auth/useAuth.ts index c4204bfc8b..78b58f03e5 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/auth/useAuth.ts +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/useAuth.ts @@ -13,13 +13,16 @@ export default function useAuth(queryFnArgs?: unknown, config?: any) { function createUserGetter() { const getMeRelativePath = "auth/me"; + /* filip */ console.log("tu smo"); const getMeRoute = { method: HttpMethod.Get, path: `/${getMeRelativePath}` }; async function getMe(): Promise { try { const response = await api.get(getMeRoute.path); + /* filip */ console.log("uspio"); return superjsonDeserialize(response.data); } catch (error) { + /* filip */ console.log("failao"); if (error.response?.status === 401) { } else { handleApiError(error); diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/serverTypes.ts b/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/serverTypes.ts index 3f1b705b66..33f91956f4 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/serverTypes.ts +++ b/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/serverTypes.ts @@ -1,6 +1,6 @@ -import { User, Prisma } from '@prisma/client'; +import { User, Prisma } from "@prisma/client"; -export { Task } from '@prisma/client'; +export { type Task } from "@prisma/client"; export type Context = { user: User; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/operations/index.ts b/waspc/examples/todo-typescript/node_modules/@wasp/operations/index.ts index 2d4494b050..c2dfee5c6e 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/operations/index.ts +++ b/waspc/examples/todo-typescript/node_modules/@wasp/operations/index.ts @@ -1,22 +1,27 @@ -import api, { handleApiError } from '../api' -import { HttpMethod } from '../types' -import { +import api, { handleApiError } from "@wasp/api"; +import { HttpMethod } from "@wasp/types"; +import { serialize as superjsonSerialize, deserialize as superjsonDeserialize, - } from 'superjson' +} from "superjson"; -export type OperationRoute = { method: HttpMethod, path: string } +export type OperationRoute = { method: HttpMethod; path: string }; -export async function callOperation(operationRoute: OperationRoute & { method: HttpMethod.Post }, args: any) { +export async function callOperation( + operationRoute: OperationRoute & { method: HttpMethod.Post }, + args: any +) { try { - const superjsonArgs = superjsonSerialize(args) - const response = await api.post(operationRoute.path, superjsonArgs) - return superjsonDeserialize(response.data) + const superjsonArgs = superjsonSerialize(args); + const response = await api.post(operationRoute.path, superjsonArgs); + return superjsonDeserialize(response.data); } catch (error) { - handleApiError(error) + handleApiError(error); } } -export function makeOperationRoute(relativeOperationRoute: string): OperationRoute { - return { method: HttpMethod.Post, path: `/${relativeOperationRoute}` } +export function makeOperationRoute( + relativeOperationRoute: string +): OperationRoute { + return { method: HttpMethod.Post, path: `/${relativeOperationRoute}` }; } diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queryClient.ts b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queryClient.ts index 2322109b25..448be4c5ce 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queryClient.ts +++ b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queryClient.ts @@ -1,26 +1,33 @@ -import { QueryClient } from '@tanstack/react-query' +import { QueryClient } from "@tanstack/react-query"; +type QueryClientConfig = object; -type QueryClientConfig = object +const defaultQueryClientConfig = {}; -const defaultQueryClientConfig = {} +let queryClientConfig: QueryClientConfig, + resolveQueryClientInitialized: (...args: any[]) => any, + isQueryClientInitialized: boolean; -let queryClientConfig: QueryClientConfig, resolveQueryClientInitialized: (...args: any[]) => any, isQueryClientInitialized: boolean - -export const queryClientInitialized = new Promise(resolve => { - resolveQueryClientInitialized = resolve -}); +export const queryClientInitialized: Promise = new Promise( + (resolve) => { + resolveQueryClientInitialized = resolve; + } +); export function configureQueryClient(config: QueryClientConfig): void { if (isQueryClientInitialized) { - throw new Error("Attempted to configure the QueryClient after initialization") + throw new Error( + "Attempted to configure the QueryClient after initialization" + ); } - queryClientConfig = config + queryClientConfig = config; } export function initializeQueryClient(): void { - const queryClient = new QueryClient(queryClientConfig ?? defaultQueryClientConfig) + const queryClient = new QueryClient( + queryClientConfig ?? defaultQueryClientConfig + ); isQueryClientInitialized = true; - resolveQueryClientInitialized(queryClient) + resolveQueryClientInitialized(queryClient); } diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/_types/index.ts b/waspc/examples/todo-typescript/node_modules/@wasp/server/_types/index.ts index 9579a5b606..186513b293 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/server/_types/index.ts +++ b/waspc/examples/todo-typescript/node_modules/@wasp/server/_types/index.ts @@ -3,9 +3,9 @@ import { type ParamsDictionary as ExpressParams, type Query as ExpressQuery, } from "express-serve-static-core"; -import { type Expand } from "../../universal/types.js"; -import prisma from "../dbClient.js"; -import { type User } from "../../entities"; +import { type Expand } from "@wasp/universal/types.js"; +import prisma from "@wasp/server/dbClient.js"; +import { type User } from "@wasp/entities"; import { type _Entity } from "./taggedEntities"; import { type Payload } from "./serialization"; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/index.ts b/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/index.ts index 8e1f86e877..c13ec22d34 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/index.ts +++ b/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/index.ts @@ -1,8 +1,8 @@ -import prisma from "../dbClient.js"; +import prisma from "@wasp/server/dbClient.js"; import { updateTask as updateTaskUser, createTask as createTaskUser, -} from "../../ext-src/actions.js"; +} from "@wasp/ext-src/actions.js"; export type UpdateTask = typeof updateTask; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth.js b/waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth.js index 11c884307d..2a02d3c6fe 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth.js +++ b/waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth.js @@ -1,109 +1,162 @@ -import jwt from 'jsonwebtoken' -import SecurePassword from 'secure-password' -import util from 'util' -import { randomInt } from 'node:crypto' +import jwt from "jsonwebtoken"; +import SecurePassword from "secure-password"; +import util from "util"; +import { randomInt } from "node:crypto"; -import prisma from '../dbClient.js' -import { handleRejection } from '../utils.js' -import HttpError from '../core/HttpError.js' -import config from '../config.js' +import prisma from "@wasp/server/dbClient.js"; +import { handleRejection } from "../utils.js"; +import HttpError from "@wasp/core/HttpError.js"; +import config from "../config.js"; -const jwtSign = util.promisify(jwt.sign) -const jwtVerify = util.promisify(jwt.verify) +const jwtSign = util.promisify(jwt.sign); +const jwtVerify = util.promisify(jwt.verify); -const JWT_SECRET = config.auth.jwtSecret +const JWT_SECRET = config.auth.jwtSecret; -export const sign = (id, options) => jwtSign({ id }, JWT_SECRET, options) -export const verify = (token) => jwtVerify(token, JWT_SECRET) +export const sign = (id, options) => jwtSign({ id }, JWT_SECRET, options); +export const verify = (token) => jwtVerify(token, JWT_SECRET); const auth = handleRejection(async (req, res, next) => { - const authHeader = req.get('Authorization') + const authHeader = req.get("Authorization"); if (!authHeader) { // NOTE(matija): for now we let tokenless requests through and make it operation's // responsibility to verify whether the request is authenticated or not. In the future // we will develop our own system at Wasp-level for that. - return next() + return next(); } - if (authHeader.startsWith('Bearer ')) { - const token = authHeader.substring(7, authHeader.length) - req.user = await getUserFromToken(token) + if (authHeader.startsWith("Bearer ")) { + const token = authHeader.substring(7, authHeader.length); + req.user = await getUserFromToken(token); } else { - throwInvalidCredentialsError() + throwInvalidCredentialsError(); } - next() -}) + next(); +}); export async function getUserFromToken(token) { - let userIdFromToken + let userIdFromToken; try { - userIdFromToken = (await verify(token)).id + userIdFromToken = (await verify(token)).id; } catch (error) { - if (['TokenExpiredError', 'JsonWebTokenError', 'NotBeforeError'].includes(error.name)) { - throwInvalidCredentialsError() + if ( + ["TokenExpiredError", "JsonWebTokenError", "NotBeforeError"].includes( + error.name + ) + ) { + throwInvalidCredentialsError(); } else { - throw error + throw error; } } - const user = await prisma.user.findUnique({ where: { id: userIdFromToken } }) + const user = await prisma.user.findUnique({ where: { id: userIdFromToken } }); if (!user) { - throwInvalidCredentialsError() + throwInvalidCredentialsError(); } // TODO: This logic must match the type in types/index.ts (if we remove the // password field from the object here, we must to do the same there). // Ideally, these two things would live in the same place: // https://github.com/wasp-lang/wasp/issues/965 - const { password, ...userView } = user + const { password, ...userView } = user; - return userView + return userView; } -const SP = new SecurePassword() +const SP = new SecurePassword(); export const hashPassword = async (password) => { - const hashedPwdBuffer = await SP.hash(Buffer.from(password)) - return hashedPwdBuffer.toString("base64") -} + const hashedPwdBuffer = await SP.hash(Buffer.from(password)); + return hashedPwdBuffer.toString("base64"); +}; export const verifyPassword = async (hashedPassword, password) => { - const result = await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64")) + const result = await SP.verify( + Buffer.from(password), + Buffer.from(hashedPassword, "base64") + ); if (result !== SecurePassword.VALID) { - throw new Error('Invalid password.') + throw new Error("Invalid password."); } -} +}; -// Generates an unused username that looks similar to "quick-purple-sheep-91231". +// Generates an unused username that looks similar to "quick-purple-sheep-91231". // It generates several options and ensures it picks one that is not currently in use. export function generateAvailableDictionaryUsername() { - const adjectives = ['fuzzy', 'tall', 'short', 'nice', 'happy', 'quick', 'slow', 'good', 'new', 'old', 'first', 'last', 'old', 'young'] - const colors = ['red', 'green', 'blue', 'white', 'black', 'brown', 'purple', 'orange', 'yellow'] - const nouns = ['wasp', 'cat', 'dog', 'lion', 'rabbit', 'duck', 'pig', 'bee', 'goat', 'crab', 'fish', 'chicken', 'horse', 'llama', 'camel', 'sheep'] - - const potentialUsernames = [] + const adjectives = [ + "fuzzy", + "tall", + "short", + "nice", + "happy", + "quick", + "slow", + "good", + "new", + "old", + "first", + "last", + "old", + "young", + ]; + const colors = [ + "red", + "green", + "blue", + "white", + "black", + "brown", + "purple", + "orange", + "yellow", + ]; + const nouns = [ + "wasp", + "cat", + "dog", + "lion", + "rabbit", + "duck", + "pig", + "bee", + "goat", + "crab", + "fish", + "chicken", + "horse", + "llama", + "camel", + "sheep", + ]; + + const potentialUsernames = []; for (let i = 0; i < 10; i++) { - const potentialUsername = `${adjectives[randomInt(adjectives.length)]}-${colors[randomInt(colors.length)]}-${nouns[randomInt(nouns.length)]}-${randomInt(100_000)}` - potentialUsernames.push(potentialUsername) + const potentialUsername = `${adjectives[randomInt(adjectives.length)]}-${ + colors[randomInt(colors.length)] + }-${nouns[randomInt(nouns.length)]}-${randomInt(100_000)}`; + potentialUsernames.push(potentialUsername); } - return findAvailableUsername(potentialUsernames) + return findAvailableUsername(potentialUsernames); } -// Generates an unused username based on an array of username segments and a separator. +// Generates an unused username based on an array of username segments and a separator. // It generates several options and ensures it picks one that is not currently in use. export function generateAvailableUsername(usernameSegments, config) { - const separator = config?.separator || '-' - const baseUsername = usernameSegments.join(separator) + const separator = config?.separator || "-"; + const baseUsername = usernameSegments.join(separator); - const potentialUsernames = [] + const potentialUsernames = []; for (let i = 0; i < 10; i++) { - const potentialUsername = `${baseUsername}${separator}${randomInt(100_000)}` - potentialUsernames.push(potentialUsername) + const potentialUsername = `${baseUsername}${separator}${randomInt( + 100_000 + )}`; + potentialUsernames.push(potentialUsername); } - return findAvailableUsername(potentialUsernames) + return findAvailableUsername(potentialUsernames); } // Checks the database for an unused username from an array provided and returns first. @@ -111,20 +164,24 @@ async function findAvailableUsername(potentialUsernames) { const users = await prisma.user.findMany({ where: { username: { in: potentialUsernames }, - } - }) - const takenUsernames = users.map(user => user.username) - const availableUsernames = potentialUsernames.filter(username => !takenUsernames.includes(username)) + }, + }); + const takenUsernames = users.map((user) => user.username); + const availableUsernames = potentialUsernames.filter( + (username) => !takenUsernames.includes(username) + ); if (availableUsernames.length === 0) { - throw new Error('Unable to generate a unique username. Please contact Wasp.') + throw new Error( + "Unable to generate a unique username. Please contact Wasp." + ); } - return availableUsernames[0] + return availableUsernames[0]; } export function throwInvalidCredentialsError(message) { - throw new HttpError(401, 'Invalid credentials', { message }) + throw new HttpError(401, "Invalid credentials", { message }); } -export default auth +export default auth; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/queries/index.ts b/waspc/examples/todo-typescript/node_modules/@wasp/server/queries/index.ts index f26278bda1..031d2321c9 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/server/queries/index.ts +++ b/waspc/examples/todo-typescript/node_modules/@wasp/server/queries/index.ts @@ -1,5 +1,5 @@ -import prisma from "../dbClient.js"; -import { getTasks as getTasksUser } from "../../ext-src/queries.js"; +import prisma from "@wasp/server/dbClient.js"; +import { getTasks as getTasksUser } from "@wasp/ext-src/queries.js"; export type GetTasks = typeof getTasks; diff --git a/waspc/examples/todo-typescript/package-lock.json b/waspc/examples/todo-typescript/package-lock.json index 83fb0f8781..1f7c759e26 100644 --- a/waspc/examples/todo-typescript/package-lock.json +++ b/waspc/examples/todo-typescript/package-lock.json @@ -5,7 +5,11 @@ "packages": { "": { "dependencies": { + "@prisma/client": "^4.12.0", "vite": "^4.5.0" + }, + "devDependencies": { + "prisma": "^4.12.0" } }, "node_modules/@esbuild/linux-x64": { @@ -22,6 +26,38 @@ "node": ">=12" } }, + "node_modules/@prisma/client": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.12.0.tgz", + "integrity": "sha512-j9/ighfWwux97J2dS15nqhl60tYoH8V0IuSsgZDb6bCFcQD3fXbXmxjYC8GHhIgOk3lB7Pq+8CwElz2MiDpsSg==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines-version": "4.12.0-67.659ef412370fa3b41cd7bf6e94587c1dfb7f67e7" + }, + "engines": { + "node": ">=14.17" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/engines": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.12.0.tgz", + "integrity": "sha512-0alKtnxhNB5hYU+ymESBlGI4b9XrGGSdv7Ud+8TE/fBNOEhIud0XQsAR+TrvUZgS4na5czubiMsODw0TUrgkIA==", + "devOptional": true, + "hasInstallScript": true + }, + "node_modules/@prisma/engines-version": { + "version": "4.12.0-67.659ef412370fa3b41cd7bf6e94587c1dfb7f67e7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.12.0-67.659ef412370fa3b41cd7bf6e94587c1dfb7f67e7.tgz", + "integrity": "sha512-JIHNj5jlXb9mcaJwakM0vpgRYJIAurxTUqM0iX0tfEQA5XLZ9ONkIckkhuAKdAzocZ+80GYg7QSsfpjg7OxbOA==" + }, "node_modules/@types/node": { "version": "18.15.13", "license": "MIT", @@ -122,6 +158,23 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prisma": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.12.0.tgz", + "integrity": "sha512-xqVper4mbwl32BWzLpdznHAYvYDWQQWK2tBfXjdUD397XaveRyAP7SkBZ6kFlIg8kKayF4hvuaVtYwXd9BodAg==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "4.12.0" + }, + "bin": { + "prisma": "build/index.js", + "prisma2": "build/index.js" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/rollup": { "version": "3.29.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", diff --git a/waspc/examples/todo-typescript/package.json b/waspc/examples/todo-typescript/package.json index 0c7717f7b6..6cd0c22699 100644 --- a/waspc/examples/todo-typescript/package.json +++ b/waspc/examples/todo-typescript/package.json @@ -1,5 +1,9 @@ { "dependencies": { + "@prisma/client": "^4.12.0", "vite": "^4.5.0" + }, + "devDependencies": { + "prisma": "^4.12.0" } } diff --git a/waspc/examples/todo-typescript/src/client/vite.config.ts b/waspc/examples/todo-typescript/src/client/vite.config.ts new file mode 100644 index 0000000000..742d3f1024 --- /dev/null +++ b/waspc/examples/todo-typescript/src/client/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + server: { + open: false + } +}) From 6789cd536eec1bdcfaa27dc6ecc01998d186be5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Thu, 16 Nov 2023 16:38:25 +0100 Subject: [PATCH 03/54] Update the todo-ts prototype version to match main --- waspc/examples/todo-typescript/main.wasp | 20 ++- .../node_modules/@wasp/ext-src/actions.ts | 50 +++++-- .../node_modules/@wasp/rpc/actions/index.ts | 20 +-- .../@wasp/server/actions/index.ts | 28 ++-- .../@wasp/server/actions/types.ts | 10 ++ .../todo-typescript/src/client/LoginPage.tsx | 5 +- .../todo-typescript/src/client/Main.css | 22 ++- .../todo-typescript/src/client/MainPage.tsx | 100 +++++++------ .../todo-typescript/src/client/SignupPage.tsx | 13 +- .../todo-typescript/src/client/opcije.md | 132 ------------------ .../todo-typescript/src/client/tsconfig.json | 27 ++++ .../todo-typescript/src/client/types.ts | 6 - .../todo-typescript/src/client/vite.config.ts | 7 - .../todo-typescript/src/server/actions.ts | 51 ++++--- .../todo-typescript/src/server/queries.ts | 18 ++- .../todo-typescript/src/server/serverTypes.ts | 11 -- 16 files changed, 252 insertions(+), 268 deletions(-) delete mode 100644 waspc/examples/todo-typescript/src/client/opcije.md delete mode 100644 waspc/examples/todo-typescript/src/client/types.ts delete mode 100644 waspc/examples/todo-typescript/src/client/vite.config.ts delete mode 100644 waspc/examples/todo-typescript/src/server/serverTypes.ts diff --git a/waspc/examples/todo-typescript/main.wasp b/waspc/examples/todo-typescript/main.wasp index 8f3c274adf..bfb394c34e 100644 --- a/waspc/examples/todo-typescript/main.wasp +++ b/waspc/examples/todo-typescript/main.wasp @@ -7,7 +7,10 @@ app TodoTypescript { auth: { userEntity: User, methods: { - usernameAndPassword: {}, + usernameAndPassword: {}, // this is a very naive implementation, use 'email' in production instead + //google: {}, //https://wasp-lang.dev/docs/integrations/google + //gitHub: {}, //https://wasp-lang.dev/docs/integrations/github + //email: {} //https://wasp-lang.dev/docs/guides/email-auth }, onAuthFailedRedirectTo: "/login", } @@ -24,11 +27,11 @@ entity User {=psl psl=} entity Task {=psl - id Int @id @default(autoincrement()) - description String - isDone Boolean @default(false) - user User? @relation(fields: [userId], references: [id]) - userId Int? + id Int @id @default(autoincrement()) + description String + isDone Boolean @default(false) + user User @relation(fields: [userId], references: [id]) + userId Int psl=} route RootRoute { path: "/", to: MainPage } @@ -66,3 +69,8 @@ action updateTask { fn: import { updateTask } from "@server/actions.js", entities: [Task] } + +action deleteTasks { + fn: import { deleteTasks } from "@server/actions.js", + entities: [Task], +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/actions.ts b/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/actions.ts index 0245af9e77..dbf287d1de 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/actions.ts +++ b/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/actions.ts @@ -1,14 +1,19 @@ -import HttpError from "@wasp/core/HttpError.js"; -import { Context, Task } from "./serverTypes"; +import HttpError from '@wasp/core/HttpError.js' +import type { + CreateTask, + UpdateTask, + DeleteTasks, +} from '@wasp/server/actions/types' +import type { Task } from '@wasp/entities' -type CreateArgs = Pick; +type CreateArgs = Pick -export async function createTask( - { description }: CreateArgs, - context: Context -) { +export const createTask: CreateTask = async ( + { description }, + context +) => { if (!context.user) { - throw new HttpError(401); + throw new HttpError(401) } return context.entities.Task.create({ @@ -16,21 +21,36 @@ export async function createTask( description, user: { connect: { id: context.user.id } }, }, - }); + }) } -type UpdateArgs = Pick; +type UpdateArgs = Pick -export async function updateTask({ id, isDone }: UpdateArgs, context: Context) { +export const updateTask: UpdateTask = async ( + { id, isDone }, + context +) => { if (!context.user) { - throw new HttpError(401); + throw new HttpError(401) } - return context.entities.Task.updateMany({ + return context.entities.Task.update({ where: { id, - user: { id: context.user.id }, }, data: { isDone }, - }); + }) +} + +export const deleteTasks: DeleteTasks = async ( + idsToDelete, + context +) => { + return context.entities.Task.deleteMany({ + where: { + id: { + in: idsToDelete, + }, + }, + }) } diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/index.ts b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/index.ts index c570cfe7b6..12431ca158 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/index.ts +++ b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/index.ts @@ -1,10 +1,14 @@ -import { createAction } from "./core"; -import { CreateTask, UpdateTask } from "@wasp/server/actions"; +import { createAction } from './core' +import { CreateTask, UpdateTask } from '@wasp/server/actions' -export const updateTask = createAction("operations/update-task", [ - "Task", -]); +export const updateTask = createAction('operations/update-task', [ + 'Task', +]) -export const createTask = createAction("operations/create-task", [ - "Task", -]); +export const createTask = createAction('operations/create-task', [ + 'Task', +]) + +export const deleteTasks = createAction('operations/delete-tasks', [ + 'Task', +]) diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/index.ts b/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/index.ts index c13ec22d34..1efb502f0a 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/index.ts +++ b/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/index.ts @@ -1,10 +1,11 @@ -import prisma from "@wasp/server/dbClient.js"; +import prisma from '@wasp/server/dbClient.js' import { updateTask as updateTaskUser, createTask as createTaskUser, -} from "@wasp/ext-src/actions.js"; + deleteTasks as deleteTasksUser, +} from '@wasp/ext-src/actions.js' -export type UpdateTask = typeof updateTask; +export type UpdateTask = typeof updateTask export const updateTask = async (args, context) => { return (updateTaskUser as any)(args, { @@ -12,10 +13,10 @@ export const updateTask = async (args, context) => { entities: { Task: prisma.task, }, - }); -}; + }) +} -export type CreateTask = typeof createTask; +export type CreateTask = typeof createTask export const createTask = async (args, context) => { return (createTaskUser as any)(args, { @@ -23,5 +24,16 @@ export const createTask = async (args, context) => { entities: { Task: prisma.task, }, - }); -}; + }) +} + +export type DeleteTasks = typeof deleteTasks + +export const deleteTasks = async (args, context) => { + return (deleteTasksUser as any)(args, { + ...context, + entities: { + Task: prisma.task, + }, + }) +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/types.ts b/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/types.ts index 38294c6207..8e03d4963e 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/types.ts +++ b/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/types.ts @@ -22,3 +22,13 @@ export type UpdateTask +export type DeleteTasks = + AuthenticatedAction< + [ + _Task, + ], + Input, + Output + > + + diff --git a/waspc/examples/todo-typescript/src/client/LoginPage.tsx b/waspc/examples/todo-typescript/src/client/LoginPage.tsx index c203946d81..98281ca6b0 100644 --- a/waspc/examples/todo-typescript/src/client/LoginPage.tsx +++ b/waspc/examples/todo-typescript/src/client/LoginPage.tsx @@ -4,8 +4,9 @@ import { LoginForm } from "@wasp/auth/forms/Login"; export function LoginPage() { return (
    -

    Login

    - {/** Wasp has built-in auth forms & flows, which you can also opt-out of, if you wish :) */} + {/** Wasp has built-in auth forms & flows, which you can customize or opt-out of, if you wish :) + * https://wasp-lang.dev/docs/guides/auth-ui + */}
    diff --git a/waspc/examples/todo-typescript/src/client/Main.css b/waspc/examples/todo-typescript/src/client/Main.css index 5c086b1dc7..15a61f2399 100644 --- a/waspc/examples/todo-typescript/src/client/Main.css +++ b/waspc/examples/todo-typescript/src/client/Main.css @@ -26,7 +26,7 @@ img { max-height: 100px; } -button { +.logout { margin-top: 1rem; } @@ -43,6 +43,13 @@ code { font-size: 1.2rem; } +.buttons { + display: flex; + flex-direction: row; + width: 300px; + justify-content: space-between; +} + .tasklist { display: flex; flex-direction: column; @@ -50,4 +57,17 @@ code { justify-content: center; width: 300px; margin-top: 1rem; + padding: 0 +} + +li { + width: 100%; +} + +.todo-item { + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; } diff --git a/waspc/examples/todo-typescript/src/client/MainPage.tsx b/waspc/examples/todo-typescript/src/client/MainPage.tsx index 817fbd4878..72d1ff50ed 100644 --- a/waspc/examples/todo-typescript/src/client/MainPage.tsx +++ b/waspc/examples/todo-typescript/src/client/MainPage.tsx @@ -1,36 +1,45 @@ -import "./Main.css"; -import React, { useEffect, FormEventHandler, FormEvent } from "react"; -import logout from "@wasp/auth/logout"; -import useAuth from "@wasp/auth/useAuth"; -import { useQuery, useAction } from "@wasp/rpc"; // Wasp uses a thin wrapper around react-query -import { getTasks } from "@wasp/rpc/queries"; -import { createTask, updateTask } from "@wasp/rpc/actions"; -import waspLogo from "./waspLogo.png"; -import { Task } from "./types"; +import './Main.css' +import React, { useEffect, FormEventHandler, FormEvent } from 'react' +import logout from '@wasp/auth/logout' +import useAuth from '@wasp/auth/useAuth' +import { useQuery, useAction } from '@wasp/rpc' // Wasp uses a thin wrapper around react-query +import { getTasks } from '@wasp/rpc/queries' +import { createTask, updateTask, deleteTasks } from '@wasp/rpc/actions' +import waspLogo from './waspLogo.png' +import { User } from '@wasp/auth/types' -export function MainPage() { - const { data: user } = useAuth(); - const { data: tasks, isLoading, error } = useQuery(getTasks); +export const MainPage = ({ user }: { user: User }) => { + const { data: tasks, isLoading, error } = useQuery(getTasks) - useEffect(() => { - console.log(user); - }, [user]); + if (isLoading) return 'Loading...' + if (error) return 'Error: ' + error - if (isLoading) return "Loading..."; - if (error) return "Error: " + error; + const completed = tasks?.filter((task) => task.isDone).map((task) => task.id) return (
    wasp logo -

    - {user.username} - {`'s tasks :)`} -

    + {user && ( +

    + {user.username} + {`'s tasks :)`} +

    + )} {tasks && } - +
    + + +
    - ); + ) } function Todo({ id, isDone, description }: Task) { @@ -41,54 +50,57 @@ function Todo({ id, isDone, description }: Task) { await updateTask({ id, isDone: event.currentTarget.checked, - }); + }) } catch (err: any) { - window.alert("Error while updating task " + err?.message); + window.alert('Error while updating task ' + err?.message) } - }; + } return (
  • - - {description} + + + {description} + +
  • - ); + ) } function TasksList({ tasks }: { tasks: Task[] }) { - if (tasks.length === 0) return

    No tasks yet.

    ; + if (tasks.length === 0) return

    No tasks yet.

    return (
      {tasks.map((task, idx) => ( ))}
    - ); + ) } function NewTaskForm() { const handleSubmit = async (event: FormEvent) => { - event.preventDefault(); + event.preventDefault() try { - const description = event.currentTarget.description.value; - console.log(description); - event.currentTarget.reset(); - await createTask({ description }); + const description = event.currentTarget.description.value + console.log(description) + event.currentTarget.reset() + await createTask({ description }) } catch (err: any) { - window.alert("Error: " + err?.message); + window.alert('Error: ' + err?.message) } - }; + } return (
    - ); + ) } diff --git a/waspc/examples/todo-typescript/src/client/SignupPage.tsx b/waspc/examples/todo-typescript/src/client/SignupPage.tsx index 11d01c9429..a04b901f0f 100644 --- a/waspc/examples/todo-typescript/src/client/SignupPage.tsx +++ b/waspc/examples/todo-typescript/src/client/SignupPage.tsx @@ -1,16 +1,17 @@ -import { Link } from 'react-router-dom'; -import { SignupForm } from '@wasp/auth/forms/Signup'; +import { Link } from "react-router-dom"; +import { SignupForm } from "@wasp/auth/forms/Signup"; export function SignupPage() { return (
    -

    Sign Up

    - {/** Wasp has built-in auth forms & flows, which you can also opt-out of, if you wish :) */} + {/** Wasp has built-in auth forms & flows, which you can customize or opt-out of, if you wish :) + * https://wasp-lang.dev/docs/guides/auth-ui + */}
    - I already have an account (go to login). + I already have an account (go to login).
    ); -}; +} diff --git a/waspc/examples/todo-typescript/src/client/opcije.md b/waspc/examples/todo-typescript/src/client/opcije.md deleted file mode 100644 index 5dfd11e599..0000000000 --- a/waspc/examples/todo-typescript/src/client/opcije.md +++ /dev/null @@ -1,132 +0,0 @@ -korisnikova definicija queryja: src/queries.js - -Ciljevi: - -- Zelimo li da gtd na frontendu i backendu vodi na implementaciju koju je definirao korisnik (ovo je laz)? -- Zelimo li da tip na frontendu bude tocan tip (tip generiranog rpc poziva, tip koji tocno opisuje runtime fenkcije)? -- Zelimo li da tip na backendu bude tocan tip (tip generiranog dekoratora na backendu, tip koji tocno opisuje runtime funkcije)? -- Zelimo li da su importi isti cak i ako API bude razlicit? - -## Rjesenje 1 - -To mozemo postici tako da: - -- Kazemo korisniku da omota svoju implementaciju queryja u nas hook koji vraca tocan tip -- Ucinimo tip na frontendu istim ko na serveru (izbacimo query cache key) zato sto imamo samo jedno mjesto na kojem mozemo zakacit tip na query, ali dva mjesta koja ga moraju koristit -- note: korisnik bi sada _morao_ omotavati svoje queryje, assignement tipa vise ne bi bio izborna stvar - -## Opcija 1 - -GTD na klijentu pokazuje na generirani rpc poziv na klijentu -GTD na serveru pokazuje na generirani poziv queryja (dekorator) na serveru - -properties: - -- nije potreban post processing da se projekt uspjesno koristi -- import za server i klijent ne smije biti isti string - -## Opcija 2 - -GTD na klijentu pokazuje na korisnikov query u src/queries.js -GTD na serveru pokazuje na korisnikov query u src/queries.js - -properties: - -- potrebno je mijenjanje importa da se projekt koristi - -## Martinova ideja - -Generirani library kod u @wasp/queries/getSomething u runtimeu odlucuje hoce li koristit klijentsku ili serversku implementaciju queryja. -Korisnikov kod i dalje na gtd pokazuje na definiciju queryja koju je korisnik napisao (omotanu u hook) - vjerojatno bismo to postigli pomocu tsconfiga. -U tom bismo slucaju trebali prilagoditi tsconfig prije pokretanja korisnikovog koda u Wasp datoteci. -Mozemo izbjec runtime odlucivanje o implementaciji koju koristimo ako znamo koji fileovi su na serveru a koji na klijentu - -file://./MainPage.tsx -http://google.com - -## Najgluplje rjesenje - -- dva importa koji se resolveaju u razlicite lokacije za server i za klijent (tipovi su razliciti) -- svaki ide na svoju implementaciju u node_modules SDK kodu -- jedini nacin da ovdje dobijemo GTD je pisanje TS LS plugina koji ce importe - resolveat na Wasp file uz tocne tipove, sto je cudno jer iz typescripta vodimo - na Wasp (al ok, radili smo mi i gore stvar) -- istrazit jel se moze napravit typescript lsp plugin - -## Sljedece najgluplje rjesenje - -- Importi se resolveaju u user kod na isti file koji je napisao korisnik, to postignemo s konfiguarcijom projekta (path resolution/aliases) -- Mozemo odlucit zelimo li iste ili razlicite import pathove (vjerojatno razlicite jer ljude zbunjuje) -- Mogli bismo imat tocne relativne import pathove (import getTask from - '../queries.ts'). Ovo zadnje bi znacilo da svoj _pravi_ kod ne moze nikak - importat, a to nam je predivlje. -- Tipovi moraju bit jednaki. Odaberemo tip tako da wrapper (koji bismo morali dodat) njega ima kao povratnu vrijednost. -- Problem kod servera je sto operacije nekad trebaju korisnika kojeg ne mogu same znati. -- U tom slucaju, bolje je da na serveru imamo pogresan runtime tip nego na - klijentu. Normalan poziv operacije koja ovisi o Useru na serveru bi bacio runtime - exception, a mogli bismo ju pozvati s nekim dodatnim "hookom" - ```typescript - getTask.callQuery(user); - callQuery(getTask, user); - getTask.withUser(user); - getTask.withContext(context); - ``` - -```typescript -const getTask = defineQuery("getTask", () => {}); -``` - -## Ideje - -- Treba li nam znanje o tome koji su fileovi na serveru ili klijentu -- mozda ne trebamo uopce generirati istinite (generirane) implementacije queryja - i akcija u library kodu (ako gtd pokazuje na korisnikov), a u generiranom kodu - (.wasp/out) presretnemo rezoluciju drugim node_modules/@wasp folderom koji - sadrzi tocne implementacije (jedan u .wasp/out/server, drugi u - .wasp/out/web-app) - -## Sastanak u srijedu - -```typescript -type Args = string; -type Context = object; // sadrzi usera -type Task = object; - -// client - -import getTask from "@wasp/queries/getTask"; - -// opcija 1 -// ovo ne odgovara stvarnom tipu u runtimeu -getTask: (args: Args, context: Context) => Task; - -// opcija 2 -// ovo odgovara stvarnom tipu u runtimeu -getTask: (args: Args) => Task; - -// opcija 3 -// ovo odgovara stvarnom tipu u runtimeu -getTask: (args: Args) => Task; - -// server - -import getTask from "@wasp/queries/getTask"; - -// opcija 1 -// ovo odgovara stvarnom tipu u runtimeu -getTask: (args: Args, context: Context) => Task; - -// opcija 2 -// ovo ne odgovara stvarnom tipu u runtimeu -getTask: (args: Args) => Task; - -// opcija 3 -// ovo ne odgovara stvarnom tipu u runtimeu -// ali baca exception jer nema konteksta -getTask: (args: Args) => Task; - -// kak cemo ju koristit -getTask.callQuery(); -callQuery(getTask); -getTask.withUser(); -``` diff --git a/waspc/examples/todo-typescript/src/client/tsconfig.json b/waspc/examples/todo-typescript/src/client/tsconfig.json index 93c79bf3d8..d501a4193a 100644 --- a/waspc/examples/todo-typescript/src/client/tsconfig.json +++ b/waspc/examples/todo-typescript/src/client/tsconfig.json @@ -16,6 +16,33 @@ "esnext" ], "allowJs": true, + // Wasp needs the following settings enable IDE support in your source + // files. Editing them might break features like import autocompletion and + // definition lookup. Don't change them unless you know what you're doing. + // + // The relative path to the generated web app's root directory. This must be + // set to define the "paths" option. + "baseUrl": "../../.wasp/out/web-app/", + "paths": { + // Resolve all "@wasp" imports to the generated source code. + "@wasp/*": [ + "src/*" + ], + // Resolve all non-relative imports to the correct node module. Source: + // https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping + "*": [ + // Start by looking for the definiton inside the node modules root + // directory... + "node_modules/*", + // ... If that fails, try to find it inside definitely-typed type + // definitions. + "node_modules/@types/*" + ] + }, + // Correctly resolve types: https://www.typescriptlang.org/tsconfig#typeRoots + "typeRoots": [ + "../../.wasp/out/web-app/node_modules/@types" + ], // Since this TS config is used only for IDE support and not for // compilation, the following directory doesn't exist. We need to specify // it to prevent this error: diff --git a/waspc/examples/todo-typescript/src/client/types.ts b/waspc/examples/todo-typescript/src/client/types.ts deleted file mode 100644 index 7739835bb7..0000000000 --- a/waspc/examples/todo-typescript/src/client/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type Task = { - id: number; - description: string; - isDone: boolean; - userId: number | null; -}; diff --git a/waspc/examples/todo-typescript/src/client/vite.config.ts b/waspc/examples/todo-typescript/src/client/vite.config.ts deleted file mode 100644 index 742d3f1024..0000000000 --- a/waspc/examples/todo-typescript/src/client/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vite' - -export default defineConfig({ - server: { - open: false - } -}) diff --git a/waspc/examples/todo-typescript/src/server/actions.ts b/waspc/examples/todo-typescript/src/server/actions.ts index c3703c7ae0..dbf287d1de 100644 --- a/waspc/examples/todo-typescript/src/server/actions.ts +++ b/waspc/examples/todo-typescript/src/server/actions.ts @@ -1,15 +1,19 @@ -import HttpError from "@wasp/core/HttpError.js"; -import { Context, Task } from "./serverTypes"; -import { getTasks } from "@wasp/server/queries"; +import HttpError from '@wasp/core/HttpError.js' +import type { + CreateTask, + UpdateTask, + DeleteTasks, +} from '@wasp/server/actions/types' +import type { Task } from '@wasp/entities' -type CreateArgs = Pick; +type CreateArgs = Pick -export async function createTask( - { description }: CreateArgs, - context: Context -) { +export const createTask: CreateTask = async ( + { description }, + context +) => { if (!context.user) { - throw new HttpError(401); + throw new HttpError(401) } return context.entities.Task.create({ @@ -17,21 +21,36 @@ export async function createTask( description, user: { connect: { id: context.user.id } }, }, - }); + }) } -type UpdateArgs = Pick; +type UpdateArgs = Pick -export async function updateTask({ id, isDone }: UpdateArgs, context: Context) { +export const updateTask: UpdateTask = async ( + { id, isDone }, + context +) => { if (!context.user) { - throw new HttpError(401); + throw new HttpError(401) } - return context.entities.Task.updateMany({ + return context.entities.Task.update({ where: { id, - user: { id: context.user.id }, }, data: { isDone }, - }); + }) +} + +export const deleteTasks: DeleteTasks = async ( + idsToDelete, + context +) => { + return context.entities.Task.deleteMany({ + where: { + id: { + in: idsToDelete, + }, + }, + }) } diff --git a/waspc/examples/todo-typescript/src/server/queries.ts b/waspc/examples/todo-typescript/src/server/queries.ts index 47a90cdf0c..c67a2a0baa 100644 --- a/waspc/examples/todo-typescript/src/server/queries.ts +++ b/waspc/examples/todo-typescript/src/server/queries.ts @@ -1,9 +1,15 @@ -import HttpError from '@wasp/core/HttpError.js'; -import { Context, Task } from './serverTypes' +import HttpError from '@wasp/core/HttpError.js' +import type { GetTasks } from '@wasp/server/queries/types' +import type { Task } from '@wasp/entities' -export async function getTasks(args: unknown, context: Context): Promise { +//Using TypeScript's new 'satisfies' keyword, it will infer the types of the arguments and return value +export const getTasks = ((_args, context) => { if (!context.user) { - throw new HttpError(401); + throw new HttpError(401) } - return context.entities.Task.findMany({ where: { user: { id: context.user.id } } }); -}; + + return context.entities.Task.findMany({ + where: { user: { id: context.user.id } }, + orderBy: { id: 'asc' }, + }) +}) satisfies GetTasks diff --git a/waspc/examples/todo-typescript/src/server/serverTypes.ts b/waspc/examples/todo-typescript/src/server/serverTypes.ts deleted file mode 100644 index 3f1b705b66..0000000000 --- a/waspc/examples/todo-typescript/src/server/serverTypes.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { User, Prisma } from '@prisma/client'; - -export { Task } from '@prisma/client'; - -export type Context = { - user: User; - entities: { - Task: Prisma.TaskDelegate<{}>; - User: Prisma.UserDelegate<{}>; - }; -}; From eb338c60abcaa618ef61d37b589e82453fedac93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Fri, 17 Nov 2023 15:29:42 +0100 Subject: [PATCH 04/54] Switch user files to the new (vertical) structure in prototype --- waspc/cli/src/Wasp/Cli/Common.hs | 6 +- .../templates/basic/src/client/vite-env.d.ts | 2 +- .../templates/react-app/tsconfig.json | 12 +++- .../Generator/templates/server/tsconfig.json | 16 ++++- waspc/examples/todo-typescript/main.wasp | 12 ++-- waspc/examples/todo-typescript/migrate | 2 +- .../node_modules/@wasp/core/package.json | 2 +- .../node_modules/@wasp/ext-src/actions.ts | 2 +- .../node_modules/@wasp/ext-src/queries.ts | 19 +++--- .../@wasp/server/queries/index.ts | 2 +- .../todo-typescript/src/{client => }/Main.css | 0 .../src/{client => }/MainPage.tsx | 2 +- .../todo-typescript/src/client/tsconfig.json | 55 ------------------ .../todo-typescript/src/client/vite-env.d.ts | 1 - .../todo-typescript/src/server/tsconfig.json | 48 --------------- .../todo-typescript/src/shared/tsconfig.json | 28 --------- .../src/{server => task}/actions.ts | 2 +- .../src/{server => task}/queries.ts | 2 +- .../src/{client => user}/LoginPage.tsx | 0 .../src/{client => user}/SignupPage.tsx | 0 .../todo-typescript/src/vite-env.d.ts | 1 + .../src/{client => }/waspLogo.png | Bin 22 files changed, 50 insertions(+), 164 deletions(-) rename waspc/examples/todo-typescript/src/{client => }/Main.css (100%) rename waspc/examples/todo-typescript/src/{client => }/MainPage.tsx (98%) delete mode 100644 waspc/examples/todo-typescript/src/client/tsconfig.json delete mode 100644 waspc/examples/todo-typescript/src/client/vite-env.d.ts delete mode 100644 waspc/examples/todo-typescript/src/server/tsconfig.json delete mode 100644 waspc/examples/todo-typescript/src/shared/tsconfig.json rename waspc/examples/todo-typescript/src/{server => task}/actions.ts (95%) rename waspc/examples/todo-typescript/src/{server => task}/queries.ts (90%) rename waspc/examples/todo-typescript/src/{client => user}/LoginPage.tsx (100%) rename waspc/examples/todo-typescript/src/{client => user}/SignupPage.tsx (100%) create mode 100644 waspc/examples/todo-typescript/src/vite-env.d.ts rename waspc/examples/todo-typescript/src/{client => }/waspLogo.png (100%) diff --git a/waspc/cli/src/Wasp/Cli/Common.hs b/waspc/cli/src/Wasp/Cli/Common.hs index 282cf40dfe..fe4a66858c 100644 --- a/waspc/cli/src/Wasp/Cli/Common.hs +++ b/waspc/cli/src/Wasp/Cli/Common.hs @@ -44,13 +44,13 @@ dotWaspInfoFileInGeneratedCodeDir :: Path' (Rel Wasp.Generator.Common.ProjectRoo dotWaspInfoFileInGeneratedCodeDir = [relfile|.waspinfo|] extServerCodeDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir SourceExternalCodeDir) -extServerCodeDirInWaspProjectDir = [reldir|src/server|] +extServerCodeDirInWaspProjectDir = [reldir|src|] extClientCodeDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir SourceExternalCodeDir) -extClientCodeDirInWaspProjectDir = [reldir|src/client|] +extClientCodeDirInWaspProjectDir = [reldir|src|] extSharedCodeDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir SourceExternalCodeDir) -extSharedCodeDirInWaspProjectDir = [reldir|src/shared|] +extSharedCodeDirInWaspProjectDir = [reldir|src|] waspSays :: String -> IO () waspSays what = putStrLn $ Term.applyStyles [Term.Yellow] what diff --git a/waspc/data/Cli/templates/basic/src/client/vite-env.d.ts b/waspc/data/Cli/templates/basic/src/client/vite-env.d.ts index 1623b9c79c..11f02fe2a0 100644 --- a/waspc/data/Cli/templates/basic/src/client/vite-env.d.ts +++ b/waspc/data/Cli/templates/basic/src/client/vite-env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/waspc/data/Generator/templates/react-app/tsconfig.json b/waspc/data/Generator/templates/react-app/tsconfig.json index 968a1bb47f..263338d1ce 100644 --- a/waspc/data/Generator/templates/react-app/tsconfig.json +++ b/waspc/data/Generator/templates/react-app/tsconfig.json @@ -8,6 +8,12 @@ // Allow importing pages with the .tsx extension. "allowImportingTsExtensions": true }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} + "include": [ + "src" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} \ No newline at end of file diff --git a/waspc/data/Generator/templates/server/tsconfig.json b/waspc/data/Generator/templates/server/tsconfig.json index 8999362fee..92e317e783 100644 --- a/waspc/data/Generator/templates/server/tsconfig.json +++ b/waspc/data/Generator/templates/server/tsconfig.json @@ -2,6 +2,15 @@ "extends": "@tsconfig/node18/tsconfig.json", "compilerOptions": { // Overriding this until we implement more complete TypeScript support. + // Filip: begin client file hacks + // We need this to make server work with copied client files (we copy everything) + "jsx": "preserve", + "lib": [ + "esnext", + "dom", + "DOM.Iterable" + ], + // Filip: end client file hacks "strict": false, // Overriding this because we want to use top-level await "module": "esnext", @@ -10,12 +19,13 @@ "sourceMap": true, // The remaining settings should match node18/tsconfig.json, but I kept // them here to be explicit. - // Enable default imports in TypeScript. "esModuleInterop": true, "moduleResolution": "node", "outDir": "dist", "allowJs": true }, - "include": ["src"] -} + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/waspc/examples/todo-typescript/main.wasp b/waspc/examples/todo-typescript/main.wasp index bfb394c34e..e960f2bb93 100644 --- a/waspc/examples/todo-typescript/main.wasp +++ b/waspc/examples/todo-typescript/main.wasp @@ -42,35 +42,35 @@ page MainPage { route LoginRoute { path: "/login", to: LoginPage } page LoginPage { - component: import { LoginPage } from "@client/LoginPage.tsx" + component: import { LoginPage } from "@client/user/LoginPage.tsx" } route SignupRoute { path: "/signup", to: SignupPage } page SignupPage { - component: import { SignupPage } from "@client/SignupPage.tsx" + component: import { SignupPage } from "@client/user/SignupPage.tsx" } query getTasks { // We specify the JS implementation of our query (which is an async JS function) // Even if you use TS and have a queries.ts file, you will still need to import it using the .js extension. // see here for more info: https://wasp-lang.dev/docs/tutorials/todo-app/03-listing-tasks#wasp-declaration - fn: import { getTasks } from "@server/queries.js", + fn: import { getTasks } from "@server/task/queries.js", // We tell Wasp that this query is doing something with the `Task` entity. With that, Wasp will // automatically refresh the results of this query when tasks change. entities: [Task] } action createTask { - fn: import { createTask } from "@server/actions.js", + fn: import { createTask } from "@server/task/actions.js", entities: [Task] } action updateTask { - fn: import { updateTask } from "@server/actions.js", + fn: import { updateTask } from "@server/task/actions.js", entities: [Task] } action deleteTasks { - fn: import { deleteTasks } from "@server/actions.js", + fn: import { deleteTasks } from "@server/task/actions.js", entities: [Task], } diff --git a/waspc/examples/todo-typescript/migrate b/waspc/examples/todo-typescript/migrate index 01bdfb532a..d583bfa6e3 100755 --- a/waspc/examples/todo-typescript/migrate +++ b/waspc/examples/todo-typescript/migrate @@ -1,6 +1,6 @@ rsync -a .wasp/out/web-app/node_modules/ node_modules/ rsync -a .wasp/out/server/node_modules/ node_modules/ -rsync -a node_modules_wasp/ node_modules +# rsync -a node_modules_wasp/ node_modules rw db migrate-dev fd . '.wasp/out/server/node_modules' | grep -v prisma -i | xargs rm -r rm -r .wasp/out/web-app/node_modules diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/core/package.json b/waspc/examples/todo-typescript/node_modules/@wasp/core/package.json index bebae82718..b1c9828d73 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/core/package.json +++ b/waspc/examples/todo-typescript/node_modules/@wasp/core/package.json @@ -8,7 +8,7 @@ "types": "tsc --declaration --emitDeclarationOnly --stripInternal --declarationDir dist" }, "exports": { - "./HttpError.js": "./HttpError.js", + "./HttpError": "./HttpError.js", "./config": "./config.js", "./stitches.config": "./stitches.config.js", "./storage": "./storage.ts" diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/actions.ts b/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/actions.ts index dbf287d1de..9b164b7fb0 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/actions.ts +++ b/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/actions.ts @@ -1,4 +1,4 @@ -import HttpError from '@wasp/core/HttpError.js' +import HttpError from '@wasp/core/HttpError' import type { CreateTask, UpdateTask, diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/queries.ts b/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/queries.ts index e61bd737f9..a86ad1c3ee 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/queries.ts +++ b/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/queries.ts @@ -1,14 +1,15 @@ -import HttpError from "@wasp/core/HttpError.js"; -import { Context, Task } from "./serverTypes"; +import HttpError from '@wasp/core/HttpError' +import type { GetTasks } from '@wasp/server/queries/types' +import type { Task } from '@wasp/entities' -export async function getTasks( - args: unknown, - context: Context -): Promise { +//Using TypeScript's new 'satisfies' keyword, it will infer the types of the arguments and return value +export const getTasks = ((_args, context) => { if (!context.user) { - throw new HttpError(401); + throw new HttpError(401) } + return context.entities.Task.findMany({ where: { user: { id: context.user.id } }, - }); -} + orderBy: { id: 'asc' }, + }) +}) satisfies GetTasks diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/queries/index.ts b/waspc/examples/todo-typescript/node_modules/@wasp/server/queries/index.ts index 031d2321c9..ad120b0991 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/server/queries/index.ts +++ b/waspc/examples/todo-typescript/node_modules/@wasp/server/queries/index.ts @@ -1,7 +1,7 @@ import prisma from "@wasp/server/dbClient.js"; import { getTasks as getTasksUser } from "@wasp/ext-src/queries.js"; -export type GetTasks = typeof getTasks; +export type GetTasks = typeof getTasksUser; export const getTasks = async (args, context) => { return (getTasksUser as any)(args, { diff --git a/waspc/examples/todo-typescript/src/client/Main.css b/waspc/examples/todo-typescript/src/Main.css similarity index 100% rename from waspc/examples/todo-typescript/src/client/Main.css rename to waspc/examples/todo-typescript/src/Main.css diff --git a/waspc/examples/todo-typescript/src/client/MainPage.tsx b/waspc/examples/todo-typescript/src/MainPage.tsx similarity index 98% rename from waspc/examples/todo-typescript/src/client/MainPage.tsx rename to waspc/examples/todo-typescript/src/MainPage.tsx index 72d1ff50ed..085530736c 100644 --- a/waspc/examples/todo-typescript/src/client/MainPage.tsx +++ b/waspc/examples/todo-typescript/src/MainPage.tsx @@ -6,7 +6,7 @@ import { useQuery, useAction } from '@wasp/rpc' // Wasp uses a thin wrapper arou import { getTasks } from '@wasp/rpc/queries' import { createTask, updateTask, deleteTasks } from '@wasp/rpc/actions' import waspLogo from './waspLogo.png' -import { User } from '@wasp/auth/types' +import type { Task, User } from '@wasp/entities' export const MainPage = ({ user }: { user: User }) => { const { data: tasks, isLoading, error } = useQuery(getTasks) diff --git a/waspc/examples/todo-typescript/src/client/tsconfig.json b/waspc/examples/todo-typescript/src/client/tsconfig.json deleted file mode 100644 index d501a4193a..0000000000 --- a/waspc/examples/todo-typescript/src/client/tsconfig.json +++ /dev/null @@ -1,55 +0,0 @@ -// =============================== IMPORTANT ================================= -// -// This file is only used for Wasp IDE support. You can change it to configure -// your IDE checks, but none of these options will affect the TypeScript -// compiler. Proper TS compiler configuration in Wasp is coming soon :) -{ - "compilerOptions": { - // JSX support - "jsx": "preserve", - "strict": true, - // Allow default imports. - "esModuleInterop": true, - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "allowJs": true, - // Wasp needs the following settings enable IDE support in your source - // files. Editing them might break features like import autocompletion and - // definition lookup. Don't change them unless you know what you're doing. - // - // The relative path to the generated web app's root directory. This must be - // set to define the "paths" option. - "baseUrl": "../../.wasp/out/web-app/", - "paths": { - // Resolve all "@wasp" imports to the generated source code. - "@wasp/*": [ - "src/*" - ], - // Resolve all non-relative imports to the correct node module. Source: - // https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping - "*": [ - // Start by looking for the definiton inside the node modules root - // directory... - "node_modules/*", - // ... If that fails, try to find it inside definitely-typed type - // definitions. - "node_modules/@types/*" - ] - }, - // Correctly resolve types: https://www.typescriptlang.org/tsconfig#typeRoots - "typeRoots": [ - "../../.wasp/out/web-app/node_modules/@types" - ], - // Since this TS config is used only for IDE support and not for - // compilation, the following directory doesn't exist. We need to specify - // it to prevent this error: - // https://stackoverflow.com/questions/42609768/typescript-error-cannot-write-file-because-it-would-overwrite-input-file - "outDir": "phantom" - }, - "exclude": [ - "phantom" - ], -} \ No newline at end of file diff --git a/waspc/examples/todo-typescript/src/client/vite-env.d.ts b/waspc/examples/todo-typescript/src/client/vite-env.d.ts deleted file mode 100644 index 1623b9c79c..0000000000 --- a/waspc/examples/todo-typescript/src/client/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/waspc/examples/todo-typescript/src/server/tsconfig.json b/waspc/examples/todo-typescript/src/server/tsconfig.json deleted file mode 100644 index 70a79b44ee..0000000000 --- a/waspc/examples/todo-typescript/src/server/tsconfig.json +++ /dev/null @@ -1,48 +0,0 @@ -// =============================== IMPORTANT ================================= -// -// This file is only used for Wasp IDE support. You can change it to configure -// your IDE checks, but none of these options will affect the TypeScript -// compiler. Proper TS compiler configuration in Wasp is coming soon :) -{ - "compilerOptions": { - // Allows default imports. - "esModuleInterop": true, - "allowJs": true, - "strict": true, - // Wasp needs the following settings enable IDE support in your source - // files. Editing them might break features like import autocompletion and - // definition lookup. Don't change them unless you know what you're doing. - // - // The relative path to the generated web app's root directory. This must be - // set to define the "paths" option. - "baseUrl": "../../.wasp/out/server/", - "paths": { - // Resolve all "@wasp" imports to the generated source code. - "@wasp/*": [ - "src/*" - ], - // Resolve all non-relative imports to the correct node module. Source: - // https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping - "*": [ - // Start by looking for the definiton inside the node modules root - // directory... - "node_modules/*", - // ... If that fails, try to find it inside definitely-typed type - // definitions. - "node_modules/@types/*" - ] - }, - // Correctly resolve types: https://www.typescriptlang.org/tsconfig#typeRoots - "typeRoots": [ - "../../.wasp/out/server/node_modules/@types" - ], - // Since this TS config is used only for IDE support and not for - // compilation, the following directory doesn't exist. We need to specify - // it to prevent this error: - // https://stackoverflow.com/questions/42609768/typescript-error-cannot-write-file-because-it-would-overwrite-input-file - "outDir": "phantom", - }, - "exclude": [ - "phantom" - ], -} \ No newline at end of file diff --git a/waspc/examples/todo-typescript/src/shared/tsconfig.json b/waspc/examples/todo-typescript/src/shared/tsconfig.json deleted file mode 100644 index f78b58a772..0000000000 --- a/waspc/examples/todo-typescript/src/shared/tsconfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "compilerOptions": { - // Enable default imports in TypeScript. - "esModuleInterop": true, - "allowJs": true, - // The following settings enable IDE support in user-provided source files. - // Editing them might break features like import autocompletion and - // definition lookup. Don't change them unless you know what you're doing. - // - // The relative path to the generated web app's root directory. This must be - // set to define the "paths" option. - "baseUrl": "../../.wasp/out/server/", - "paths": { - // Resolve all non-relative imports to the correct node module. Source: - // https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping - "*": [ - // Start by looking for the definiton inside the node modules root - // directory... - "node_modules/*", - // ... If that fails, try to find it inside definitely-typed type - // definitions. - "node_modules/@types/*" - ] - }, - // Correctly resolve types: https://www.typescriptlang.org/tsconfig#typeRoots - "typeRoots": ["../../.wasp/out/server/node_modules/@types"], - } -} diff --git a/waspc/examples/todo-typescript/src/server/actions.ts b/waspc/examples/todo-typescript/src/task/actions.ts similarity index 95% rename from waspc/examples/todo-typescript/src/server/actions.ts rename to waspc/examples/todo-typescript/src/task/actions.ts index dbf287d1de..9b164b7fb0 100644 --- a/waspc/examples/todo-typescript/src/server/actions.ts +++ b/waspc/examples/todo-typescript/src/task/actions.ts @@ -1,4 +1,4 @@ -import HttpError from '@wasp/core/HttpError.js' +import HttpError from '@wasp/core/HttpError' import type { CreateTask, UpdateTask, diff --git a/waspc/examples/todo-typescript/src/server/queries.ts b/waspc/examples/todo-typescript/src/task/queries.ts similarity index 90% rename from waspc/examples/todo-typescript/src/server/queries.ts rename to waspc/examples/todo-typescript/src/task/queries.ts index c67a2a0baa..a86ad1c3ee 100644 --- a/waspc/examples/todo-typescript/src/server/queries.ts +++ b/waspc/examples/todo-typescript/src/task/queries.ts @@ -1,4 +1,4 @@ -import HttpError from '@wasp/core/HttpError.js' +import HttpError from '@wasp/core/HttpError' import type { GetTasks } from '@wasp/server/queries/types' import type { Task } from '@wasp/entities' diff --git a/waspc/examples/todo-typescript/src/client/LoginPage.tsx b/waspc/examples/todo-typescript/src/user/LoginPage.tsx similarity index 100% rename from waspc/examples/todo-typescript/src/client/LoginPage.tsx rename to waspc/examples/todo-typescript/src/user/LoginPage.tsx diff --git a/waspc/examples/todo-typescript/src/client/SignupPage.tsx b/waspc/examples/todo-typescript/src/user/SignupPage.tsx similarity index 100% rename from waspc/examples/todo-typescript/src/client/SignupPage.tsx rename to waspc/examples/todo-typescript/src/user/SignupPage.tsx diff --git a/waspc/examples/todo-typescript/src/vite-env.d.ts b/waspc/examples/todo-typescript/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/waspc/examples/todo-typescript/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/waspc/examples/todo-typescript/src/client/waspLogo.png b/waspc/examples/todo-typescript/src/waspLogo.png similarity index 100% rename from waspc/examples/todo-typescript/src/client/waspLogo.png rename to waspc/examples/todo-typescript/src/waspLogo.png From 35c9efae62e4b0cd100653473c8bdd60b449cb5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Tue, 21 Nov 2023 02:34:15 +0100 Subject: [PATCH 05/54] Prototype with direct imports works in development --- .../src/client/MainPage.tsx | 60 +++++++++---------- .../Generator/templates/server/nodemon.json | 2 + .../Generator/templates/server/package.json | 3 +- waspc/examples/todo-typescript/main.wasp | 15 ++--- .../Evaluation/TypedExpr/Combinators.hs | 16 +++-- waspc/src/Wasp/Generator/JsImport.hs | 16 +++++ waspc/src/Wasp/Generator/ServerGenerator.hs | 7 +-- .../Generator/ServerGenerator/JsImport.hs | 9 +-- waspc/src/Wasp/Generator/WebAppGenerator.hs | 14 ++--- .../Generator/WebAppGenerator/JsImport.hs | 29 +++++++-- waspc/src/Wasp/JsImport.hs | 23 +++++-- waspc/waspls/src/Wasp/LSP/ExtImport/Path.hs | 1 + 12 files changed, 127 insertions(+), 68 deletions(-) diff --git a/examples/todo-app-in-typescript/src/client/MainPage.tsx b/examples/todo-app-in-typescript/src/client/MainPage.tsx index b16d4242cf..1b7364a246 100644 --- a/examples/todo-app-in-typescript/src/client/MainPage.tsx +++ b/examples/todo-app-in-typescript/src/client/MainPage.tsx @@ -1,23 +1,23 @@ -import React, { FormEventHandler, FormEvent } from "react"; -import waspLogo from "./waspLogo.png"; +import React, { FormEventHandler, FormEvent } from 'react' +import waspLogo from './waspLogo.png' -import "./Main.css"; +import './Main.css' // Wasp imports 🐝 = } -import logout from "@wasp/auth/logout"; -import { useQuery } from "@wasp/queries"; // Wasp uses a thin wrapper around react-query -import getTasks from "@wasp/queries/getTasks"; -import createTask from "@wasp/actions/createTask"; -import updateTask from "@wasp/actions/updateTask"; -import deleteTasks from "@wasp/actions/deleteTasks"; -import type { Task, User } from "@wasp/entities"; +import logout from '@wasp/auth/logout' +import { useQuery } from '@wasp/queries' // Wasp uses a thin wrapper around react-query +import getTasks from '@wasp/queries/getTasks' +import createTask from '@wasp/actions/createTask' +import updateTask from '@wasp/actions/updateTask' +import deleteTasks from '@wasp/actions/deleteTasks' +import type { Task, User } from '@wasp/entities' export const MainPage = ({ user }: { user: User }) => { - const { data: tasks, isLoading, error } = useQuery(getTasks); + const { data: tasks, isLoading, error } = useQuery(getTasks) - if (isLoading) return "Loading..."; - if (error) return "Error: " + error; + if (isLoading) return 'Loading...' + if (error) return 'Error: ' + error - const completed = tasks?.filter((task) => task.isDone).map((task) => task.id); + const completed = tasks?.filter((task) => task.isDone).map((task) => task.id) return (
    @@ -42,8 +42,8 @@ export const MainPage = ({ user }: { user: User }) => {
    - ); -}; + ) +} function Todo({ id, isDone, description }: Task) { const handleIsDoneChange: FormEventHandler = async ( @@ -53,11 +53,11 @@ function Todo({ id, isDone, description }: Task) { await updateTask({ id, isDone: event.currentTarget.checked, - }); + }) } catch (err: any) { - window.alert("Error while updating task " + err?.message); + window.alert('Error while updating task ' + err?.message) } - }; + } return (
  • @@ -72,38 +72,38 @@ function Todo({ id, isDone, description }: Task) {
  • - ); + ) } function TasksList({ tasks }: { tasks: Task[] }) { - if (tasks.length === 0) return

    No tasks yet.

    ; + if (tasks.length === 0) return

    No tasks yet.

    return (
      {tasks.map((task, idx) => ( ))}
    - ); + ) } function NewTaskForm() { const handleSubmit = async (event: FormEvent) => { - event.preventDefault(); + event.preventDefault() try { - const description = event.currentTarget.description.value; - console.log(description); - event.currentTarget.reset(); - await createTask({ description }); + const description = event.currentTarget.description.value + console.log(description) + event.currentTarget.reset() + await createTask({ description }) } catch (err: any) { - window.alert("Error: " + err?.message); + window.alert('Error: ' + err?.message) } - }; + } return (
    - ); + ) } diff --git a/waspc/data/Generator/templates/server/nodemon.json b/waspc/data/Generator/templates/server/nodemon.json index 9ac8c1df77..01fe71701a 100644 --- a/waspc/data/Generator/templates/server/nodemon.json +++ b/waspc/data/Generator/templates/server/nodemon.json @@ -4,7 +4,9 @@ }, "watch": [ "src/", + "../../../src/", ".env" ], + "comment-filip": "We now have to watch ../../../src/ because we're importing client files directly", "ext": "ts,mts,js,mjs,json" } diff --git a/waspc/data/Generator/templates/server/package.json b/waspc/data/Generator/templates/server/package.json index 0b3f962d56..fcdd31a197 100644 --- a/waspc/data/Generator/templates/server/package.json +++ b/waspc/data/Generator/templates/server/package.json @@ -4,9 +4,10 @@ "version": "0.0.0", "private": true, "type": "module", + "comment-filip": "The server.js location changed because we have now included client source files above .wasp/out/server/src." "scripts": { "build": "npx tsc", - "start": "npm run validate-env && NODE_PATH=dist node -r dotenv/config dist/server.js", + "start": "npm run validate-env && NODE_PATH=dist node -r dotenv/config dist/.wasp/out/server/src/server.js", "build-and-start": "npm run build && npm run start", "watch": "nodemon --exec 'npm run build-and-start || exit 1'", "validate-env": "node -r dotenv/config ./scripts/validate-env.mjs", diff --git a/waspc/examples/todo-typescript/main.wasp b/waspc/examples/todo-typescript/main.wasp index e960f2bb93..8bcf70987e 100644 --- a/waspc/examples/todo-typescript/main.wasp +++ b/waspc/examples/todo-typescript/main.wasp @@ -37,40 +37,41 @@ psl=} route RootRoute { path: "/", to: MainPage } page MainPage { authRequired: true, - component: import { MainPage } from "@client/MainPage.tsx" + // todo(filip): LSP features are broken beucase I haven't yet updated LSP to the new structure. + component: import { MainPage } from "@src/MainPage.tsx" } route LoginRoute { path: "/login", to: LoginPage } page LoginPage { - component: import { LoginPage } from "@client/user/LoginPage.tsx" + component: import { LoginPage } from "@src/user/LoginPage.tsx" } route SignupRoute { path: "/signup", to: SignupPage } page SignupPage { - component: import { SignupPage } from "@client/user/SignupPage.tsx" + component: import { SignupPage } from "@src/user/SignupPage.tsx" } query getTasks { // We specify the JS implementation of our query (which is an async JS function) // Even if you use TS and have a queries.ts file, you will still need to import it using the .js extension. // see here for more info: https://wasp-lang.dev/docs/tutorials/todo-app/03-listing-tasks#wasp-declaration - fn: import { getTasks } from "@server/task/queries.js", + fn: import { getTasks } from "@src/task/queries.js", // We tell Wasp that this query is doing something with the `Task` entity. With that, Wasp will // automatically refresh the results of this query when tasks change. entities: [Task] } action createTask { - fn: import { createTask } from "@server/task/actions.js", + fn: import { createTask } from "@src/task/actions.js", entities: [Task] } action updateTask { - fn: import { updateTask } from "@server/task/actions.js", + fn: import { updateTask } from "@src/task/actions.js", entities: [Task] } action deleteTasks { - fn: import { deleteTasks } from "@server/task/actions.js", + fn: import { deleteTasks } from "@src/task/actions.js", entities: [Task], } diff --git a/waspc/src/Wasp/Analyzer/Evaluator/Evaluation/TypedExpr/Combinators.hs b/waspc/src/Wasp/Analyzer/Evaluator/Evaluation/TypedExpr/Combinators.hs index e0da203357..f1baae97cc 100644 --- a/waspc/src/Wasp/Analyzer/Evaluator/Evaluation/TypedExpr/Combinators.hs +++ b/waspc/src/Wasp/Analyzer/Evaluator/Evaluation/TypedExpr/Combinators.hs @@ -169,13 +169,21 @@ extImport = evaluation' . withCtx $ \ctx -> \case Nothing -> mkParseError ctx - $ "Path in external import must start with \"" ++ serverPrefix ++ "\"" ++ " or \"" ++ clientPrefix ++ "\"!" + $ "Path in external import must start with \"" ++ extSrcPrefix ++ "\"!" expr -> Left $ ER.mkEvaluationError ctx $ ER.ExpectedType T.ExtImportType (TypedAST.exprType expr) where mkParseError ctx msg = Left $ ER.mkEvaluationError ctx $ ER.ParseError $ ER.EvaluationParseError msg - stripImportPrefix importPath = stripPrefix serverPrefix importPath <|> stripPrefix clientPrefix importPath - serverPrefix = "@server/" - clientPrefix = "@client/" + stripImportPrefix importPath = stripPrefix extSrcPrefix importPath + -- Filip: We no longer want separation between client and server code + -- todo (filip): Do we still want to know whic is which. We might (because of the reloading). + -- For now, as we'd like (expect): + -- - Nodemon watches all files in the user's source folder (client files + -- included), but tsc only compiles the server files (I think because it + -- knows that the others aren't used). I am not yet sure how it knows this. + -- - Vite also only triggers on client files. I am not sure how it knows + -- about the difference either. + -- todo (filip): investigate + extSrcPrefix = "@src/" -- | An evaluation that expects a "JSON". json :: TypedExprEvaluation AppSpec.JSON.JSON diff --git a/waspc/src/Wasp/Generator/JsImport.hs b/waspc/src/Wasp/Generator/JsImport.hs index 25db2c7df3..c5da3f7a74 100644 --- a/waspc/src/Wasp/Generator/JsImport.hs +++ b/waspc/src/Wasp/Generator/JsImport.hs @@ -34,6 +34,22 @@ extImportToJsImport pathFromSrcDirToExtCodeDir pathFromImportLocationToSrcDir ex extImportNameToJsImportName (EI.ExtImportModule name) = JsImportModule name extImportNameToJsImportName (EI.ExtImportField name) = JsImportField name +-- filip: attempt to simplify how we generate imports. I wanted to generate a +-- module import (e.g., '@ext-src/something') and couldn't do it +-- jsImportToImportJsonRaw :: Maybe (FilePath, JsImportName, Maybe JsImportAlias) -> Aeson.Value +-- jsImportToImportJsonRaw importData = maybe notDefinedValue mkTmplData importData +-- where +-- notDefinedValue = object ["isDefined" .= False] + +-- mkTmplData :: (FilePath, JsImportName, Maybe JsImportAlias) -> Aeson.Value +-- mkTmplData (importPath, importName, maybeImportAlias) = +-- let (jsImportStmt, jsImportIdentifier) = getJsImportStmtAndIdentifierRaw importPath importName maybeImportAlias +-- in object +-- [ "isDefined" .= True, +-- "importStatement" .= jsImportStmt, +-- "importIdentifier" .= jsImportIdentifier +-- ] + jsImportToImportJson :: Maybe JsImport -> Aeson.Value jsImportToImportJson maybeJsImport = maybe notDefinedValue mkTmplData maybeJsImport where diff --git a/waspc/src/Wasp/Generator/ServerGenerator.hs b/waspc/src/Wasp/Generator/ServerGenerator.hs index a5df648336..e3db939cef 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator.hs @@ -47,7 +47,6 @@ import Wasp.Generator.Common makeJsonWithEntityData, prismaVersion, ) -import Wasp.Generator.ExternalCodeGenerator (genExternalCodeDir) import Wasp.Generator.FileDraft (FileDraft, createTextFileDraft) import Wasp.Generator.Monad (Generator) import qualified Wasp.Generator.NpmDependencies as N @@ -59,7 +58,6 @@ import Wasp.Generator.ServerGenerator.ConfigG (genConfigFile) import Wasp.Generator.ServerGenerator.CrudG (genCrud) import Wasp.Generator.ServerGenerator.Db.Seed (genDbSeed, getPackageJsonPrismaSeedField) import Wasp.Generator.ServerGenerator.EmailSenderG (depsRequiredByEmail, genEmailSender) -import Wasp.Generator.ServerGenerator.ExternalCodeGenerator (extServerCodeGeneratorStrategy, extSharedCodeGeneratorStrategy) import Wasp.Generator.ServerGenerator.JobGenerator (depsRequiredByJobs, genJobExecutors, genJobs) import Wasp.Generator.ServerGenerator.JsImport (extImportToImportJson, getAliasedJsImportStmtAndIdentifier) import Wasp.Generator.ServerGenerator.OperationsG (genOperations) @@ -81,8 +79,9 @@ genServer spec = genGitignore ] <++> genSrcDir spec - <++> genExternalCodeDir extServerCodeGeneratorStrategy (AS.externalServerFiles spec) - <++> genExternalCodeDir extSharedCodeGeneratorStrategy (AS.externalSharedFiles spec) + -- Filip: I don't generate external source folders as we're importing the user's code direclty (see ServerGenerator/JsImport.hs). + -- <++> genExternalCodeDir extServerCodeGeneratorStrategy (AS.externalServerFiles spec) + -- <++> genExternalCodeDir extSharedCodeGeneratorStrategy (AS.externalSharedFiles spec) <++> genDotEnv spec <++> genJobs spec <++> genJobExecutors spec diff --git a/waspc/src/Wasp/Generator/ServerGenerator/JsImport.hs b/waspc/src/Wasp/Generator/ServerGenerator/JsImport.hs index 448d9e05df..292264f898 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/JsImport.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/JsImport.hs @@ -1,13 +1,11 @@ module Wasp.Generator.ServerGenerator.JsImport where import qualified Data.Aeson as Aeson -import Data.Maybe (fromJust) import StrongPath (Dir, Path, Posix, Rel) -import qualified StrongPath as SP +import StrongPath.TH (reldirP) import qualified Wasp.AppSpec.ExtImport as EI import qualified Wasp.Generator.JsImport as GJI import Wasp.Generator.ServerGenerator.Common (ServerSrcDir) -import Wasp.Generator.ServerGenerator.ExternalCodeGenerator (extServerCodeDirInServerSrcDir) import Wasp.JsImport ( JsImport, JsImportAlias, @@ -44,4 +42,7 @@ extImportToJsImport :: JsImport extImportToJsImport = GJI.extImportToJsImport serverExtDir where - serverExtDir = fromJust (SP.relDirToPosix extServerCodeDirInServerSrcDir) + -- filip: Instead of generating the ext-src folder with the user's code and referencing that, we reference user code directly. + -- This gives us proper error messages (with user's file names and line numbers). + -- It works great with Vite (Vite outputs absolute file paths), but less great on the server (TS outputs relative paths, resulting in ../../src/something) + serverExtDir = [reldirP|../../../../src|] diff --git a/waspc/src/Wasp/Generator/WebAppGenerator.hs b/waspc/src/Wasp/Generator/WebAppGenerator.hs index 89cad1261f..5cdc9e3546 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator.hs @@ -50,7 +50,6 @@ import qualified Wasp.Generator.WebAppGenerator.Common as C import Wasp.Generator.WebAppGenerator.CrudG (genCrud) import Wasp.Generator.WebAppGenerator.ExternalCodeGenerator ( extClientCodeGeneratorStrategy, - extSharedCodeGeneratorStrategy, ) import qualified Wasp.Generator.WebAppGenerator.ExternalCodeGenerator as EC import Wasp.Generator.WebAppGenerator.JsImport (extImportToImportJson) @@ -84,8 +83,9 @@ genWebApp spec = do genViteConfig spec ] <++> genSrcDir spec - <++> return extClientCodeFileDrafts - <++> genExternalCodeDir extSharedCodeGeneratorStrategy (AS.externalSharedFiles spec) + -- Filip: I don't generate external source folders as we're importing the user's code direclty (see ServerGenerator/JsImport.hs). + -- <++> return extClientCodeFileDrafts + -- <++> genExternalCodeDir extSharedCodeGeneratorStrategy (AS.externalSharedFiles spec) <++> genPublicDir spec extClientCodeFileDrafts <++> genDotEnv spec <++> genUniversalDir @@ -318,10 +318,10 @@ genEnvValidationScript = genWebSockets :: AppSpec -> Generator [FileDraft] genWebSockets spec | AS.WS.areWebSocketsUsed spec = - sequence - [ genFileCopy [relfile|webSocket.ts|], - genWebSocketProvider spec - ] + sequence + [ genFileCopy [relfile|webSocket.ts|], + genWebSocketProvider spec + ] | otherwise = return [] where genFileCopy = return . C.mkSrcTmplFd diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/JsImport.hs b/waspc/src/Wasp/Generator/WebAppGenerator/JsImport.hs index 68332fa7dc..8e83c4c427 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/JsImport.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/JsImport.hs @@ -1,14 +1,12 @@ module Wasp.Generator.WebAppGenerator.JsImport where import qualified Data.Aeson as Aeson -import Data.Maybe (fromJust) import StrongPath (Dir, Path, Posix, Rel) -import qualified StrongPath as SP -import Wasp.AppSpec.ExtImport (ExtImport) +import StrongPath.TH (reldirP) +import Wasp.AppSpec.ExtImport (ExtImport (..)) import qualified Wasp.AppSpec.ExtImport as EI import qualified Wasp.Generator.JsImport as GJI import Wasp.Generator.WebAppGenerator.Common (WebAppSrcDir) -import Wasp.Generator.WebAppGenerator.ExternalCodeGenerator (extClientCodeDirInWebAppSrcDir) import Wasp.JsImport ( JsImport, JsImportIdentifier, @@ -24,6 +22,26 @@ extImportToImportJson pathFromImportLocationToSrcDir maybeExtImport = GJI.jsImpo where jsImport = extImportToJsImport pathFromImportLocationToSrcDir <$> maybeExtImport +-- extImportToImportJson :: +-- Path Posix (Rel importLocation) (Dir WebAppSrcDir) -> +-- Maybe ExtImport -> +-- Aeson.Value +-- extImportToImportJson _ maybeExtImport = case maybeExtImport of +-- Nothing -> object ["isDefined" .= False] +-- Just extImport -> makeImportObject extImport +-- where +-- makeImportObject (ExtImport importName importPath) = +-- let importClause = makeImportClause importName +-- importPathStr = "ext-sdrc/" ++ SP.toFilePath importPath +-- in object +-- [ "isDefined" .= True, +-- "importStatement" .= ("import " ++ importClause ++ "from \"" ++ importPathStr ++ "\""), +-- "importIdentifier" .= importName +-- ] +-- makeImportClause = \case +-- EI.ExtImportModule name -> name +-- EI.ExtImportField name -> "{ " ++ name ++ " + getJsImportStmtAndIdentifier :: Path Posix (Rel importLocation) (Dir WebAppSrcDir) -> EI.ExtImport -> @@ -36,4 +54,5 @@ extImportToJsImport :: JsImport extImportToJsImport = GJI.extImportToJsImport webAppExtDir where - webAppExtDir = fromJust (SP.relDirToPosix extClientCodeDirInWebAppSrcDir) + -- filip: read notes in ServerGenerator/JsImport.hs + webAppExtDir = [reldirP|../../../../src|] diff --git a/waspc/src/Wasp/JsImport.hs b/waspc/src/Wasp/JsImport.hs index 39a0db10f1..8ef9da26b4 100644 --- a/waspc/src/Wasp/JsImport.hs +++ b/waspc/src/Wasp/JsImport.hs @@ -10,6 +10,7 @@ module Wasp.JsImport makeJsImport, applyJsImportAlias, getJsImportStmtAndIdentifier, + getJsImportStmtAndIdentifierRaw, ) where @@ -34,6 +35,7 @@ data JsImport = JsImport type JsImportPath = Path Posix (Rel Dir') File' +-- Note (filip): not a fan of so many aliases for regular types type JsImportAlias = String data JsImportName @@ -60,15 +62,24 @@ applyJsImportAlias importAlias jsImport = jsImport {_importAlias = importAlias} getJsImportStmtAndIdentifier :: JsImport -> (JsImportStatement, JsImportIdentifier) getJsImportStmtAndIdentifier (JsImport importPath importName maybeImportAlias) = + getJsImportStmtAndIdentifierRaw normalizedPath importName maybeImportAlias + where + filePath = SP.fromRelFileP importPath + normalizedPath = if ".." `isPrefixOf` filePath then filePath else "./" ++ filePath + +-- filip: attempt to simplify how we generate imports. I wanted to generate a +-- module import (e.g., '@ext-src/something') and couldn't do it. This is one of +-- the funtions I implemented while I was trying to pull it off. +getJsImportStmtAndIdentifierRaw :: + FilePath -> + JsImportName -> + Maybe JsImportAlias -> + (JsImportStatement, JsImportIdentifier) +getJsImportStmtAndIdentifierRaw importPath importName maybeImportAlias = (importStatement, importIdentifier) where (importIdentifier, importClause) = jsImportIdentifierAndClause - - importStatement :: JsImportStatement - importStatement = "import " ++ importClause ++ " from '" ++ normalizedPath ++ "'" - where - filePath = SP.fromRelFileP importPath - normalizedPath = if ".." `isPrefixOf` filePath then filePath else "./" ++ filePath + importStatement = "import " ++ importClause ++ " from '" ++ importPath ++ "'" -- First part of import statement based on type of import and alias -- e.g. for import { Name as Alias } from "file.js" it returns ("Alias", "{ Name as Alias }") diff --git a/waspc/waspls/src/Wasp/LSP/ExtImport/Path.hs b/waspc/waspls/src/Wasp/LSP/ExtImport/Path.hs index d1c4882d96..a10fa90a7a 100644 --- a/waspc/waspls/src/Wasp/LSP/ExtImport/Path.hs +++ b/waspc/waspls/src/Wasp/LSP/ExtImport/Path.hs @@ -67,6 +67,7 @@ waspStylePathToCachePath (WaspStyleExtFilePath waspStylePath) = then ExtFileCachePath relPathWithoutExt (DotExact ext) else ExtFileCachePath relPathWithoutExt (widenExtension ext) where + -- Filip: todo - update for new structure useExactExtension = "@client" `isPrefixOf` waspStylePath absPathToCachePath :: HasProjectRootDir m => SP.Path' SP.Abs (SP.File a) -> m (Maybe ExtFileCachePath) From d51e64bab07033eaf0ac2f88385936ff80c04e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Tue, 21 Nov 2023 02:35:50 +0100 Subject: [PATCH 06/54] Fix typo in package.json --- waspc/data/Generator/templates/server/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/waspc/data/Generator/templates/server/package.json b/waspc/data/Generator/templates/server/package.json index fcdd31a197..1a80a9ee47 100644 --- a/waspc/data/Generator/templates/server/package.json +++ b/waspc/data/Generator/templates/server/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "private": true, "type": "module", - "comment-filip": "The server.js location changed because we have now included client source files above .wasp/out/server/src." + "comment-filip": "The server.js location changed because we have now included client source files above .wasp/out/server/src.", "scripts": { "build": "npx tsc", "start": "npm run validate-env && NODE_PATH=dist node -r dotenv/config dist/.wasp/out/server/src/server.js", From 22c77065d7cb4040d83dbc32a1c24e5d21743946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Thu, 23 Nov 2023 16:16:10 +0100 Subject: [PATCH 07/54] Revert cosmetic updates to todo-typescript example apps --- .../src/client/MainPage.tsx | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/examples/todo-app-in-typescript/src/client/MainPage.tsx b/examples/todo-app-in-typescript/src/client/MainPage.tsx index 1b7364a246..b16d4242cf 100644 --- a/examples/todo-app-in-typescript/src/client/MainPage.tsx +++ b/examples/todo-app-in-typescript/src/client/MainPage.tsx @@ -1,23 +1,23 @@ -import React, { FormEventHandler, FormEvent } from 'react' -import waspLogo from './waspLogo.png' +import React, { FormEventHandler, FormEvent } from "react"; +import waspLogo from "./waspLogo.png"; -import './Main.css' +import "./Main.css"; // Wasp imports 🐝 = } -import logout from '@wasp/auth/logout' -import { useQuery } from '@wasp/queries' // Wasp uses a thin wrapper around react-query -import getTasks from '@wasp/queries/getTasks' -import createTask from '@wasp/actions/createTask' -import updateTask from '@wasp/actions/updateTask' -import deleteTasks from '@wasp/actions/deleteTasks' -import type { Task, User } from '@wasp/entities' +import logout from "@wasp/auth/logout"; +import { useQuery } from "@wasp/queries"; // Wasp uses a thin wrapper around react-query +import getTasks from "@wasp/queries/getTasks"; +import createTask from "@wasp/actions/createTask"; +import updateTask from "@wasp/actions/updateTask"; +import deleteTasks from "@wasp/actions/deleteTasks"; +import type { Task, User } from "@wasp/entities"; export const MainPage = ({ user }: { user: User }) => { - const { data: tasks, isLoading, error } = useQuery(getTasks) + const { data: tasks, isLoading, error } = useQuery(getTasks); - if (isLoading) return 'Loading...' - if (error) return 'Error: ' + error + if (isLoading) return "Loading..."; + if (error) return "Error: " + error; - const completed = tasks?.filter((task) => task.isDone).map((task) => task.id) + const completed = tasks?.filter((task) => task.isDone).map((task) => task.id); return (
    @@ -42,8 +42,8 @@ export const MainPage = ({ user }: { user: User }) => {
    - ) -} + ); +}; function Todo({ id, isDone, description }: Task) { const handleIsDoneChange: FormEventHandler = async ( @@ -53,11 +53,11 @@ function Todo({ id, isDone, description }: Task) { await updateTask({ id, isDone: event.currentTarget.checked, - }) + }); } catch (err: any) { - window.alert('Error while updating task ' + err?.message) + window.alert("Error while updating task " + err?.message); } - } + }; return (
  • @@ -72,38 +72,38 @@ function Todo({ id, isDone, description }: Task) {
  • - ) + ); } function TasksList({ tasks }: { tasks: Task[] }) { - if (tasks.length === 0) return

    No tasks yet.

    + if (tasks.length === 0) return

    No tasks yet.

    ; return (
      {tasks.map((task, idx) => ( ))}
    - ) + ); } function NewTaskForm() { const handleSubmit = async (event: FormEvent) => { - event.preventDefault() + event.preventDefault(); try { - const description = event.currentTarget.description.value - console.log(description) - event.currentTarget.reset() - await createTask({ description }) + const description = event.currentTarget.description.value; + console.log(description); + event.currentTarget.reset(); + await createTask({ description }); } catch (err: any) { - window.alert('Error: ' + err?.message) + window.alert("Error: " + err?.message); } - } + }; return (
    - ) + ); } From 02bdcbe2f78feec2f954f310aecdf4a8c2b9a022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Fri, 24 Nov 2023 08:31:48 +0100 Subject: [PATCH 08/54] Remove old queries files --- .../node_modules/@wasp/.queries/core.d.ts | 23 -------------- .../node_modules/@wasp/.queries/core.js | 30 ------------------- .../node_modules/@wasp/.queries/getTasks.ts | 6 ---- .../node_modules/@wasp/.queries/index.d.ts | 10 ------- .../node_modules/@wasp/.queries/index.js | 18 ----------- .../node_modules/@wasp/.queries/queries.ts | 4 --- 6 files changed, 91 deletions(-) delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.d.ts delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.js delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/.queries/getTasks.ts delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/.queries/index.d.ts delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/.queries/index.js delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/.queries/queries.ts diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.d.ts b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.d.ts deleted file mode 100644 index 0b6da79944..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { type Query } from "."; -import { Route } from "@wasp/types"; -import type { Expand, _Awaited, _ReturnType } from "@wasp/universal/types"; - -export function createQuery( - queryRoute: string, - entitiesUsed: any[] -): QueryFor; - -export function addMetadataToQuery( - query: (...args: any[]) => Promise, - metadata: { - relativeQueryPath: string; - queryRoute: Route; - entitiesUsed: string[]; - } -): void; - -type QueryFor = Expand< - Query[0], _Awaited<_ReturnType>> ->; - -type GenericBackendQuery = (args: never, context: any) => unknown; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.js b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.js deleted file mode 100644 index 00974ffa08..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/core.js +++ /dev/null @@ -1,30 +0,0 @@ -import { callOperation, makeOperationRoute } from "@wasp/operations"; -import { - addResourcesUsedByQuery, - getActiveOptimisticUpdates, -} from "@wasp/operations/resources"; - -export function createQuery(relativeQueryPath, entitiesUsed) { - const queryRoute = makeOperationRoute(relativeQueryPath); - - async function query(queryKey, queryArgs) { - const serverResult = await callOperation(queryRoute, queryArgs); - return getActiveOptimisticUpdates(queryKey).reduce( - (result, update) => update(result), - serverResult - ); - } - - addMetadataToQuery(query, { relativeQueryPath, queryRoute, entitiesUsed }); - - return query; -} - -export function addMetadataToQuery( - query, - { relativeQueryPath, queryRoute, entitiesUsed } -) { - query.queryCacheKey = [relativeQueryPath]; - query.route = queryRoute; - addResourcesUsedByQuery(query.queryCacheKey, entitiesUsed); -} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/getTasks.ts b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/getTasks.ts deleted file mode 100644 index 25fc1f2859..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/getTasks.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createQuery } from "./core"; -import { GetTasks } from "@wasp/server/queries"; - -const query = createQuery("operations/get-tasks", ["Task"]); - -export default query; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/index.d.ts b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/index.d.ts deleted file mode 100644 index c007ff4c92..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { UseQueryResult } from "@tanstack/react-query"; - -export type Query = { - (queryCacheKey: string[], args: Input): Promise -} - -export function useQuery( - queryFn: Query, - queryFnArgs?: Input, options?: any -): UseQueryResult diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/index.js b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/index.js deleted file mode 100644 index 03e52ce0f9..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import { useQuery as rqUseQuery } from '@tanstack/react-query' -export { configureQueryClient } from '../queryClient' - -export function useQuery(queryFn, queryFnArgs, options) { - 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 queryKey = queryFnArgs !== undefined ? [...queryFn.queryCacheKey, queryFnArgs] : queryFn.queryCacheKey - return rqUseQuery({ - queryKey, - queryFn: () => queryFn(queryKey, queryFnArgs), - ...options - }) -} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/queries.ts b/waspc/examples/todo-typescript/node_modules/@wasp/.queries/queries.ts deleted file mode 100644 index 678281f07f..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/.queries/queries.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { createQuery } from "./core"; -import { GetTasks } from "../server/queries"; - -export const getTasks = createQuery("operations/get-tasks", ["Task"]); From 1002c4bf3d93189aa3b9cd3f8d824b8918fba588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Fri, 24 Nov 2023 11:19:35 +0100 Subject: [PATCH 09/54] Remove extra dependencies from auth package --- .../todo-typescript/node_modules/@wasp/auth/package.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/package.json b/waspc/examples/todo-typescript/node_modules/@wasp/auth/package.json index 2a1df894d8..190801a489 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/auth/package.json +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/package.json @@ -1,6 +1,3 @@ { - "name": "auth", - "dependencies": { - "@wasp/api": "*" - } + "name": "auth" } From 8d39046b7ac08a77e58d3bf984f79eb2e082c433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Fri, 24 Nov 2023 11:20:52 +0100 Subject: [PATCH 10/54] Remove completed todo --- .../examples/todo-typescript/node_modules/@wasp/auth/types.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/types.ts b/waspc/examples/todo-typescript/node_modules/@wasp/auth/types.ts index b706ce3f67..87582f82f9 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/auth/types.ts +++ b/waspc/examples/todo-typescript/node_modules/@wasp/auth/types.ts @@ -1,2 +1 @@ -// todo(filip): turn into a proper import/path -export { type SanitizedUser as User } from "@wasp/server/_types/"; +export { type SanitizedUser as User } from '@wasp/server/_types/' From 3fd2f163a31d9c4dff4939a8afa274ebd7603ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Fri, 24 Nov 2023 13:17:50 +0100 Subject: [PATCH 11/54] Remove redundant server types --- .../node_modules/@wasp/ext-src/serverTypes.ts | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/ext-src/serverTypes.ts diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/serverTypes.ts b/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/serverTypes.ts deleted file mode 100644 index 33f91956f4..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/serverTypes.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { User, Prisma } from "@prisma/client"; - -export { type Task } from "@prisma/client"; - -export type Context = { - user: User; - entities: { - Task: Prisma.TaskDelegate<{}>; - User: Prisma.UserDelegate<{}>; - }; -}; From 20b5ef7c4e0cf46517517a958e7323c55ba92977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Fri, 24 Nov 2023 13:35:15 +0100 Subject: [PATCH 12/54] Remove redundant exports from operations --- .../node_modules/@wasp/operations/package.json | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/operations/package.json b/waspc/examples/todo-typescript/node_modules/@wasp/operations/package.json index 2e944ae8ae..f63f61e276 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/operations/package.json +++ b/waspc/examples/todo-typescript/node_modules/@wasp/operations/package.json @@ -1,7 +1,3 @@ { - "name": "operations", - "exports": { - ".": "./index.ts", - "./resources": "./resources.js" - } -} \ No newline at end of file + "name": "operations" +} From 577c6cf9c8ac4b792479eafe9d55987d85123435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Fri, 24 Nov 2023 14:01:11 +0100 Subject: [PATCH 13/54] Remove redundant dependencies and package lock --- .../todo-typescript/package-lock.json | 255 ------------------ waspc/examples/todo-typescript/package.json | 8 +- 2 files changed, 1 insertion(+), 262 deletions(-) delete mode 100644 waspc/examples/todo-typescript/package-lock.json diff --git a/waspc/examples/todo-typescript/package-lock.json b/waspc/examples/todo-typescript/package-lock.json deleted file mode 100644 index 1f7c759e26..0000000000 --- a/waspc/examples/todo-typescript/package-lock.json +++ /dev/null @@ -1,255 +0,0 @@ -{ - "name": "todo-typescript", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@prisma/client": "^4.12.0", - "vite": "^4.5.0" - }, - "devDependencies": { - "prisma": "^4.12.0" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.18.15", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@prisma/client": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.12.0.tgz", - "integrity": "sha512-j9/ighfWwux97J2dS15nqhl60tYoH8V0IuSsgZDb6bCFcQD3fXbXmxjYC8GHhIgOk3lB7Pq+8CwElz2MiDpsSg==", - "hasInstallScript": true, - "dependencies": { - "@prisma/engines-version": "4.12.0-67.659ef412370fa3b41cd7bf6e94587c1dfb7f67e7" - }, - "engines": { - "node": ">=14.17" - }, - "peerDependencies": { - "prisma": "*" - }, - "peerDependenciesMeta": { - "prisma": { - "optional": true - } - } - }, - "node_modules/@prisma/engines": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.12.0.tgz", - "integrity": "sha512-0alKtnxhNB5hYU+ymESBlGI4b9XrGGSdv7Ud+8TE/fBNOEhIud0XQsAR+TrvUZgS4na5czubiMsODw0TUrgkIA==", - "devOptional": true, - "hasInstallScript": true - }, - "node_modules/@prisma/engines-version": { - "version": "4.12.0-67.659ef412370fa3b41cd7bf6e94587c1dfb7f67e7", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.12.0-67.659ef412370fa3b41cd7bf6e94587c1dfb7f67e7.tgz", - "integrity": "sha512-JIHNj5jlXb9mcaJwakM0vpgRYJIAurxTUqM0iX0tfEQA5XLZ9ONkIckkhuAKdAzocZ+80GYg7QSsfpjg7OxbOA==" - }, - "node_modules/@types/node": { - "version": "18.15.13", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/esbuild": { - "version": "0.18.15", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.15", - "@esbuild/android-arm64": "0.18.15", - "@esbuild/android-x64": "0.18.15", - "@esbuild/darwin-arm64": "0.18.15", - "@esbuild/darwin-x64": "0.18.15", - "@esbuild/freebsd-arm64": "0.18.15", - "@esbuild/freebsd-x64": "0.18.15", - "@esbuild/linux-arm": "0.18.15", - "@esbuild/linux-arm64": "0.18.15", - "@esbuild/linux-ia32": "0.18.15", - "@esbuild/linux-loong64": "0.18.15", - "@esbuild/linux-mips64el": "0.18.15", - "@esbuild/linux-ppc64": "0.18.15", - "@esbuild/linux-riscv64": "0.18.15", - "@esbuild/linux-s390x": "0.18.15", - "@esbuild/linux-x64": "0.18.15", - "@esbuild/netbsd-x64": "0.18.15", - "@esbuild/openbsd-x64": "0.18.15", - "@esbuild/sunos-x64": "0.18.15", - "@esbuild/win32-arm64": "0.18.15", - "@esbuild/win32-ia32": "0.18.15", - "@esbuild/win32-x64": "0.18.15" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.6", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "license": "ISC" - }, - "node_modules/postcss": { - "version": "8.4.27", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prisma": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.12.0.tgz", - "integrity": "sha512-xqVper4mbwl32BWzLpdznHAYvYDWQQWK2tBfXjdUD397XaveRyAP7SkBZ6kFlIg8kKayF4hvuaVtYwXd9BodAg==", - "devOptional": true, - "hasInstallScript": true, - "dependencies": { - "@prisma/engines": "4.12.0" - }, - "bin": { - "prisma": "build/index.js", - "prisma2": "build/index.js" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vite": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", - "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", - "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - } - } -} diff --git a/waspc/examples/todo-typescript/package.json b/waspc/examples/todo-typescript/package.json index 6cd0c22699..b7219eda1d 100644 --- a/waspc/examples/todo-typescript/package.json +++ b/waspc/examples/todo-typescript/package.json @@ -1,9 +1,3 @@ { - "dependencies": { - "@prisma/client": "^4.12.0", - "vite": "^4.5.0" - }, - "devDependencies": { - "prisma": "^4.12.0" - } + "name": "prototype" } From 3da3f52ec984bc1d238fa1acaeedccb10b3033af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Fri, 24 Nov 2023 14:10:46 +0100 Subject: [PATCH 14/54] Remove npm install skip --- waspc/src/Wasp/Generator/Setup.hs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/waspc/src/Wasp/Generator/Setup.hs b/waspc/src/Wasp/Generator/Setup.hs index 435e31acd0..9aac72b391 100644 --- a/waspc/src/Wasp/Generator/Setup.hs +++ b/waspc/src/Wasp/Generator/Setup.hs @@ -28,10 +28,9 @@ runNpmInstallIfNeeded spec dstDir sendMessage = do Right maybeFullStackDeps -> case maybeFullStackDeps of Nothing -> return ([], []) Just fullStackDeps -> do - sendMessage $ Msg.Start "Skipping npm install..." + sendMessage $ Msg.Start "Starting npm install..." (Left (npmInstallWarnings, npmInstallErrors)) <- installNpmDependenciesWithInstallRecord fullStackDeps dstDir `race` reportInstallationProgress reportInstallationProgressMessages when (null npmInstallErrors) (sendMessage $ Msg.Success "Successfully completed npm install.") - return ([], []) return (npmInstallWarnings, npmInstallErrors) where reportInstallationProgress :: [String] -> IO () From 3ceb92de6a7763883f9ed9eea0ac9ea92ddc2c51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Fri, 24 Nov 2023 14:14:48 +0100 Subject: [PATCH 15/54] Fix formatting --- waspc/src/Wasp/Generator/Setup.hs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/waspc/src/Wasp/Generator/Setup.hs b/waspc/src/Wasp/Generator/Setup.hs index 9aac72b391..05ba4912e2 100644 --- a/waspc/src/Wasp/Generator/Setup.hs +++ b/waspc/src/Wasp/Generator/Setup.hs @@ -29,7 +29,9 @@ runNpmInstallIfNeeded spec dstDir sendMessage = do Nothing -> return ([], []) Just fullStackDeps -> do sendMessage $ Msg.Start "Starting npm install..." - (Left (npmInstallWarnings, npmInstallErrors)) <- installNpmDependenciesWithInstallRecord fullStackDeps dstDir `race` reportInstallationProgress reportInstallationProgressMessages + (Left (npmInstallWarnings, npmInstallErrors)) <- + installNpmDependenciesWithInstallRecord fullStackDeps dstDir + `race` reportInstallationProgress reportInstallationProgressMessages when (null npmInstallErrors) (sendMessage $ Msg.Success "Successfully completed npm install.") return (npmInstallWarnings, npmInstallErrors) where From 1c6a12f1ae0e2136fadf40452e7324bd21342ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Fri, 24 Nov 2023 15:18:16 +0100 Subject: [PATCH 16/54] Make migrate script more portable --- waspc/examples/todo-typescript/migrate | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/waspc/examples/todo-typescript/migrate b/waspc/examples/todo-typescript/migrate index d583bfa6e3..3c41785806 100755 --- a/waspc/examples/todo-typescript/migrate +++ b/waspc/examples/todo-typescript/migrate @@ -1,6 +1,6 @@ rsync -a .wasp/out/web-app/node_modules/ node_modules/ rsync -a .wasp/out/server/node_modules/ node_modules/ # rsync -a node_modules_wasp/ node_modules -rw db migrate-dev -fd . '.wasp/out/server/node_modules' | grep -v prisma -i | xargs rm -r +cabal run wasp-cli db migrate-dev +find .wasp/out/server/node_modules -mindepth 1 -type d | grep -Eiv 'prisma|\.bin' | xargs rm -r 2> /dev/null rm -r .wasp/out/web-app/node_modules From d22922ddde72b3fb9a6238aebe4af27a17021a4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Thu, 30 Nov 2023 17:18:29 +0100 Subject: [PATCH 17/54] Turn scoped package into a single package --- .../templates/react-app/src/index.tsx | 2 +- .../templates/react-app/src/router.tsx | 2 +- .../templates/react-app/vite.config.ts | 2 +- .../crud-testing/src/server/auth_simple.js | 2 +- .../node_modules/@wasp/api/package.json | 3 - .../@wasp/auth/forms/internal/Form.tsx | 83 -------- .../@wasp/auth/forms/internal/Message.tsx | 18 -- .../node_modules/@wasp/auth/login.ts | 16 -- .../node_modules/@wasp/auth/package.json | 3 - .../node_modules/@wasp/auth/signup.ts | 12 -- .../node_modules/@wasp/auth/types.ts | 1 - .../node_modules/@wasp/auth/useAuth.ts | 41 ---- .../node_modules/@wasp/core/config.js | 11 -- .../node_modules/@wasp/core/package.json | 20 -- .../@wasp/operations/package.json | 3 - .../node_modules/@wasp/rpc/package.json | 3 - .../node_modules/@wasp/rpc/queries/index.ts | 6 - .../node_modules/@wasp/server/core/auth.js | 187 ------------------ .../@wasp/server/queries/index.ts | 13 -- .../node_modules/@wasp/types/package.json | 3 - .../node_modules/@wasp/universal/package.json | 3 - .../{@wasp => wasp}/api/events.ts | 0 .../node_modules/{@wasp => wasp}/api/index.ts | 80 ++++---- .../{@wasp => wasp}/auth/forms/Auth.tsx | 66 +++---- .../{@wasp => wasp}/auth/forms/Login.tsx | 0 .../{@wasp => wasp}/auth/forms/Signup.tsx | 0 .../wasp/auth/forms/internal/Form.tsx | 83 ++++++++ .../wasp/auth/forms/internal/Message.tsx | 18 ++ .../forms/internal/common/LoginSignupForm.tsx | 52 ++--- .../useUsernameAndPassword.ts | 0 .../{@wasp => wasp}/auth/forms/types.ts | 0 .../{@wasp => wasp}/auth/helpers/user.ts | 8 +- .../node_modules/wasp/auth/login.ts | 16 ++ .../{@wasp => wasp}/auth/logout.ts | 8 +- .../auth/pages/createAuthRequiredPage.jsx | 0 .../node_modules/wasp/auth/signup.ts | 12 ++ .../{@wasp => wasp}/auth/stitches.config.js | 0 .../node_modules/wasp/auth/types.ts | 1 + .../node_modules/wasp/auth/useAuth.ts | 41 ++++ .../{@wasp => wasp}/core/AuthError.js | 0 .../{@wasp => wasp}/core/HttpError.js | 0 .../node_modules/{@wasp => wasp}/core/auth.js | 0 .../core/auth/prismaMiddleware.js | 0 .../{@wasp => wasp}/core/auth/validators.ts | 0 .../node_modules/wasp/core/config.js | 11 ++ .../{@wasp => wasp}/core/stitches.config.js | 0 .../{@wasp => wasp}/core/storage.ts | 0 .../{@wasp => wasp}/entities/index.ts | 0 .../{@wasp => wasp}/ext-src/actions.ts | 6 +- .../{@wasp => wasp}/ext-src/queries.ts | 6 +- .../{@wasp => wasp}/operations/index.ts | 18 +- .../{@wasp => wasp}/operations/resources.js | 46 ++--- .../operations/updateHandlersMap.js | 0 .../node_modules/wasp/package.json | 33 ++++ .../{@wasp => wasp}/rpc/actions/core.d.ts | 2 +- .../{@wasp => wasp}/rpc/actions/core.js | 18 +- .../{@wasp => wasp}/rpc/actions/index.ts | 2 +- .../node_modules/{@wasp => wasp}/rpc/index.ts | 0 .../{@wasp => wasp}/rpc/queries/core.d.ts | 12 +- .../{@wasp => wasp}/rpc/queries/core.js | 20 +- .../node_modules/wasp/rpc/queries/index.ts | 6 + .../{@wasp => wasp}/rpc/queryClient.ts | 0 .../{@wasp => wasp}/server/_types/index.ts | 52 ++--- .../server/_types/serialization.ts | 0 .../server/_types/taggedEntities.ts | 0 .../{@wasp => wasp}/server/actions/index.ts | 4 +- .../{@wasp => wasp}/server/actions/types.ts | 0 .../{@wasp => wasp}/server/core/AuthError.js | 0 .../{@wasp => wasp}/server/core/HttpError.js | 0 .../node_modules/wasp/server/core/auth.js | 185 +++++++++++++++++ .../server/core/auth/prismaMiddleware.js | 0 .../server/core/auth/validators.ts | 0 .../{@wasp => wasp}/server/dbClient.ts | 0 .../node_modules/wasp/server/queries/index.ts | 13 ++ .../{@wasp => wasp}/server/queries/types.ts | 0 .../{@wasp => wasp}/types/index.ts | 0 .../{@wasp => wasp}/universal/types.ts | 0 .../{@wasp => wasp}/universal/url.ts | 0 .../examples/todo-typescript/src/MainPage.tsx | 11 +- .../todo-typescript/src/task/actions.ts | 6 +- .../todo-typescript/src/task/queries.ts | 6 +- .../todo-typescript/src/user/LoginPage.tsx | 6 +- .../todo-typescript/src/user/SignupPage.tsx | 6 +- 83 files changed, 640 insertions(+), 648 deletions(-) delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/api/package.json delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/Form.tsx delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/Message.tsx delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/login.ts delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/package.json delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/signup.ts delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/types.ts delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/auth/useAuth.ts delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/core/config.js delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/core/package.json delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/operations/package.json delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/rpc/package.json delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/index.ts delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth.js delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/server/queries/index.ts delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/types/package.json delete mode 100644 waspc/examples/todo-typescript/node_modules/@wasp/universal/package.json rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/api/events.ts (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/api/index.ts (63%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/auth/forms/Auth.tsx (60%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/auth/forms/Login.tsx (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/auth/forms/Signup.tsx (100%) create mode 100644 waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/Form.tsx create mode 100644 waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/Message.tsx rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/auth/forms/internal/common/LoginSignupForm.tsx (64%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/auth/forms/types.ts (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/auth/helpers/user.ts (73%) create mode 100644 waspc/examples/todo-typescript/node_modules/wasp/auth/login.ts rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/auth/logout.ts (52%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/auth/pages/createAuthRequiredPage.jsx (100%) create mode 100644 waspc/examples/todo-typescript/node_modules/wasp/auth/signup.ts rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/auth/stitches.config.js (100%) create mode 100644 waspc/examples/todo-typescript/node_modules/wasp/auth/types.ts create mode 100644 waspc/examples/todo-typescript/node_modules/wasp/auth/useAuth.ts rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/core/AuthError.js (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/core/HttpError.js (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/core/auth.js (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/core/auth/prismaMiddleware.js (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/core/auth/validators.ts (100%) create mode 100644 waspc/examples/todo-typescript/node_modules/wasp/core/config.js rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/core/stitches.config.js (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/core/storage.ts (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/entities/index.ts (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/ext-src/actions.ts (87%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/ext-src/queries.ts (71%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/operations/index.ts (66%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/operations/resources.js (74%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/operations/updateHandlersMap.js (100%) create mode 100644 waspc/examples/todo-typescript/node_modules/wasp/package.json rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/rpc/actions/core.d.ts (84%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/rpc/actions/core.js (77%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/rpc/actions/index.ts (83%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/rpc/index.ts (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/rpc/queries/core.d.ts (71%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/rpc/queries/core.js (60%) create mode 100644 waspc/examples/todo-typescript/node_modules/wasp/rpc/queries/index.ts rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/rpc/queryClient.ts (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/server/_types/index.ts (67%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/server/_types/serialization.ts (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/server/_types/taggedEntities.ts (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/server/actions/index.ts (90%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/server/actions/types.ts (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/server/core/AuthError.js (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/server/core/HttpError.js (100%) create mode 100644 waspc/examples/todo-typescript/node_modules/wasp/server/core/auth.js rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/server/core/auth/prismaMiddleware.js (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/server/core/auth/validators.ts (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/server/dbClient.ts (100%) create mode 100644 waspc/examples/todo-typescript/node_modules/wasp/server/queries/index.ts rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/server/queries/types.ts (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/types/index.ts (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/universal/types.ts (100%) rename waspc/examples/todo-typescript/node_modules/{@wasp => wasp}/universal/url.ts (100%) diff --git a/waspc/data/Generator/templates/react-app/src/index.tsx b/waspc/data/Generator/templates/react-app/src/index.tsx index ef2e68bafb..da29899438 100644 --- a/waspc/data/Generator/templates/react-app/src/index.tsx +++ b/waspc/data/Generator/templates/react-app/src/index.tsx @@ -7,7 +7,7 @@ import router from './router' import { initializeQueryClient, queryClientInitialized, -} from '@wasp/rpc/queryClient' +} from 'wasp/rpc/queryClient' {=# setupFn.isDefined =} {=& setupFn.importStatement =} diff --git a/waspc/data/Generator/templates/react-app/src/router.tsx b/waspc/data/Generator/templates/react-app/src/router.tsx index 37e2ea93cf..ed1de164a0 100644 --- a/waspc/data/Generator/templates/react-app/src/router.tsx +++ b/waspc/data/Generator/templates/react-app/src/router.tsx @@ -12,7 +12,7 @@ import type { {=/ rootComponent.isDefined =} {=# isAuthEnabled =} -import createAuthRequiredPage from "@wasp/auth/pages/createAuthRequiredPage" +import createAuthRequiredPage from "wasp/auth/pages/createAuthRequiredPage" {=/ isAuthEnabled =} {=# pagesToImport =} diff --git a/waspc/data/Generator/templates/react-app/vite.config.ts b/waspc/data/Generator/templates/react-app/vite.config.ts index 84c889f1b2..f7fd3d8720 100644 --- a/waspc/data/Generator/templates/react-app/vite.config.ts +++ b/waspc/data/Generator/templates/react-app/vite.config.ts @@ -15,7 +15,7 @@ const defaultViteConfig = { base: "{= baseDir =}", plugins: [react()], optimizeDeps: { - exclude: ['@wasp'] + exclude: ['wasp'] }, server: { port: {= defaultClientPort =}, diff --git a/waspc/examples/crud-testing/src/server/auth_simple.js b/waspc/examples/crud-testing/src/server/auth_simple.js index 52d6aad97e..bf98625829 100644 --- a/waspc/examples/crud-testing/src/server/auth_simple.js +++ b/waspc/examples/crud-testing/src/server/auth_simple.js @@ -1,4 +1,4 @@ -import { defineAdditionalSignupFields } from '@wasp/auth/index.js' +import { defineAdditionalSignupFields } from 'wasp/auth/index.js' export const fields = defineAdditionalSignupFields({ address: (data) => data.address, diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/api/package.json b/waspc/examples/todo-typescript/node_modules/@wasp/api/package.json deleted file mode 100644 index 2673a1a809..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/api/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "api" -} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/Form.tsx b/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/Form.tsx deleted file mode 100644 index 611d98f345..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/Form.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { styled } from "@wasp/core/stitches.config"; - -export const Form = styled("form", { - marginTop: "1.5rem", -}); - -export const FormItemGroup = styled("div", { - "& + div": { - marginTop: "1.5rem", - }, -}); - -export const FormLabel = styled("label", { - display: "block", - fontSize: "$sm", - fontWeight: "500", -}); - -export const FormInput = styled("input", { - display: "block", - lineHeight: "1.5rem", - fontSize: "$sm", - borderWidth: "1px", - borderColor: "$gray600", - backgroundColor: "#f8f4ff", - boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)", - "&:focus": { - borderWidth: "1px", - borderColor: "$gray700", - boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)", - }, - "&:disabled": { - opacity: 0.5, - cursor: "not-allowed", - backgroundColor: "$gray400", - borderColor: "$gray400", - color: "$gray500", - }, - - borderRadius: "0.375rem", - width: "100%", - - paddingTop: "0.375rem", - paddingBottom: "0.375rem", - paddingLeft: "0.75rem", - paddingRight: "0.75rem", - - marginTop: "0.5rem", -}); - -export const SubmitButton = styled("button", { - display: "flex", - justifyContent: "center", - - width: "100%", - borderWidth: "1px", - borderColor: "$brand", - backgroundColor: "$brand", - color: "$submitButtonText", - - padding: "0.5rem 0.75rem", - boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)", - - fontWeight: "600", - fontSize: "$sm", - lineHeight: "1.25rem", - borderRadius: "0.375rem", - - // TODO(matija): extract this into separate BaseButton component and then inherit it. - "&:hover": { - backgroundColor: "$brandAccent", - borderColor: "$brandAccent", - }, - "&:disabled": { - opacity: 0.5, - cursor: "not-allowed", - backgroundColor: "$gray400", - borderColor: "$gray400", - color: "$gray500", - }, - transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)", - transitionDuration: "100ms", -}); diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/Message.tsx b/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/Message.tsx deleted file mode 100644 index c0b28e33e9..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/Message.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { styled } from "@wasp/core/stitches.config"; - -export const Message = styled("div", { - padding: "0.5rem 0.75rem", - borderRadius: "0.375rem", - marginTop: "1rem", - background: "$gray400", -}); - -export const MessageError = styled(Message, { - background: "$errorBackground", - color: "$errorText", -}); - -export const MessageSuccess = styled(Message, { - background: "$successBackground", - color: "$successText", -}); diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/login.ts b/waspc/examples/todo-typescript/node_modules/@wasp/auth/login.ts deleted file mode 100644 index dbd09feae7..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/auth/login.ts +++ /dev/null @@ -1,16 +0,0 @@ -import api, { handleApiError } from "@wasp/api"; -import { initSession } from "./helpers/user"; - -export default async function login( - username: string, - password: string -): Promise { - try { - const args = { username, password }; - const response = await api.post("/auth/local/login", args); - - await initSession(response.data.token); - } catch (error) { - handleApiError(error); - } -} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/package.json b/waspc/examples/todo-typescript/node_modules/@wasp/auth/package.json deleted file mode 100644 index 190801a489..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/auth/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "auth" -} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/signup.ts b/waspc/examples/todo-typescript/node_modules/@wasp/auth/signup.ts deleted file mode 100644 index cfaf25fb6a..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/auth/signup.ts +++ /dev/null @@ -1,12 +0,0 @@ -import api, { handleApiError } from "@wasp/api"; - -export default async function signup(userFields: { - username: string; - password: string; -}): Promise { - try { - await api.post("/auth/local/signup", userFields); - } catch (error) { - handleApiError(error); - } -} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/types.ts b/waspc/examples/todo-typescript/node_modules/@wasp/auth/types.ts deleted file mode 100644 index 87582f82f9..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/auth/types.ts +++ /dev/null @@ -1 +0,0 @@ -export { type SanitizedUser as User } from '@wasp/server/_types/' diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/useAuth.ts b/waspc/examples/todo-typescript/node_modules/@wasp/auth/useAuth.ts deleted file mode 100644 index 78b58f03e5..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/auth/useAuth.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { deserialize as superjsonDeserialize } from "superjson"; -import { useQuery } from "@wasp/rpc"; -import api, { handleApiError } from "@wasp/api"; -import { HttpMethod } from "@wasp/types"; -import type { User } from "./types"; -import { addMetadataToQuery } from "@wasp/rpc/queries"; - -export const getMe = createUserGetter(); - -export default function useAuth(queryFnArgs?: unknown, config?: any) { - return useQuery(getMe, queryFnArgs, config); -} - -function createUserGetter() { - const getMeRelativePath = "auth/me"; - /* filip */ console.log("tu smo"); - const getMeRoute = { method: HttpMethod.Get, path: `/${getMeRelativePath}` }; - async function getMe(): Promise { - try { - const response = await api.get(getMeRoute.path); - - /* filip */ console.log("uspio"); - return superjsonDeserialize(response.data); - } catch (error) { - /* filip */ console.log("failao"); - if (error.response?.status === 401) { - } else { - handleApiError(error); - } - return null; - } - } - - addMetadataToQuery(getMe, { - relativeQueryPath: getMeRelativePath, - queryRoute: getMeRoute, - entitiesUsed: ["User"], - }); - - return getMe; -} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/core/config.js b/waspc/examples/todo-typescript/node_modules/@wasp/core/config.js deleted file mode 100644 index 2c2cc63358..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/core/config.js +++ /dev/null @@ -1,11 +0,0 @@ -import { stripTrailingSlash } from "@wasp/universal/url"; - -const apiUrl = - stripTrailingSlash(import.meta.env.REACT_APP_API_URL) || - "http://localhost:3001"; - -const config = { - apiUrl, -}; - -export default config; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/core/package.json b/waspc/examples/todo-typescript/node_modules/@wasp/core/package.json deleted file mode 100644 index b1c9828d73..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/core/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "core", - "type": "module", - "version": "1.0.0", - "description": "", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "types": "tsc --declaration --emitDeclarationOnly --stripInternal --declarationDir dist" - }, - "exports": { - "./HttpError": "./HttpError.js", - "./config": "./config.js", - "./stitches.config": "./stitches.config.js", - "./storage": "./storage.ts" - }, - "types": "./index.d.ts", - "author": "", - "license": "ISC", - "include": ["src/**/*"] -} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/operations/package.json b/waspc/examples/todo-typescript/node_modules/@wasp/operations/package.json deleted file mode 100644 index f63f61e276..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/operations/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "operations" -} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/package.json b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/package.json deleted file mode 100644 index 841a5f80a5..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "rpc" -} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/index.ts b/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/index.ts deleted file mode 100644 index ef0bf5ced4..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createQuery } from "./core"; -import { GetTasks } from "@wasp/server/queries"; - -export const getTasks = createQuery("operations/get-tasks", ["Task"]); - -export { addMetadataToQuery } from "./core"; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth.js b/waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth.js deleted file mode 100644 index 2a02d3c6fe..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth.js +++ /dev/null @@ -1,187 +0,0 @@ -import jwt from "jsonwebtoken"; -import SecurePassword from "secure-password"; -import util from "util"; -import { randomInt } from "node:crypto"; - -import prisma from "@wasp/server/dbClient.js"; -import { handleRejection } from "../utils.js"; -import HttpError from "@wasp/core/HttpError.js"; -import config from "../config.js"; - -const jwtSign = util.promisify(jwt.sign); -const jwtVerify = util.promisify(jwt.verify); - -const JWT_SECRET = config.auth.jwtSecret; - -export const sign = (id, options) => jwtSign({ id }, JWT_SECRET, options); -export const verify = (token) => jwtVerify(token, JWT_SECRET); - -const auth = handleRejection(async (req, res, next) => { - const authHeader = req.get("Authorization"); - if (!authHeader) { - // NOTE(matija): for now we let tokenless requests through and make it operation's - // responsibility to verify whether the request is authenticated or not. In the future - // we will develop our own system at Wasp-level for that. - return next(); - } - - if (authHeader.startsWith("Bearer ")) { - const token = authHeader.substring(7, authHeader.length); - req.user = await getUserFromToken(token); - } else { - throwInvalidCredentialsError(); - } - - next(); -}); - -export async function getUserFromToken(token) { - let userIdFromToken; - try { - userIdFromToken = (await verify(token)).id; - } catch (error) { - if ( - ["TokenExpiredError", "JsonWebTokenError", "NotBeforeError"].includes( - error.name - ) - ) { - throwInvalidCredentialsError(); - } else { - throw error; - } - } - - const user = await prisma.user.findUnique({ where: { id: userIdFromToken } }); - if (!user) { - throwInvalidCredentialsError(); - } - - // TODO: This logic must match the type in types/index.ts (if we remove the - // password field from the object here, we must to do the same there). - // Ideally, these two things would live in the same place: - // https://github.com/wasp-lang/wasp/issues/965 - const { password, ...userView } = user; - - return userView; -} - -const SP = new SecurePassword(); - -export const hashPassword = async (password) => { - const hashedPwdBuffer = await SP.hash(Buffer.from(password)); - return hashedPwdBuffer.toString("base64"); -}; - -export const verifyPassword = async (hashedPassword, password) => { - const result = await SP.verify( - Buffer.from(password), - Buffer.from(hashedPassword, "base64") - ); - if (result !== SecurePassword.VALID) { - throw new Error("Invalid password."); - } -}; - -// Generates an unused username that looks similar to "quick-purple-sheep-91231". -// It generates several options and ensures it picks one that is not currently in use. -export function generateAvailableDictionaryUsername() { - const adjectives = [ - "fuzzy", - "tall", - "short", - "nice", - "happy", - "quick", - "slow", - "good", - "new", - "old", - "first", - "last", - "old", - "young", - ]; - const colors = [ - "red", - "green", - "blue", - "white", - "black", - "brown", - "purple", - "orange", - "yellow", - ]; - const nouns = [ - "wasp", - "cat", - "dog", - "lion", - "rabbit", - "duck", - "pig", - "bee", - "goat", - "crab", - "fish", - "chicken", - "horse", - "llama", - "camel", - "sheep", - ]; - - const potentialUsernames = []; - for (let i = 0; i < 10; i++) { - const potentialUsername = `${adjectives[randomInt(adjectives.length)]}-${ - colors[randomInt(colors.length)] - }-${nouns[randomInt(nouns.length)]}-${randomInt(100_000)}`; - potentialUsernames.push(potentialUsername); - } - - return findAvailableUsername(potentialUsernames); -} - -// Generates an unused username based on an array of username segments and a separator. -// It generates several options and ensures it picks one that is not currently in use. -export function generateAvailableUsername(usernameSegments, config) { - const separator = config?.separator || "-"; - const baseUsername = usernameSegments.join(separator); - - const potentialUsernames = []; - for (let i = 0; i < 10; i++) { - const potentialUsername = `${baseUsername}${separator}${randomInt( - 100_000 - )}`; - potentialUsernames.push(potentialUsername); - } - - return findAvailableUsername(potentialUsernames); -} - -// Checks the database for an unused username from an array provided and returns first. -async function findAvailableUsername(potentialUsernames) { - const users = await prisma.user.findMany({ - where: { - username: { in: potentialUsernames }, - }, - }); - const takenUsernames = users.map((user) => user.username); - const availableUsernames = potentialUsernames.filter( - (username) => !takenUsernames.includes(username) - ); - - if (availableUsernames.length === 0) { - throw new Error( - "Unable to generate a unique username. Please contact Wasp." - ); - } - - return availableUsernames[0]; -} - -export function throwInvalidCredentialsError(message) { - throw new HttpError(401, "Invalid credentials", { message }); -} - -export default auth; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/queries/index.ts b/waspc/examples/todo-typescript/node_modules/@wasp/server/queries/index.ts deleted file mode 100644 index ad120b0991..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/server/queries/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import prisma from "@wasp/server/dbClient.js"; -import { getTasks as getTasksUser } from "@wasp/ext-src/queries.js"; - -export type GetTasks = typeof getTasksUser; - -export const getTasks = async (args, context) => { - return (getTasksUser as any)(args, { - ...context, - entities: { - Task: prisma.task, - }, - }); -}; diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/types/package.json b/waspc/examples/todo-typescript/node_modules/@wasp/types/package.json deleted file mode 100644 index c14a10c91f..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/types/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "types" -} \ No newline at end of file diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/universal/package.json b/waspc/examples/todo-typescript/node_modules/@wasp/universal/package.json deleted file mode 100644 index 12d29660b8..0000000000 --- a/waspc/examples/todo-typescript/node_modules/@wasp/universal/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "universal" -} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/api/events.ts b/waspc/examples/todo-typescript/node_modules/wasp/api/events.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/api/events.ts rename to waspc/examples/todo-typescript/node_modules/wasp/api/events.ts diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/api/index.ts b/waspc/examples/todo-typescript/node_modules/wasp/api/index.ts similarity index 63% rename from waspc/examples/todo-typescript/node_modules/@wasp/api/index.ts rename to waspc/examples/todo-typescript/node_modules/wasp/api/index.ts index 56ed992ec5..7c3b69d863 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/api/index.ts +++ b/waspc/examples/todo-typescript/node_modules/wasp/api/index.ts @@ -1,70 +1,70 @@ -import axios, { type AxiosError } from "axios"; +import axios, { type AxiosError } from 'axios' -import config from "@wasp/core/config"; -import { storage } from "@wasp/core/storage"; -import { apiEventsEmitter } from "@wasp/api/events"; +import config from 'wasp/core/config' +import { storage } from 'wasp/core/storage' +import { apiEventsEmitter } from 'wasp/api/events' -console.log("Top level node module"); +console.log('Top level node module') const api = axios.create({ baseURL: config.apiUrl, -}); +}) -const WASP_APP_AUTH_TOKEN_NAME = "authToken"; +const WASP_APP_AUTH_TOKEN_NAME = 'authToken' -let authToken = storage.get(WASP_APP_AUTH_TOKEN_NAME) as string | undefined; +let authToken = storage.get(WASP_APP_AUTH_TOKEN_NAME) as string | undefined export function setAuthToken(token: string): void { - authToken = token; - storage.set(WASP_APP_AUTH_TOKEN_NAME, token); - apiEventsEmitter.emit("authToken.set"); + authToken = token + storage.set(WASP_APP_AUTH_TOKEN_NAME, token) + apiEventsEmitter.emit('authToken.set') } export function getAuthToken(): string | undefined { - return authToken; + return authToken } export function clearAuthToken(): void { - authToken = undefined; - storage.remove(WASP_APP_AUTH_TOKEN_NAME); - apiEventsEmitter.emit("authToken.clear"); + authToken = undefined + storage.remove(WASP_APP_AUTH_TOKEN_NAME) + apiEventsEmitter.emit('authToken.clear') } export function removeLocalUserData(): void { - authToken = undefined; - storage.clear(); - apiEventsEmitter.emit("authToken.clear"); + authToken = undefined + storage.clear() + apiEventsEmitter.emit('authToken.clear') } api.interceptors.request.use((request) => { if (authToken) { - request.headers["Authorization"] = `Bearer ${authToken}`; + request.headers['Authorization'] = `Bearer ${authToken}` } - return request; -}); + return request +}) api.interceptors.response.use(undefined, (error) => { if (error.response?.status === 401) { - clearAuthToken(); + clearAuthToken() } - return Promise.reject(error); -}); + return Promise.reject(error) +}) // This handler will run on other tabs (not the active one calling API functions), // and will ensure they know about auth token changes. // Ref: https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event // "Note: This won't work on the same page that is making the changes — it is really a way // for other pages on the domain using the storage to sync any changes that are made." -window.addEventListener("storage", (event) => { +window.addEventListener('storage', (event) => { if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_TOKEN_NAME)) { if (!!event.newValue) { - authToken = event.newValue; - apiEventsEmitter.emit("authToken.set"); + authToken = event.newValue + apiEventsEmitter.emit('authToken.set') } else { - authToken = undefined; - apiEventsEmitter.emit("authToken.clear"); + authToken = undefined + apiEventsEmitter.emit('authToken.clear') } } -}); +}) /** * Takes an error returned by the app's API (as returned by axios), and transforms into a more @@ -82,29 +82,29 @@ export function handleApiError( // TODO: We might want to use HttpError here instead of just Error, since // HttpError is also used on server to throw errors like these. // That would require copying HttpError code to web-app also and using it here. - const responseJson = error.response?.data; - const responseStatusCode = error.response.status; + const responseJson = error.response?.data + const responseStatusCode = error.response.status throw new WaspHttpError( responseStatusCode, responseJson?.message ?? error.message, responseJson - ); + ) } else { // If any other error, we just propagate it. - throw error; + throw error } } class WaspHttpError extends Error { - statusCode: number; + statusCode: number - data: unknown; + data: unknown constructor(statusCode: number, message: string, data: unknown) { - super(message); - this.statusCode = statusCode; - this.data = data; + super(message) + this.statusCode = statusCode + this.data = data } } -export default api; +export default api diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/Auth.tsx b/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/Auth.tsx similarity index 60% rename from waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/Auth.tsx rename to waspc/examples/todo-typescript/node_modules/wasp/auth/forms/Auth.tsx index e84f0d3717..e532fd5d59 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/Auth.tsx +++ b/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/Auth.tsx @@ -1,63 +1,63 @@ -import React from "react"; -import { useState, createContext } from "react"; -import { createTheme } from "@stitches/react"; -import { styled } from "@wasp/core/stitches.config"; +import React from 'react' +import { useState, createContext } from 'react' +import { createTheme } from '@stitches/react' +import { styled } from 'wasp/core/stitches.config' import { type State, type CustomizationOptions, type ErrorMessage, -} from "./types"; -import { LoginSignupForm } from "./internal/common/LoginSignupForm"; -import { MessageError, MessageSuccess } from "./internal/Message"; +} from './types' +import { LoginSignupForm } from './internal/common/LoginSignupForm' +import { MessageError, MessageSuccess } from './internal/Message' const logoStyle = { - height: "3rem", -}; + height: '3rem', +} -const Container = styled("div", { - display: "flex", - flexDirection: "column", -}); +const Container = styled('div', { + display: 'flex', + flexDirection: 'column', +}) -const HeaderText = styled("h2", { - fontSize: "1.875rem", - fontWeight: "700", - marginTop: "1.5rem", -}); +const HeaderText = styled('h2', { + fontSize: '1.875rem', + fontWeight: '700', + marginTop: '1.5rem', +}) export const AuthContext = createContext({ isLoading: false, setIsLoading: (isLoading: boolean) => {}, setErrorMessage: (errorMessage: ErrorMessage | null) => {}, setSuccessMessage: (successMessage: string | null) => {}, -}); +}) export function Auth({ state, appearance, logo, - socialLayout = "horizontal", + socialLayout = 'horizontal', }: { - state: State; + state: State } & CustomizationOptions) { - const [errorMessage, setErrorMessage] = useState(null); - const [successMessage, setSuccessMessage] = useState(null); - const [isLoading, setIsLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(null) + const [successMessage, setSuccessMessage] = useState(null) + const [isLoading, setIsLoading] = useState(false) // TODO(matija): this is called on every render, is it a problem? // If we do it in useEffect(), then there is a glitch between the default color and the // user provided one. - const customTheme = createTheme(appearance ?? {}); + const customTheme = createTheme(appearance ?? {}) const titles: Record = { - login: "Log in to your account", - signup: "Create a new account", - }; - const title = titles[state]; + login: 'Log in to your account', + signup: 'Create a new account', + } + const title = titles[state] const socialButtonsDirection = - socialLayout === "vertical" ? "vertical" : "horizontal"; + socialLayout === 'vertical' ? 'vertical' : 'horizontal' return ( @@ -69,7 +69,7 @@ export function Auth({ {errorMessage && ( {errorMessage.title} - {errorMessage.description && ": "} + {errorMessage.description && ': '} {errorMessage.description} )} @@ -77,7 +77,7 @@ export function Auth({ - {(state === "login" || state === "signup") && ( + {(state === 'login' || state === 'signup') && ( - ); + ) } diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/Login.tsx b/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/Login.tsx similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/Login.tsx rename to waspc/examples/todo-typescript/node_modules/wasp/auth/forms/Login.tsx diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/Signup.tsx b/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/Signup.tsx similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/Signup.tsx rename to waspc/examples/todo-typescript/node_modules/wasp/auth/forms/Signup.tsx diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/Form.tsx b/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/Form.tsx new file mode 100644 index 0000000000..0feab2d646 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/Form.tsx @@ -0,0 +1,83 @@ +import { styled } from 'wasp/core/stitches.config' + +export const Form = styled('form', { + marginTop: '1.5rem', +}) + +export const FormItemGroup = styled('div', { + '& + div': { + marginTop: '1.5rem', + }, +}) + +export const FormLabel = styled('label', { + display: 'block', + fontSize: '$sm', + fontWeight: '500', +}) + +export const FormInput = styled('input', { + display: 'block', + lineHeight: '1.5rem', + fontSize: '$sm', + borderWidth: '1px', + borderColor: '$gray600', + backgroundColor: '#f8f4ff', + boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', + '&:focus': { + borderWidth: '1px', + borderColor: '$gray700', + boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', + }, + '&:disabled': { + opacity: 0.5, + cursor: 'not-allowed', + backgroundColor: '$gray400', + borderColor: '$gray400', + color: '$gray500', + }, + + borderRadius: '0.375rem', + width: '100%', + + paddingTop: '0.375rem', + paddingBottom: '0.375rem', + paddingLeft: '0.75rem', + paddingRight: '0.75rem', + + marginTop: '0.5rem', +}) + +export const SubmitButton = styled('button', { + display: 'flex', + justifyContent: 'center', + + width: '100%', + borderWidth: '1px', + borderColor: '$brand', + backgroundColor: '$brand', + color: '$submitButtonText', + + padding: '0.5rem 0.75rem', + boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', + + fontWeight: '600', + fontSize: '$sm', + lineHeight: '1.25rem', + borderRadius: '0.375rem', + + // TODO(matija): extract this into separate BaseButton component and then inherit it. + '&:hover': { + backgroundColor: '$brandAccent', + borderColor: '$brandAccent', + }, + '&:disabled': { + opacity: 0.5, + cursor: 'not-allowed', + backgroundColor: '$gray400', + borderColor: '$gray400', + color: '$gray500', + }, + transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)', + transitionDuration: '100ms', +}) diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/Message.tsx b/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/Message.tsx new file mode 100644 index 0000000000..7279ed2525 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/Message.tsx @@ -0,0 +1,18 @@ +import { styled } from 'wasp/core/stitches.config' + +export const Message = styled('div', { + padding: '0.5rem 0.75rem', + borderRadius: '0.375rem', + marginTop: '1rem', + background: '$gray400', +}) + +export const MessageError = styled(Message, { + background: '$errorBackground', + color: '$errorText', +}) + +export const MessageSuccess = styled(Message, { + background: '$successBackground', + color: '$successText', +}) diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/common/LoginSignupForm.tsx b/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/common/LoginSignupForm.tsx similarity index 64% rename from waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/common/LoginSignupForm.tsx rename to waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/common/LoginSignupForm.tsx index 5bfe0cc83c..084d91c802 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/common/LoginSignupForm.tsx +++ b/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/common/LoginSignupForm.tsx @@ -1,35 +1,35 @@ -import { useContext, type FormEvent } from "react"; -import { styled } from "@wasp/core/stitches.config"; -import config from "@wasp/core/config"; +import { useContext, type FormEvent } from 'react' +import { styled } from 'wasp/core/stitches.config' +import config from 'wasp/core/config' -import { AuthContext } from "../../Auth"; +import { AuthContext } from '../../Auth' import { Form, FormInput, FormItemGroup, FormLabel, SubmitButton, -} from "../Form"; -import { useHistory } from "react-router-dom"; -import { useUsernameAndPassword } from "../usernameAndPassword/useUsernameAndPassword"; +} from '../Form' +import { useHistory } from 'react-router-dom' +import { useUsernameAndPassword } from '../usernameAndPassword/useUsernameAndPassword' export const LoginSignupForm = ({ state, - socialButtonsDirection = "horizontal", + socialButtonsDirection = 'horizontal', }: { - state: "login" | "signup"; - socialButtonsDirection?: "horizontal" | "vertical"; + state: 'login' | 'signup' + socialButtonsDirection?: 'horizontal' | 'vertical' }) => { const { isLoading, setErrorMessage, setSuccessMessage, setIsLoading } = - useContext(AuthContext); - const cta = state === "login" ? "Log in" : "Sign up"; - const history = useHistory(); + useContext(AuthContext) + const cta = state === 'login' ? 'Log in' : 'Sign up' + const history = useHistory() const onErrorHandler = (error) => { setErrorMessage({ title: error.message, description: error.data?.data?.message, - }); - }; + }) + } const { handleSubmit, usernameFieldVal, @@ -37,21 +37,21 @@ export const LoginSignupForm = ({ setUsernameFieldVal, setPasswordFieldVal, } = useUsernameAndPassword({ - isLogin: state === "login", + isLogin: state === 'login', onError: onErrorHandler, onSuccess() { - history.push("/"); + history.push('/') }, - }); + }) async function onSubmit(event: FormEvent) { - event.preventDefault(); - setIsLoading(true); - setErrorMessage(null); - setSuccessMessage(null); + event.preventDefault() + setIsLoading(true) + setErrorMessage(null) + setSuccessMessage(null) try { - await handleSubmit(); + await handleSubmit() } finally { - setIsLoading(false); + setIsLoading(false) } } @@ -85,5 +85,5 @@ export const LoginSignupForm = ({ - ); -}; + ) +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts b/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts rename to waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/types.ts b/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/types.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/auth/forms/types.ts rename to waspc/examples/todo-typescript/node_modules/wasp/auth/forms/types.ts diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/helpers/user.ts b/waspc/examples/todo-typescript/node_modules/wasp/auth/helpers/user.ts similarity index 73% rename from waspc/examples/todo-typescript/node_modules/@wasp/auth/helpers/user.ts rename to waspc/examples/todo-typescript/node_modules/wasp/auth/helpers/user.ts index 6e2315eccc..5669f2f00e 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/auth/helpers/user.ts +++ b/waspc/examples/todo-typescript/node_modules/wasp/auth/helpers/user.ts @@ -1,8 +1,8 @@ -import { setAuthToken } from "@wasp/api"; -import { invalidateAndRemoveQueries } from "@wasp/operations/resources"; +import { setAuthToken } from 'wasp/api' +import { invalidateAndRemoveQueries } from 'wasp/operations/resources' export async function initSession(token: string): Promise { - setAuthToken(token); + setAuthToken(token) // We need to invalidate queries after login in order to get the correct user // data in the React components (using `useAuth`). // Redirects after login won't work properly without this. @@ -10,5 +10,5 @@ export async function initSession(token: string): Promise { // TODO(filip): We are currently removing all the queries, but we should // remove only non-public, user-dependent queries - public queries are // expected not to change in respect to the currently logged in user. - await invalidateAndRemoveQueries(); + await invalidateAndRemoveQueries() } diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/login.ts b/waspc/examples/todo-typescript/node_modules/wasp/auth/login.ts new file mode 100644 index 0000000000..3ba94755e5 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/wasp/auth/login.ts @@ -0,0 +1,16 @@ +import api, { handleApiError } from 'wasp/api' +import { initSession } from './helpers/user' + +export default async function login( + username: string, + password: string +): Promise { + try { + const args = { username, password } + const response = await api.post('/auth/local/login', args) + + await initSession(response.data.token) + } catch (error) { + handleApiError(error) + } +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/logout.ts b/waspc/examples/todo-typescript/node_modules/wasp/auth/logout.ts similarity index 52% rename from waspc/examples/todo-typescript/node_modules/@wasp/auth/logout.ts rename to waspc/examples/todo-typescript/node_modules/wasp/auth/logout.ts index 248a371e13..340e9dec9c 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/auth/logout.ts +++ b/waspc/examples/todo-typescript/node_modules/wasp/auth/logout.ts @@ -1,9 +1,9 @@ -import { removeLocalUserData } from "@wasp/api"; -import { invalidateAndRemoveQueries } from "@wasp/operations/resources"; +import { removeLocalUserData } from 'wasp/api' +import { invalidateAndRemoveQueries } from 'wasp/operations/resources' export default async function logout(): Promise { - removeLocalUserData(); + removeLocalUserData() // TODO(filip): We are currently invalidating and removing all the queries, but // we should remove only the non-public, user-dependent ones. - await invalidateAndRemoveQueries(); + await invalidateAndRemoveQueries() } diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/pages/createAuthRequiredPage.jsx b/waspc/examples/todo-typescript/node_modules/wasp/auth/pages/createAuthRequiredPage.jsx similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/auth/pages/createAuthRequiredPage.jsx rename to waspc/examples/todo-typescript/node_modules/wasp/auth/pages/createAuthRequiredPage.jsx diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/signup.ts b/waspc/examples/todo-typescript/node_modules/wasp/auth/signup.ts new file mode 100644 index 0000000000..86eff253e1 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/wasp/auth/signup.ts @@ -0,0 +1,12 @@ +import api, { handleApiError } from 'wasp/api' + +export default async function signup(userFields: { + username: string + password: string +}): Promise { + try { + await api.post('/auth/local/signup', userFields) + } catch (error) { + handleApiError(error) + } +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/auth/stitches.config.js b/waspc/examples/todo-typescript/node_modules/wasp/auth/stitches.config.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/auth/stitches.config.js rename to waspc/examples/todo-typescript/node_modules/wasp/auth/stitches.config.js diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/types.ts b/waspc/examples/todo-typescript/node_modules/wasp/auth/types.ts new file mode 100644 index 0000000000..861a4aefb2 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/wasp/auth/types.ts @@ -0,0 +1 @@ +export { type SanitizedUser as User } from 'wasp/server/_types/' diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/useAuth.ts b/waspc/examples/todo-typescript/node_modules/wasp/auth/useAuth.ts new file mode 100644 index 0000000000..ab40a41edb --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/wasp/auth/useAuth.ts @@ -0,0 +1,41 @@ +import { deserialize as superjsonDeserialize } from 'superjson' +import { useQuery } from 'wasp/rpc' +import api, { handleApiError } from 'wasp/api' +import { HttpMethod } from 'wasp/types' +import type { User } from './types' +import { addMetadataToQuery } from 'wasp/rpc/queries' + +export const getMe = createUserGetter() + +export default function useAuth(queryFnArgs?: unknown, config?: any) { + return useQuery(getMe, queryFnArgs, config) +} + +function createUserGetter() { + const getMeRelativePath = 'auth/me' + /* filip */ console.log('tu smo') + const getMeRoute = { method: HttpMethod.Get, path: `/${getMeRelativePath}` } + async function getMe(): Promise { + try { + const response = await api.get(getMeRoute.path) + + /* filip */ console.log('uspio') + return superjsonDeserialize(response.data) + } catch (error) { + /* filip */ console.log('failao') + if (error.response?.status === 401) { + } else { + handleApiError(error) + } + return null + } + } + + addMetadataToQuery(getMe, { + relativeQueryPath: getMeRelativePath, + queryRoute: getMeRoute, + entitiesUsed: ['User'], + }) + + return getMe +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/core/AuthError.js b/waspc/examples/todo-typescript/node_modules/wasp/core/AuthError.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/core/AuthError.js rename to waspc/examples/todo-typescript/node_modules/wasp/core/AuthError.js diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/core/HttpError.js b/waspc/examples/todo-typescript/node_modules/wasp/core/HttpError.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/core/HttpError.js rename to waspc/examples/todo-typescript/node_modules/wasp/core/HttpError.js diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/core/auth.js b/waspc/examples/todo-typescript/node_modules/wasp/core/auth.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/core/auth.js rename to waspc/examples/todo-typescript/node_modules/wasp/core/auth.js diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/core/auth/prismaMiddleware.js b/waspc/examples/todo-typescript/node_modules/wasp/core/auth/prismaMiddleware.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/core/auth/prismaMiddleware.js rename to waspc/examples/todo-typescript/node_modules/wasp/core/auth/prismaMiddleware.js diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/core/auth/validators.ts b/waspc/examples/todo-typescript/node_modules/wasp/core/auth/validators.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/core/auth/validators.ts rename to waspc/examples/todo-typescript/node_modules/wasp/core/auth/validators.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/core/config.js b/waspc/examples/todo-typescript/node_modules/wasp/core/config.js new file mode 100644 index 0000000000..fa4291d5e1 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/wasp/core/config.js @@ -0,0 +1,11 @@ +import { stripTrailingSlash } from 'wasp/universal/url' + +const apiUrl = + stripTrailingSlash(import.meta.env.REACT_APP_API_URL) || + 'http://localhost:3001' + +const config = { + apiUrl, +} + +export default config diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/core/stitches.config.js b/waspc/examples/todo-typescript/node_modules/wasp/core/stitches.config.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/core/stitches.config.js rename to waspc/examples/todo-typescript/node_modules/wasp/core/stitches.config.js diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/core/storage.ts b/waspc/examples/todo-typescript/node_modules/wasp/core/storage.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/core/storage.ts rename to waspc/examples/todo-typescript/node_modules/wasp/core/storage.ts diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/entities/index.ts b/waspc/examples/todo-typescript/node_modules/wasp/entities/index.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/entities/index.ts rename to waspc/examples/todo-typescript/node_modules/wasp/entities/index.ts diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/actions.ts b/waspc/examples/todo-typescript/node_modules/wasp/ext-src/actions.ts similarity index 87% rename from waspc/examples/todo-typescript/node_modules/@wasp/ext-src/actions.ts rename to waspc/examples/todo-typescript/node_modules/wasp/ext-src/actions.ts index 9b164b7fb0..c03bfac62b 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/actions.ts +++ b/waspc/examples/todo-typescript/node_modules/wasp/ext-src/actions.ts @@ -1,10 +1,10 @@ -import HttpError from '@wasp/core/HttpError' +import HttpError from 'wasp/core/HttpError' import type { CreateTask, UpdateTask, DeleteTasks, -} from '@wasp/server/actions/types' -import type { Task } from '@wasp/entities' +} from 'wasp/server/actions/types' +import type { Task } from 'wasp/entities' type CreateArgs = Pick diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/queries.ts b/waspc/examples/todo-typescript/node_modules/wasp/ext-src/queries.ts similarity index 71% rename from waspc/examples/todo-typescript/node_modules/@wasp/ext-src/queries.ts rename to waspc/examples/todo-typescript/node_modules/wasp/ext-src/queries.ts index a86ad1c3ee..ac49e0a7a7 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/ext-src/queries.ts +++ b/waspc/examples/todo-typescript/node_modules/wasp/ext-src/queries.ts @@ -1,6 +1,6 @@ -import HttpError from '@wasp/core/HttpError' -import type { GetTasks } from '@wasp/server/queries/types' -import type { Task } from '@wasp/entities' +import HttpError from 'wasp/core/HttpError' +import type { GetTasks } from 'wasp/server/queries/types' +import type { Task } from 'wasp/entities' //Using TypeScript's new 'satisfies' keyword, it will infer the types of the arguments and return value export const getTasks = ((_args, context) => { diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/operations/index.ts b/waspc/examples/todo-typescript/node_modules/wasp/operations/index.ts similarity index 66% rename from waspc/examples/todo-typescript/node_modules/@wasp/operations/index.ts rename to waspc/examples/todo-typescript/node_modules/wasp/operations/index.ts index c2dfee5c6e..b9f0e2be4a 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/operations/index.ts +++ b/waspc/examples/todo-typescript/node_modules/wasp/operations/index.ts @@ -1,27 +1,27 @@ -import api, { handleApiError } from "@wasp/api"; -import { HttpMethod } from "@wasp/types"; +import api, { handleApiError } from 'wasp/api' +import { HttpMethod } from 'wasp/types' import { serialize as superjsonSerialize, deserialize as superjsonDeserialize, -} from "superjson"; +} from 'superjson' -export type OperationRoute = { method: HttpMethod; path: string }; +export type OperationRoute = { method: HttpMethod; path: string } export async function callOperation( operationRoute: OperationRoute & { method: HttpMethod.Post }, args: any ) { try { - const superjsonArgs = superjsonSerialize(args); - const response = await api.post(operationRoute.path, superjsonArgs); - return superjsonDeserialize(response.data); + const superjsonArgs = superjsonSerialize(args) + const response = await api.post(operationRoute.path, superjsonArgs) + return superjsonDeserialize(response.data) } catch (error) { - handleApiError(error); + handleApiError(error) } } export function makeOperationRoute( relativeOperationRoute: string ): OperationRoute { - return { method: HttpMethod.Post, path: `/${relativeOperationRoute}` }; + return { method: HttpMethod.Post, path: `/${relativeOperationRoute}` } } diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/operations/resources.js b/waspc/examples/todo-typescript/node_modules/wasp/operations/resources.js similarity index 74% rename from waspc/examples/todo-typescript/node_modules/@wasp/operations/resources.js rename to waspc/examples/todo-typescript/node_modules/wasp/operations/resources.js index cb328cb940..d7efa3fc1d 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/operations/resources.js +++ b/waspc/examples/todo-typescript/node_modules/wasp/operations/resources.js @@ -1,13 +1,13 @@ -import { queryClientInitialized } from "@wasp/rpc/queryClient"; -import { makeUpdateHandlersMap } from "./updateHandlersMap"; -import { hashQueryKey } from "@tanstack/react-query"; +import { queryClientInitialized } from 'wasp/rpc/queryClient' +import { makeUpdateHandlersMap } from './updateHandlersMap' +import { hashQueryKey } from '@tanstack/react-query' // Map where key is resource name and value is Set // containing query ids of all the queries that use // that resource. -const resourceToQueryCacheKeys = new Map(); +const resourceToQueryCacheKeys = new Map() -const updateHandlers = makeUpdateHandlersMap(hashQueryKey); +const updateHandlers = makeUpdateHandlersMap(hashQueryKey) /** * Remembers that specified query is using specified resources. * If called multiple times for same query, resources are added, not reset. @@ -16,35 +16,35 @@ const updateHandlers = makeUpdateHandlersMap(hashQueryKey); */ export function addResourcesUsedByQuery(queryCacheKey, resources) { for (const resource of resources) { - let cacheKeys = resourceToQueryCacheKeys.get(resource); + let cacheKeys = resourceToQueryCacheKeys.get(resource) if (!cacheKeys) { - cacheKeys = new Set(); - resourceToQueryCacheKeys.set(resource, cacheKeys); + cacheKeys = new Set() + resourceToQueryCacheKeys.set(resource, cacheKeys) } - cacheKeys.add(queryCacheKey); + cacheKeys.add(queryCacheKey) } } export function registerActionInProgress(optimisticUpdateTuples) { optimisticUpdateTuples.forEach(({ queryKey, updateQuery }) => updateHandlers.add(queryKey, updateQuery) - ); + ) } export async function registerActionDone(resources, optimisticUpdateTuples) { optimisticUpdateTuples.forEach(({ queryKey }) => updateHandlers.remove(queryKey) - ); - await invalidateQueriesUsing(resources); + ) + await invalidateQueriesUsing(resources) } export function getActiveOptimisticUpdates(queryKey) { - return updateHandlers.getUpdateHandlers(queryKey); + return updateHandlers.getUpdateHandlers(queryKey) } export async function invalidateAndRemoveQueries() { - console.log(queryClientInitialized); - const queryClient = await queryClientInitialized; + console.log(queryClientInitialized) + const queryClient = await queryClientInitialized // If we don't reset the queries before removing them, Wasp will stay on // the same page. The user would have to manually refresh the page to "finish" // logging out. @@ -52,10 +52,10 @@ export async function invalidateAndRemoveQueries() { // that are using the query are not re-rendered. This is why we need to reset // the queries, so that the `Observer` is re-created and the components are re-rendered. // For more details: https://github.com/wasp-lang/wasp/pull/1014/files#r1111862125 - queryClient.resetQueries(); + queryClient.resetQueries() // If we don't remove the queries after invalidating them, the old query data // remains in the cache, casuing a potential privacy issue. - queryClient.removeQueries(); + queryClient.removeQueries() } /** @@ -63,13 +63,13 @@ export async function invalidateAndRemoveQueries() { * @param {string[]} resources - Names of resources. */ async function invalidateQueriesUsing(resources) { - console.log(queryClientInitialized); - const queryClient = await queryClientInitialized; + console.log(queryClientInitialized) + const queryClient = await queryClientInitialized - const queryCacheKeysToInvalidate = getQueriesUsingResources(resources); + const queryCacheKeysToInvalidate = getQueriesUsingResources(resources) queryCacheKeysToInvalidate.forEach((queryCacheKey) => queryClient.invalidateQueries(queryCacheKey) - ); + ) } /** @@ -77,9 +77,9 @@ async function invalidateQueriesUsing(resources) { * @returns {string[]} Array of "query cache keys" of queries that use specified resource. */ function getQueriesUsingResource(resource) { - return Array.from(resourceToQueryCacheKeys.get(resource) || []); + return Array.from(resourceToQueryCacheKeys.get(resource) || []) } function getQueriesUsingResources(resources) { - return Array.from(new Set(resources.flatMap(getQueriesUsingResource))); + return Array.from(new Set(resources.flatMap(getQueriesUsingResource))) } diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/operations/updateHandlersMap.js b/waspc/examples/todo-typescript/node_modules/wasp/operations/updateHandlersMap.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/operations/updateHandlersMap.js rename to waspc/examples/todo-typescript/node_modules/wasp/operations/updateHandlersMap.js diff --git a/waspc/examples/todo-typescript/node_modules/wasp/package.json b/waspc/examples/todo-typescript/node_modules/wasp/package.json new file mode 100644 index 0000000000..1290aa1f60 --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/wasp/package.json @@ -0,0 +1,33 @@ +{ + "name": "wasp", + "type": "module", + "version": "1.0.0", + "description": "", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "types": "tsc --declaration --emitDeclarationOnly --stripInternal --declarationDir dist" + }, + "exports": { + "./core/HttpError": "./core/HttpError.js", + "./core/config": "./core/config.js", + "./core/stitches.config": "./core/stitches.config.js", + "./core/storage": "./core/storage.ts", + "./rpc": "./rpc/index.ts", + "./rpc/queries": "./rpc/queries/index.ts", + "./rpc/actions": "./rpc/actions/index.ts", + "./rpc/queryClient": "./rpc/queryClient.ts", + "./types": "./types/index.ts", + "./auth/*": "./auth/*", + "./api": "./api/index.ts", + "./api/*": "./api/*", + "./operations": "./operations/index.ts", + "./operations/*": "./operations/*", + "./universal/url": "./universal/url.ts", + "./universal/types": "./universal/url.ts" + }, + "author": "", + "license": "ISC", + "include": [ + "src/**/*" + ] +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/core.d.ts b/waspc/examples/todo-typescript/node_modules/wasp/rpc/actions/core.d.ts similarity index 84% rename from waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/core.d.ts rename to waspc/examples/todo-typescript/node_modules/wasp/rpc/actions/core.d.ts index c5d5d40d3f..ea41a0eed3 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/core.d.ts +++ b/waspc/examples/todo-typescript/node_modules/wasp/rpc/actions/core.d.ts @@ -1,5 +1,5 @@ import { type Action } from '.' -import type { Expand, _Awaited, _ReturnType } from '@wasp/universal/types' +import type { Expand, _Awaited, _ReturnType } from 'wasp/universal/types' export function createAction( actionRoute: string, diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/core.js b/waspc/examples/todo-typescript/node_modules/wasp/rpc/actions/core.js similarity index 77% rename from waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/core.js rename to waspc/examples/todo-typescript/node_modules/wasp/rpc/actions/core.js index 19c87efe38..62c65ff4a3 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/core.js +++ b/waspc/examples/todo-typescript/node_modules/wasp/rpc/actions/core.js @@ -1,26 +1,26 @@ -import { callOperation, makeOperationRoute } from "@wasp/operations"; +import { callOperation, makeOperationRoute } from 'wasp/operations' import { registerActionInProgress, registerActionDone, -} from "@wasp/operations/resources"; +} from 'wasp/operations/resources' // todo - turn helpers and core into the same thing export function createAction(relativeActionRoute, entitiesUsed) { - const actionRoute = makeOperationRoute(relativeActionRoute); + const actionRoute = makeOperationRoute(relativeActionRoute) async function internalAction(args, specificOptimisticUpdateDefinitions) { - registerActionInProgress(specificOptimisticUpdateDefinitions); + registerActionInProgress(specificOptimisticUpdateDefinitions) try { // The `return await` is not redundant here. If we removed the await, the // `finally` block would execute before the action finishes, prematurely // registering the action as done. - return await callOperation(actionRoute, args); + return await callOperation(actionRoute, args) } finally { await registerActionDone( entitiesUsed, specificOptimisticUpdateDefinitions - ); + ) } } @@ -33,8 +33,8 @@ export function createAction(relativeActionRoute, entitiesUsed) { // While it does technically allow our users to access the interal API, it // shouldn't be a problem in practice. Still, if it turns out to be a problem, // we can always hide it using a Symbol. - const action = (args) => internalAction(args, []); - action.internal = internalAction; + const action = (args) => internalAction(args, []) + action.internal = internalAction - return action; + return action } diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/index.ts b/waspc/examples/todo-typescript/node_modules/wasp/rpc/actions/index.ts similarity index 83% rename from waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/index.ts rename to waspc/examples/todo-typescript/node_modules/wasp/rpc/actions/index.ts index 12431ca158..2be33b3d65 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/actions/index.ts +++ b/waspc/examples/todo-typescript/node_modules/wasp/rpc/actions/index.ts @@ -1,5 +1,5 @@ import { createAction } from './core' -import { CreateTask, UpdateTask } from '@wasp/server/actions' +import { CreateTask, UpdateTask } from 'wasp/server/actions' export const updateTask = createAction('operations/update-task', [ 'Task', diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/index.ts b/waspc/examples/todo-typescript/node_modules/wasp/rpc/index.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/rpc/index.ts rename to waspc/examples/todo-typescript/node_modules/wasp/rpc/index.ts diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/core.d.ts b/waspc/examples/todo-typescript/node_modules/wasp/rpc/queries/core.d.ts similarity index 71% rename from waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/core.d.ts rename to waspc/examples/todo-typescript/node_modules/wasp/rpc/queries/core.d.ts index a358ac02b7..ddbb4f2b8e 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/core.d.ts +++ b/waspc/examples/todo-typescript/node_modules/wasp/rpc/queries/core.d.ts @@ -1,6 +1,6 @@ import { type Query } from '..' -import { Route } from '@wasp/types'; -import type { Expand, _Awaited, _ReturnType } from '@wasp/universal/types' +import { Route } from 'wasp/types' +import type { Expand, _Awaited, _ReturnType } from 'wasp/universal/types' export function createQuery( queryRoute: string, @@ -10,10 +10,10 @@ export function createQuery( export function addMetadataToQuery( query: (...args: any[]) => Promise, metadata: { - relativeQueryPath: string; - queryRoute: Route; - entitiesUsed: string[]; - }, + relativeQueryPath: string + queryRoute: Route + entitiesUsed: string[] + } ): void type QueryFor = Expand< diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/core.js b/waspc/examples/todo-typescript/node_modules/wasp/rpc/queries/core.js similarity index 60% rename from waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/core.js rename to waspc/examples/todo-typescript/node_modules/wasp/rpc/queries/core.js index 00974ffa08..68cc338a31 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queries/core.js +++ b/waspc/examples/todo-typescript/node_modules/wasp/rpc/queries/core.js @@ -1,30 +1,30 @@ -import { callOperation, makeOperationRoute } from "@wasp/operations"; +import { callOperation, makeOperationRoute } from 'wasp/operations' import { addResourcesUsedByQuery, getActiveOptimisticUpdates, -} from "@wasp/operations/resources"; +} from 'wasp/operations/resources' export function createQuery(relativeQueryPath, entitiesUsed) { - const queryRoute = makeOperationRoute(relativeQueryPath); + const queryRoute = makeOperationRoute(relativeQueryPath) async function query(queryKey, queryArgs) { - const serverResult = await callOperation(queryRoute, queryArgs); + const serverResult = await callOperation(queryRoute, queryArgs) return getActiveOptimisticUpdates(queryKey).reduce( (result, update) => update(result), serverResult - ); + ) } - addMetadataToQuery(query, { relativeQueryPath, queryRoute, entitiesUsed }); + addMetadataToQuery(query, { relativeQueryPath, queryRoute, entitiesUsed }) - return query; + return query } export function addMetadataToQuery( query, { relativeQueryPath, queryRoute, entitiesUsed } ) { - query.queryCacheKey = [relativeQueryPath]; - query.route = queryRoute; - addResourcesUsedByQuery(query.queryCacheKey, entitiesUsed); + query.queryCacheKey = [relativeQueryPath] + query.route = queryRoute + addResourcesUsedByQuery(query.queryCacheKey, entitiesUsed) } diff --git a/waspc/examples/todo-typescript/node_modules/wasp/rpc/queries/index.ts b/waspc/examples/todo-typescript/node_modules/wasp/rpc/queries/index.ts new file mode 100644 index 0000000000..a03221553d --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/wasp/rpc/queries/index.ts @@ -0,0 +1,6 @@ +import { createQuery } from './core' +import { GetTasks } from 'wasp/server/queries' + +export const getTasks = createQuery('operations/get-tasks', ['Task']) + +export { addMetadataToQuery } from './core' diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/rpc/queryClient.ts b/waspc/examples/todo-typescript/node_modules/wasp/rpc/queryClient.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/rpc/queryClient.ts rename to waspc/examples/todo-typescript/node_modules/wasp/rpc/queryClient.ts diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/_types/index.ts b/waspc/examples/todo-typescript/node_modules/wasp/server/_types/index.ts similarity index 67% rename from waspc/examples/todo-typescript/node_modules/@wasp/server/_types/index.ts rename to waspc/examples/todo-typescript/node_modules/wasp/server/_types/index.ts index 186513b293..e6307559c9 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/server/_types/index.ts +++ b/waspc/examples/todo-typescript/node_modules/wasp/server/_types/index.ts @@ -1,40 +1,40 @@ -import { type Request, type Response } from "express"; +import { type Request, type Response } from 'express' import { type ParamsDictionary as ExpressParams, type Query as ExpressQuery, -} from "express-serve-static-core"; -import { type Expand } from "@wasp/universal/types.js"; -import prisma from "@wasp/server/dbClient.js"; -import { type User } from "@wasp/entities"; -import { type _Entity } from "./taggedEntities"; -import { type Payload } from "./serialization"; +} from 'express-serve-static-core' +import { type Expand } from 'wasp/universal/types.js' +import prisma from 'wasp/server/dbClient.js' +import { type User } from 'wasp/entities' +import { type _Entity } from './taggedEntities' +import { type Payload } from './serialization' -export * from "./taggedEntities"; -export * from "./serialization"; +export * from './taggedEntities' +export * from './serialization' export type Query< Entities extends _Entity[], Input extends Payload, Output extends Payload -> = Operation; +> = Operation export type Action< Entities extends _Entity[], Input extends Payload, Output extends Payload -> = Operation; +> = Operation export type AuthenticatedQuery< Entities extends _Entity[], Input extends Payload, Output extends Payload -> = AuthenticatedOperation; +> = AuthenticatedOperation export type AuthenticatedAction< Entities extends _Entity[], Input extends Payload, Output extends Payload -> = AuthenticatedOperation; +> = AuthenticatedOperation type AuthenticatedOperation< Entities extends _Entity[], @@ -43,7 +43,7 @@ type AuthenticatedOperation< > = ( args: Input, context: ContextWithUser -) => Output | Promise; +) => Output | Promise export type AuthenticatedApi< Entities extends _Entity[], @@ -56,12 +56,12 @@ export type AuthenticatedApi< req: Request, res: Response, context: ContextWithUser -) => void; +) => void type Operation = ( args: Input, context: Context -) => Output | Promise; +) => Output | Promise export type Api< Entities extends _Entity[], @@ -74,27 +74,27 @@ export type Api< req: Request, res: Response, context: Context -) => void; +) => void type EntityMap = { - [EntityName in Entities[number]["_entityName"]]: PrismaDelegate[EntityName]; -}; + [EntityName in Entities[number]['_entityName']]: PrismaDelegate[EntityName] +} type PrismaDelegate = { - User: typeof prisma.user; - Task: typeof prisma.task; -}; + User: typeof prisma.user + Task: typeof prisma.task +} type Context = Expand<{ - entities: Expand>; -}>; + entities: Expand> +}> type ContextWithUser = Expand< Context & { user?: SanitizedUser } ->; +> // TODO: This type must match the logic in core/auth.js (if we remove the // password field from the object there, we must do the same here). Ideally, // these two things would live in the same place: // https://github.com/wasp-lang/wasp/issues/965 -export type SanitizedUser = Omit; +export type SanitizedUser = Omit diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/_types/serialization.ts b/waspc/examples/todo-typescript/node_modules/wasp/server/_types/serialization.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/server/_types/serialization.ts rename to waspc/examples/todo-typescript/node_modules/wasp/server/_types/serialization.ts diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/_types/taggedEntities.ts b/waspc/examples/todo-typescript/node_modules/wasp/server/_types/taggedEntities.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/server/_types/taggedEntities.ts rename to waspc/examples/todo-typescript/node_modules/wasp/server/_types/taggedEntities.ts diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/index.ts b/waspc/examples/todo-typescript/node_modules/wasp/server/actions/index.ts similarity index 90% rename from waspc/examples/todo-typescript/node_modules/@wasp/server/actions/index.ts rename to waspc/examples/todo-typescript/node_modules/wasp/server/actions/index.ts index 1efb502f0a..5f9800cf8d 100644 --- a/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/index.ts +++ b/waspc/examples/todo-typescript/node_modules/wasp/server/actions/index.ts @@ -1,9 +1,9 @@ -import prisma from '@wasp/server/dbClient.js' +import prisma from 'wasp/server/dbClient.js' import { updateTask as updateTaskUser, createTask as createTaskUser, deleteTasks as deleteTasksUser, -} from '@wasp/ext-src/actions.js' +} from 'wasp/ext-src/actions.js' export type UpdateTask = typeof updateTask diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/actions/types.ts b/waspc/examples/todo-typescript/node_modules/wasp/server/actions/types.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/server/actions/types.ts rename to waspc/examples/todo-typescript/node_modules/wasp/server/actions/types.ts diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/core/AuthError.js b/waspc/examples/todo-typescript/node_modules/wasp/server/core/AuthError.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/server/core/AuthError.js rename to waspc/examples/todo-typescript/node_modules/wasp/server/core/AuthError.js diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/core/HttpError.js b/waspc/examples/todo-typescript/node_modules/wasp/server/core/HttpError.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/server/core/HttpError.js rename to waspc/examples/todo-typescript/node_modules/wasp/server/core/HttpError.js diff --git a/waspc/examples/todo-typescript/node_modules/wasp/server/core/auth.js b/waspc/examples/todo-typescript/node_modules/wasp/server/core/auth.js new file mode 100644 index 0000000000..11d331e48c --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/wasp/server/core/auth.js @@ -0,0 +1,185 @@ +import jwt from 'jsonwebtoken' +import SecurePassword from 'secure-password' +import util from 'util' +import { randomInt } from 'node:crypto' + +import prisma from 'wasp/server/dbClient.js' +import { handleRejection } from '../utils.js' +import HttpError from 'wasp/core/HttpError.js' +import config from '../config.js' + +const jwtSign = util.promisify(jwt.sign) +const jwtVerify = util.promisify(jwt.verify) + +const JWT_SECRET = config.auth.jwtSecret + +export const sign = (id, options) => jwtSign({ id }, JWT_SECRET, options) +export const verify = (token) => jwtVerify(token, JWT_SECRET) + +const auth = handleRejection(async (req, res, next) => { + const authHeader = req.get('Authorization') + if (!authHeader) { + // NOTE(matija): for now we let tokenless requests through and make it operation's + // responsibility to verify whether the request is authenticated or not. In the future + // we will develop our own system at Wasp-level for that. + return next() + } + + if (authHeader.startsWith('Bearer ')) { + const token = authHeader.substring(7, authHeader.length) + req.user = await getUserFromToken(token) + } else { + throwInvalidCredentialsError() + } + + next() +}) + +export async function getUserFromToken(token) { + let userIdFromToken + try { + userIdFromToken = (await verify(token)).id + } catch (error) { + if ( + ['TokenExpiredError', 'JsonWebTokenError', 'NotBeforeError'].includes( + error.name + ) + ) { + throwInvalidCredentialsError() + } else { + throw error + } + } + + const user = await prisma.user.findUnique({ where: { id: userIdFromToken } }) + if (!user) { + throwInvalidCredentialsError() + } + + // TODO: This logic must match the type in types/index.ts (if we remove the + // password field from the object here, we must to do the same there). + // Ideally, these two things would live in the same place: + // https://github.com/wasp-lang/wasp/issues/965 + const { password, ...userView } = user + + return userView +} + +const SP = new SecurePassword() + +export const hashPassword = async (password) => { + const hashedPwdBuffer = await SP.hash(Buffer.from(password)) + return hashedPwdBuffer.toString('base64') +} + +export const verifyPassword = async (hashedPassword, password) => { + const result = await SP.verify( + Buffer.from(password), + Buffer.from(hashedPassword, 'base64') + ) + if (result !== SecurePassword.VALID) { + throw new Error('Invalid password.') + } +} + +// Generates an unused username that looks similar to "quick-purple-sheep-91231". +// It generates several options and ensures it picks one that is not currently in use. +export function generateAvailableDictionaryUsername() { + const adjectives = [ + 'fuzzy', + 'tall', + 'short', + 'nice', + 'happy', + 'quick', + 'slow', + 'good', + 'new', + 'old', + 'first', + 'last', + 'old', + 'young', + ] + const colors = [ + 'red', + 'green', + 'blue', + 'white', + 'black', + 'brown', + 'purple', + 'orange', + 'yellow', + ] + const nouns = [ + 'wasp', + 'cat', + 'dog', + 'lion', + 'rabbit', + 'duck', + 'pig', + 'bee', + 'goat', + 'crab', + 'fish', + 'chicken', + 'horse', + 'llama', + 'camel', + 'sheep', + ] + + const potentialUsernames = [] + for (let i = 0; i < 10; i++) { + const potentialUsername = `${adjectives[randomInt(adjectives.length)]}-${ + colors[randomInt(colors.length)] + }-${nouns[randomInt(nouns.length)]}-${randomInt(100_000)}` + potentialUsernames.push(potentialUsername) + } + + return findAvailableUsername(potentialUsernames) +} + +// Generates an unused username based on an array of username segments and a separator. +// It generates several options and ensures it picks one that is not currently in use. +export function generateAvailableUsername(usernameSegments, config) { + const separator = config?.separator || '-' + const baseUsername = usernameSegments.join(separator) + + const potentialUsernames = [] + for (let i = 0; i < 10; i++) { + const potentialUsername = `${baseUsername}${separator}${randomInt(100_000)}` + potentialUsernames.push(potentialUsername) + } + + return findAvailableUsername(potentialUsernames) +} + +// Checks the database for an unused username from an array provided and returns first. +async function findAvailableUsername(potentialUsernames) { + const users = await prisma.user.findMany({ + where: { + username: { in: potentialUsernames }, + }, + }) + const takenUsernames = users.map((user) => user.username) + const availableUsernames = potentialUsernames.filter( + (username) => !takenUsernames.includes(username) + ) + + if (availableUsernames.length === 0) { + throw new Error( + 'Unable to generate a unique username. Please contact Wasp.' + ) + } + + return availableUsernames[0] +} + +export function throwInvalidCredentialsError(message) { + throw new HttpError(401, 'Invalid credentials', { message }) +} + +export default auth diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth/prismaMiddleware.js b/waspc/examples/todo-typescript/node_modules/wasp/server/core/auth/prismaMiddleware.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth/prismaMiddleware.js rename to waspc/examples/todo-typescript/node_modules/wasp/server/core/auth/prismaMiddleware.js diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth/validators.ts b/waspc/examples/todo-typescript/node_modules/wasp/server/core/auth/validators.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/server/core/auth/validators.ts rename to waspc/examples/todo-typescript/node_modules/wasp/server/core/auth/validators.ts diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/dbClient.ts b/waspc/examples/todo-typescript/node_modules/wasp/server/dbClient.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/server/dbClient.ts rename to waspc/examples/todo-typescript/node_modules/wasp/server/dbClient.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/server/queries/index.ts b/waspc/examples/todo-typescript/node_modules/wasp/server/queries/index.ts new file mode 100644 index 0000000000..3c49adc9dc --- /dev/null +++ b/waspc/examples/todo-typescript/node_modules/wasp/server/queries/index.ts @@ -0,0 +1,13 @@ +import prisma from 'wasp/server/dbClient.js' +import { getTasks as getTasksUser } from 'wasp/ext-src/queries.js' + +export type GetTasks = typeof getTasksUser + +export const getTasks = async (args, context) => { + return (getTasksUser as any)(args, { + ...context, + entities: { + Task: prisma.task, + }, + }) +} diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/server/queries/types.ts b/waspc/examples/todo-typescript/node_modules/wasp/server/queries/types.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/server/queries/types.ts rename to waspc/examples/todo-typescript/node_modules/wasp/server/queries/types.ts diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/types/index.ts b/waspc/examples/todo-typescript/node_modules/wasp/types/index.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/types/index.ts rename to waspc/examples/todo-typescript/node_modules/wasp/types/index.ts diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/universal/types.ts b/waspc/examples/todo-typescript/node_modules/wasp/universal/types.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/universal/types.ts rename to waspc/examples/todo-typescript/node_modules/wasp/universal/types.ts diff --git a/waspc/examples/todo-typescript/node_modules/@wasp/universal/url.ts b/waspc/examples/todo-typescript/node_modules/wasp/universal/url.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/@wasp/universal/url.ts rename to waspc/examples/todo-typescript/node_modules/wasp/universal/url.ts diff --git a/waspc/examples/todo-typescript/src/MainPage.tsx b/waspc/examples/todo-typescript/src/MainPage.tsx index 085530736c..b74c3e2a42 100644 --- a/waspc/examples/todo-typescript/src/MainPage.tsx +++ b/waspc/examples/todo-typescript/src/MainPage.tsx @@ -1,12 +1,11 @@ import './Main.css' import React, { useEffect, FormEventHandler, FormEvent } from 'react' -import logout from '@wasp/auth/logout' -import useAuth from '@wasp/auth/useAuth' -import { useQuery, useAction } from '@wasp/rpc' // Wasp uses a thin wrapper around react-query -import { getTasks } from '@wasp/rpc/queries' -import { createTask, updateTask, deleteTasks } from '@wasp/rpc/actions' +import logout from 'wasp/auth/logout' +import { useQuery, useAction } from 'wasp/rpc' // Wasp uses a thin wrapper around react-query +import { getTasks } from 'wasp/rpc/queries' +import { createTask, updateTask, deleteTasks } from 'wasp/rpc/actions' import waspLogo from './waspLogo.png' -import type { Task, User } from '@wasp/entities' +import type { Task, User } from 'wasp/entities' export const MainPage = ({ user }: { user: User }) => { const { data: tasks, isLoading, error } = useQuery(getTasks) diff --git a/waspc/examples/todo-typescript/src/task/actions.ts b/waspc/examples/todo-typescript/src/task/actions.ts index 9b164b7fb0..c03bfac62b 100644 --- a/waspc/examples/todo-typescript/src/task/actions.ts +++ b/waspc/examples/todo-typescript/src/task/actions.ts @@ -1,10 +1,10 @@ -import HttpError from '@wasp/core/HttpError' +import HttpError from 'wasp/core/HttpError' import type { CreateTask, UpdateTask, DeleteTasks, -} from '@wasp/server/actions/types' -import type { Task } from '@wasp/entities' +} from 'wasp/server/actions/types' +import type { Task } from 'wasp/entities' type CreateArgs = Pick diff --git a/waspc/examples/todo-typescript/src/task/queries.ts b/waspc/examples/todo-typescript/src/task/queries.ts index a86ad1c3ee..ac49e0a7a7 100644 --- a/waspc/examples/todo-typescript/src/task/queries.ts +++ b/waspc/examples/todo-typescript/src/task/queries.ts @@ -1,6 +1,6 @@ -import HttpError from '@wasp/core/HttpError' -import type { GetTasks } from '@wasp/server/queries/types' -import type { Task } from '@wasp/entities' +import HttpError from 'wasp/core/HttpError' +import type { GetTasks } from 'wasp/server/queries/types' +import type { Task } from 'wasp/entities' //Using TypeScript's new 'satisfies' keyword, it will infer the types of the arguments and return value export const getTasks = ((_args, context) => { diff --git a/waspc/examples/todo-typescript/src/user/LoginPage.tsx b/waspc/examples/todo-typescript/src/user/LoginPage.tsx index 98281ca6b0..fa198154ab 100644 --- a/waspc/examples/todo-typescript/src/user/LoginPage.tsx +++ b/waspc/examples/todo-typescript/src/user/LoginPage.tsx @@ -1,5 +1,5 @@ -import { Link } from "react-router-dom"; -import { LoginForm } from "@wasp/auth/forms/Login"; +import { Link } from 'react-router-dom' +import { LoginForm } from 'wasp/auth/forms/Login' export function LoginPage() { return ( @@ -13,5 +13,5 @@ export function LoginPage() { I don't have an account yet (go to signup).
    - ); + ) } diff --git a/waspc/examples/todo-typescript/src/user/SignupPage.tsx b/waspc/examples/todo-typescript/src/user/SignupPage.tsx index a04b901f0f..e3599729d6 100644 --- a/waspc/examples/todo-typescript/src/user/SignupPage.tsx +++ b/waspc/examples/todo-typescript/src/user/SignupPage.tsx @@ -1,5 +1,5 @@ -import { Link } from "react-router-dom"; -import { SignupForm } from "@wasp/auth/forms/Signup"; +import { Link } from 'react-router-dom' +import { SignupForm } from 'wasp/auth/forms/Signup' export function SignupPage() { return ( @@ -13,5 +13,5 @@ export function SignupPage() { I already have an account (go to login). - ); + ) } From 49c85e0863ea3c68a28318a4fb86ce856f17d0b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Fri, 8 Dec 2023 13:33:00 +0100 Subject: [PATCH 18/54] Generate SDK from templates in prototype --- waspc/data/Cli/templates/basic/package.json | 10 ++ .../Generator/templates/sdk/dependencies.txt | 127 ++++++++++++++++++ .../data/Generator/templates/sdk/package.json | 35 +++++ .../templates/sdk}/wasp/api/events.ts | 0 .../templates/sdk}/wasp/api/index.ts | 0 .../templates/sdk}/wasp/auth/forms/Auth.tsx | 0 .../templates/sdk}/wasp/auth/forms/Login.tsx | 0 .../templates/sdk}/wasp/auth/forms/Signup.tsx | 0 .../sdk}/wasp/auth/forms/internal/Form.tsx | 0 .../sdk}/wasp/auth/forms/internal/Message.tsx | 0 .../forms/internal/common/LoginSignupForm.tsx | 0 .../useUsernameAndPassword.ts | 0 .../templates/sdk}/wasp/auth/forms/types.ts | 0 .../templates/sdk}/wasp/auth/helpers/user.ts | 0 .../templates/sdk}/wasp/auth/login.ts | 0 .../templates/sdk}/wasp/auth/logout.ts | 0 .../auth/pages/createAuthRequiredPage.jsx | 0 .../templates/sdk}/wasp/auth/signup.ts | 0 .../sdk}/wasp/auth/stitches.config.js | 0 .../templates/sdk}/wasp/auth/types.ts | 0 .../templates/sdk}/wasp/auth/useAuth.ts | 0 .../templates/sdk}/wasp/core/AuthError.js | 0 .../templates/sdk}/wasp/core/HttpError.js | 0 .../templates/sdk}/wasp/core/auth.js | 0 .../sdk}/wasp/core/auth/prismaMiddleware.js | 0 .../sdk}/wasp/core/auth/validators.ts | 0 .../templates/sdk}/wasp/core/config.js | 0 .../sdk}/wasp/core/stitches.config.js | 0 .../templates/sdk}/wasp/core/storage.ts | 0 .../templates/sdk}/wasp/entities/index.ts | 0 .../templates/sdk}/wasp/ext-src/actions.ts | 0 .../templates/sdk}/wasp/ext-src/queries.ts | 0 .../templates/sdk}/wasp/operations/index.ts | 0 .../sdk}/wasp/operations/resources.js | 0 .../sdk}/wasp/operations/updateHandlersMap.js | 0 .../templates/sdk}/wasp/rpc/actions/core.d.ts | 0 .../templates/sdk}/wasp/rpc/actions/core.js | 0 .../templates/sdk}/wasp/rpc/actions/index.ts | 0 .../templates/sdk}/wasp/rpc/index.ts | 0 .../templates/sdk}/wasp/rpc/queries/core.d.ts | 0 .../templates/sdk}/wasp/rpc/queries/core.js | 0 .../templates/sdk}/wasp/rpc/queries/index.ts | 0 .../templates/sdk}/wasp/rpc/queryClient.ts | 0 .../sdk}/wasp/server/_types/index.ts | 0 .../sdk}/wasp/server/_types/serialization.ts | 0 .../sdk}/wasp/server/_types/taggedEntities.ts | 0 .../sdk}/wasp/server/actions/index.ts | 0 .../sdk}/wasp/server/actions/types.ts | 0 .../sdk}/wasp/server/core/AuthError.js | 0 .../sdk}/wasp/server/core/HttpError.js | 0 .../templates/sdk}/wasp/server/core/auth.js | 0 .../wasp/server/core/auth/prismaMiddleware.js | 0 .../sdk}/wasp/server/core/auth/validators.ts | 0 .../templates/sdk}/wasp/server/dbClient.ts | 0 .../sdk}/wasp/server/queries/index.ts | 0 .../sdk}/wasp/server/queries/types.ts | 0 .../templates/sdk}/wasp/types/index.ts | 0 .../templates/sdk}/wasp/universal/types.ts | 0 .../templates/sdk}/wasp/universal/url.ts | 0 .../node_modules/wasp/package.json | 33 ----- waspc/examples/todo-typescript/package.json | 11 +- waspc/src/Wasp/Generator.hs | 2 + waspc/src/Wasp/Generator/SdkGenerator.hs | 89 ++++++++++++ waspc/src/Wasp/Generator/WebAppGenerator.hs | 15 +-- waspc/waspc.cabal | 1 + 65 files changed, 275 insertions(+), 48 deletions(-) create mode 100644 waspc/data/Cli/templates/basic/package.json create mode 100644 waspc/data/Generator/templates/sdk/dependencies.txt create mode 100644 waspc/data/Generator/templates/sdk/package.json rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/api/events.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/api/index.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/auth/forms/Auth.tsx (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/auth/forms/Login.tsx (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/auth/forms/Signup.tsx (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/auth/forms/internal/Form.tsx (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/auth/forms/internal/Message.tsx (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/auth/forms/internal/common/LoginSignupForm.tsx (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/auth/forms/types.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/auth/helpers/user.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/auth/login.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/auth/logout.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/auth/pages/createAuthRequiredPage.jsx (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/auth/signup.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/auth/stitches.config.js (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/auth/types.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/auth/useAuth.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/core/AuthError.js (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/core/HttpError.js (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/core/auth.js (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/core/auth/prismaMiddleware.js (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/core/auth/validators.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/core/config.js (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/core/stitches.config.js (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/core/storage.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/entities/index.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/ext-src/actions.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/ext-src/queries.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/operations/index.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/operations/resources.js (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/operations/updateHandlersMap.js (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/rpc/actions/core.d.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/rpc/actions/core.js (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/rpc/actions/index.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/rpc/index.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/rpc/queries/core.d.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/rpc/queries/core.js (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/rpc/queries/index.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/rpc/queryClient.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/server/_types/index.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/server/_types/serialization.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/server/_types/taggedEntities.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/server/actions/index.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/server/actions/types.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/server/core/AuthError.js (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/server/core/HttpError.js (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/server/core/auth.js (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/server/core/auth/prismaMiddleware.js (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/server/core/auth/validators.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/server/dbClient.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/server/queries/index.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/server/queries/types.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/types/index.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/universal/types.ts (100%) rename waspc/{examples/todo-typescript/node_modules => data/Generator/templates/sdk}/wasp/universal/url.ts (100%) delete mode 100644 waspc/examples/todo-typescript/node_modules/wasp/package.json create mode 100644 waspc/src/Wasp/Generator/SdkGenerator.hs diff --git a/waspc/data/Cli/templates/basic/package.json b/waspc/data/Cli/templates/basic/package.json new file mode 100644 index 0000000000..9657a7af1e --- /dev/null +++ b/waspc/data/Cli/templates/basic/package.json @@ -0,0 +1,10 @@ +{ + "name": "prototype", + "dependencies": { + "wasp": "file:.wasp/out/sdk/wasp", + "react": "18.2.0" + }, + "devDependencies": { + "@types/react": "^18.0.37" + } +} diff --git a/waspc/data/Generator/templates/sdk/dependencies.txt b/waspc/data/Generator/templates/sdk/dependencies.txt new file mode 100644 index 0000000000..c15f2b5a77 --- /dev/null +++ b/waspc/data/Generator/templates/sdk/dependencies.txt @@ -0,0 +1,127 @@ +Dependencies: + +("@prisma/client", show prismaVersion), // sdk +("@tanstack/react-query", "^4.29.0"), // sdk +("axios", "^1.4.0"), // sdk +("cookie-parser", "~1.4.6"), // +("cors", "^2.8.5"), // +("dotenv", "16.0.2"), // +("express", "~4.18.1"), // sdk (for types) +("helmet", "^6.0.0"), // +("jsonwebtoken", "^8.5.1"), // sdk +("lodash.merge", "^4.6.2"), // +("mitt", "3.0.0"), // sdk +("morgan", "~1.10.0"), // +("patch-package", "^6.4.7"), // +("rate-limiter-flexible", "^2.4.1"), // +("react", "^18.2.0"), // sdk +("react-dom", "^18.2.0"), // +("react-hook-form", "^7.45.4") // +("react-router-dom", "^5.3.3"), // sdk +("secure-password", "^4.0.0"), // sdk +("superjson", "^1.12.2"), // sdk +("uuid", "^9.0.0"), // + +Dev dependencies: +("@tsconfig/node" ++ show (major NodeVersion.latestMajorNodeVersion), "^1.0.1"), +("@tsconfig/vite-react", "^2.0.0") +("@types/cors", "^2.8.5") +("@types/express", "^4.17.13"), +("@types/express-serve-static-core", "^4.17.13"), +("@types/node", "^18.11.9"), +("@types/react", "^18.0.37"), +("@types/react-dom", "^18.0.11"), +("@types/react-router-dom", "^5.3.3"), +("@types/uuid", "^9.0.0"), +("@vitejs/plugin-react-swc", "^3.0.0"), +("dotenv", "^16.0.3"), // duplicate +("nodemon", "^2.0.19"), // +("prisma", show prismaVersion), // +("standard", "^17.0.0"), // +("typescript", "^5.1.0"), // +("vite", "^4.3.9"), // + +Their package.json: +("react", "^18.2.0"), +("typescript", "^5.1.0") + + +Server + +("cookie-parser", "~1.4.6"), +- [Framework] Generator/templates/server/src/middleware/globalMiddleware.ts + +("cors", "^2.8.5"), +- [Framework] Generator/templates/server/src/middleware/globalMiddleware.ts + +("express", "~4.18.1"), +- Generator/templates/server/src/auth/providers/config/local.ts +- Generator/templates/server/src/auth/providers/config/email.ts +- Generator/templates/server/src/routes/crud/index.ts +- Generator/templates/server/src/routes/crud/_crud.ts +- Generator/templates/server/src/routes/operations/index.js +- Generator/templates/server/src/routes/index.js +- Generator/templates/server/src/auth/providers/index.ts +- Generator/templates/server/src/auth/providers/oauth/createRouter.ts +- Generator/templates/server/src/routes/apis/index.ts +- Generator/templates/server/src/auth/providers/types.ts +- Generator/templates/server/src/types/index.ts +- Generator/templates/server/src/middleware/globalMiddleware.ts +- Generator/templates/server/src/app.js +- Generator/templates/server/src/auth/providers/email/signup.ts +- Generator/templates/server/src/routes/auth/index.js +- Generator/templates/server/src/auth/providers/email/login.ts +- Generator/templates/server/src/auth/providers/email/resetPassword.ts +- Generator/templates/server/src/auth/providers/email/requestPasswordReset.ts +- Generator/templates/server/src/auth/providers/email/verifyEmail.ts +- Generator/templates/server/src/_types/index.ts +- Generator/templates/server/src/apis/types.ts + +("morgan", "~1.10.0"), +- [Framework] Generator/templates/server/src/middleware/globalMiddleware.ts + +("@prisma/client", show prismaVersion), +- [SDK] Generator/templates/react-app/src/entities/index.ts +- [SDK] Generator/templates/server/src/dbClient.ts +- [Framework] Generator/templates/server/src/utils.js +- Generator/templates/server/src/auth/utils.ts +- Generator/templates/server/src/entities/index.ts +- Generator/templates/server/src/auth/providers/oauth/types.ts +- Generator/templates/server/src/crud/_operations.ts +- Generator/templates/server/src/dbSeed/types.ts + + +("jsonwebtoken", "^8.5.1"), +-- NOTE: secure-password has a package.json override for sodium-native. +("secure-password", "^4.0.0"), +("dotenv", "16.0.2"), +("helmet", "^6.0.0"), +("patch-package", "^6.4.7"), +("uuid", "^9.0.0"), +("lodash.merge", "^4.6.2"), +("rate-limiter-flexible", "^2.4.1"), +("superjson", "^1.12.2") + +depsRequiredByPassport spec + +depsRequiredByJobs spec + +depsRequiredByEmail spec + +depsRequiredByWebSockets spec, + N.waspDevDependencies = + AS.Dependency.fromList + [ ("nodemon", "^2.0.19"), + ("standard", "^17.0.0"), + ("prisma", show prismaVersion), + -- TODO: Allow users to choose whether they want to use TypeScript + -- in their projects and install these dependencies accordingly. + ("typescript", "^5.1.0"), + ("@types/express", "^4.17.13"), + ("@types/express-serve-static-core", "^4.17.13"), + ("@types/node", "^18.11.9"), + ("@tsconfig/node" ++ show (major NodeVersion.latestMajorNodeVersion), "^1.0.1"), + ("@types/uuid", "^9.0.0"), + ("@types/cors", "^2.8.5") + ] + } diff --git a/waspc/data/Generator/templates/sdk/package.json b/waspc/data/Generator/templates/sdk/package.json new file mode 100644 index 0000000000..b6abbdfd84 --- /dev/null +++ b/waspc/data/Generator/templates/sdk/package.json @@ -0,0 +1,35 @@ +{{={= =}=}} +{ + "name": "wasp", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "types": "tsc --declaration --emitDeclarationOnly --stripInternal --declarationDir dist" + }, + "exports": { + "./core/HttpError": "./core/HttpError.js", + "./core/config": "./core/config.js", + "./core/stitches.config": "./core/stitches.config.js", + "./core/storage": "./core/storage.ts", + "./rpc": "./rpc/index.ts", + "./rpc/queries": "./rpc/queries/index.ts", + "./rpc/actions": "./rpc/actions/index.ts", + "./rpc/queryClient": "./rpc/queryClient.ts", + "./types": "./types/index.ts", + "./auth/*": "./auth/*", + "./api": "./api/index.ts", + "./api/*": "./api/*", + "./operations": "./operations/index.ts", + "./operations/*": "./operations/*", + "./universal/url": "./universal/url.ts", + "./universal/types": "./universal/url.ts" + }, + "license": "ISC", + "include": [ + "src/**/*" + ], + {=& depsChunk =}, + {=& devDepsChunk =} +} diff --git a/waspc/examples/todo-typescript/node_modules/wasp/api/events.ts b/waspc/data/Generator/templates/sdk/wasp/api/events.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/api/events.ts rename to waspc/data/Generator/templates/sdk/wasp/api/events.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/api/index.ts b/waspc/data/Generator/templates/sdk/wasp/api/index.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/api/index.ts rename to waspc/data/Generator/templates/sdk/wasp/api/index.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/Auth.tsx b/waspc/data/Generator/templates/sdk/wasp/auth/forms/Auth.tsx similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/auth/forms/Auth.tsx rename to waspc/data/Generator/templates/sdk/wasp/auth/forms/Auth.tsx diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/Login.tsx b/waspc/data/Generator/templates/sdk/wasp/auth/forms/Login.tsx similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/auth/forms/Login.tsx rename to waspc/data/Generator/templates/sdk/wasp/auth/forms/Login.tsx diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/Signup.tsx b/waspc/data/Generator/templates/sdk/wasp/auth/forms/Signup.tsx similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/auth/forms/Signup.tsx rename to waspc/data/Generator/templates/sdk/wasp/auth/forms/Signup.tsx diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/Form.tsx b/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/Form.tsx similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/Form.tsx rename to waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/Form.tsx diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/Message.tsx b/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/Message.tsx similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/Message.tsx rename to waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/Message.tsx diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/common/LoginSignupForm.tsx b/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/common/LoginSignupForm.tsx similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/common/LoginSignupForm.tsx rename to waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/common/LoginSignupForm.tsx diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts b/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts rename to waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/forms/types.ts b/waspc/data/Generator/templates/sdk/wasp/auth/forms/types.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/auth/forms/types.ts rename to waspc/data/Generator/templates/sdk/wasp/auth/forms/types.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/helpers/user.ts b/waspc/data/Generator/templates/sdk/wasp/auth/helpers/user.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/auth/helpers/user.ts rename to waspc/data/Generator/templates/sdk/wasp/auth/helpers/user.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/login.ts b/waspc/data/Generator/templates/sdk/wasp/auth/login.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/auth/login.ts rename to waspc/data/Generator/templates/sdk/wasp/auth/login.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/logout.ts b/waspc/data/Generator/templates/sdk/wasp/auth/logout.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/auth/logout.ts rename to waspc/data/Generator/templates/sdk/wasp/auth/logout.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/pages/createAuthRequiredPage.jsx b/waspc/data/Generator/templates/sdk/wasp/auth/pages/createAuthRequiredPage.jsx similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/auth/pages/createAuthRequiredPage.jsx rename to waspc/data/Generator/templates/sdk/wasp/auth/pages/createAuthRequiredPage.jsx diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/signup.ts b/waspc/data/Generator/templates/sdk/wasp/auth/signup.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/auth/signup.ts rename to waspc/data/Generator/templates/sdk/wasp/auth/signup.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/stitches.config.js b/waspc/data/Generator/templates/sdk/wasp/auth/stitches.config.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/auth/stitches.config.js rename to waspc/data/Generator/templates/sdk/wasp/auth/stitches.config.js diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/types.ts b/waspc/data/Generator/templates/sdk/wasp/auth/types.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/auth/types.ts rename to waspc/data/Generator/templates/sdk/wasp/auth/types.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/auth/useAuth.ts b/waspc/data/Generator/templates/sdk/wasp/auth/useAuth.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/auth/useAuth.ts rename to waspc/data/Generator/templates/sdk/wasp/auth/useAuth.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/core/AuthError.js b/waspc/data/Generator/templates/sdk/wasp/core/AuthError.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/core/AuthError.js rename to waspc/data/Generator/templates/sdk/wasp/core/AuthError.js diff --git a/waspc/examples/todo-typescript/node_modules/wasp/core/HttpError.js b/waspc/data/Generator/templates/sdk/wasp/core/HttpError.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/core/HttpError.js rename to waspc/data/Generator/templates/sdk/wasp/core/HttpError.js diff --git a/waspc/examples/todo-typescript/node_modules/wasp/core/auth.js b/waspc/data/Generator/templates/sdk/wasp/core/auth.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/core/auth.js rename to waspc/data/Generator/templates/sdk/wasp/core/auth.js diff --git a/waspc/examples/todo-typescript/node_modules/wasp/core/auth/prismaMiddleware.js b/waspc/data/Generator/templates/sdk/wasp/core/auth/prismaMiddleware.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/core/auth/prismaMiddleware.js rename to waspc/data/Generator/templates/sdk/wasp/core/auth/prismaMiddleware.js diff --git a/waspc/examples/todo-typescript/node_modules/wasp/core/auth/validators.ts b/waspc/data/Generator/templates/sdk/wasp/core/auth/validators.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/core/auth/validators.ts rename to waspc/data/Generator/templates/sdk/wasp/core/auth/validators.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/core/config.js b/waspc/data/Generator/templates/sdk/wasp/core/config.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/core/config.js rename to waspc/data/Generator/templates/sdk/wasp/core/config.js diff --git a/waspc/examples/todo-typescript/node_modules/wasp/core/stitches.config.js b/waspc/data/Generator/templates/sdk/wasp/core/stitches.config.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/core/stitches.config.js rename to waspc/data/Generator/templates/sdk/wasp/core/stitches.config.js diff --git a/waspc/examples/todo-typescript/node_modules/wasp/core/storage.ts b/waspc/data/Generator/templates/sdk/wasp/core/storage.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/core/storage.ts rename to waspc/data/Generator/templates/sdk/wasp/core/storage.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/entities/index.ts b/waspc/data/Generator/templates/sdk/wasp/entities/index.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/entities/index.ts rename to waspc/data/Generator/templates/sdk/wasp/entities/index.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/ext-src/actions.ts b/waspc/data/Generator/templates/sdk/wasp/ext-src/actions.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/ext-src/actions.ts rename to waspc/data/Generator/templates/sdk/wasp/ext-src/actions.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/ext-src/queries.ts b/waspc/data/Generator/templates/sdk/wasp/ext-src/queries.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/ext-src/queries.ts rename to waspc/data/Generator/templates/sdk/wasp/ext-src/queries.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/operations/index.ts b/waspc/data/Generator/templates/sdk/wasp/operations/index.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/operations/index.ts rename to waspc/data/Generator/templates/sdk/wasp/operations/index.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/operations/resources.js b/waspc/data/Generator/templates/sdk/wasp/operations/resources.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/operations/resources.js rename to waspc/data/Generator/templates/sdk/wasp/operations/resources.js diff --git a/waspc/examples/todo-typescript/node_modules/wasp/operations/updateHandlersMap.js b/waspc/data/Generator/templates/sdk/wasp/operations/updateHandlersMap.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/operations/updateHandlersMap.js rename to waspc/data/Generator/templates/sdk/wasp/operations/updateHandlersMap.js diff --git a/waspc/examples/todo-typescript/node_modules/wasp/rpc/actions/core.d.ts b/waspc/data/Generator/templates/sdk/wasp/rpc/actions/core.d.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/rpc/actions/core.d.ts rename to waspc/data/Generator/templates/sdk/wasp/rpc/actions/core.d.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/rpc/actions/core.js b/waspc/data/Generator/templates/sdk/wasp/rpc/actions/core.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/rpc/actions/core.js rename to waspc/data/Generator/templates/sdk/wasp/rpc/actions/core.js diff --git a/waspc/examples/todo-typescript/node_modules/wasp/rpc/actions/index.ts b/waspc/data/Generator/templates/sdk/wasp/rpc/actions/index.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/rpc/actions/index.ts rename to waspc/data/Generator/templates/sdk/wasp/rpc/actions/index.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/rpc/index.ts b/waspc/data/Generator/templates/sdk/wasp/rpc/index.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/rpc/index.ts rename to waspc/data/Generator/templates/sdk/wasp/rpc/index.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/rpc/queries/core.d.ts b/waspc/data/Generator/templates/sdk/wasp/rpc/queries/core.d.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/rpc/queries/core.d.ts rename to waspc/data/Generator/templates/sdk/wasp/rpc/queries/core.d.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/rpc/queries/core.js b/waspc/data/Generator/templates/sdk/wasp/rpc/queries/core.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/rpc/queries/core.js rename to waspc/data/Generator/templates/sdk/wasp/rpc/queries/core.js diff --git a/waspc/examples/todo-typescript/node_modules/wasp/rpc/queries/index.ts b/waspc/data/Generator/templates/sdk/wasp/rpc/queries/index.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/rpc/queries/index.ts rename to waspc/data/Generator/templates/sdk/wasp/rpc/queries/index.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/rpc/queryClient.ts b/waspc/data/Generator/templates/sdk/wasp/rpc/queryClient.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/rpc/queryClient.ts rename to waspc/data/Generator/templates/sdk/wasp/rpc/queryClient.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/server/_types/index.ts b/waspc/data/Generator/templates/sdk/wasp/server/_types/index.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/server/_types/index.ts rename to waspc/data/Generator/templates/sdk/wasp/server/_types/index.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/server/_types/serialization.ts b/waspc/data/Generator/templates/sdk/wasp/server/_types/serialization.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/server/_types/serialization.ts rename to waspc/data/Generator/templates/sdk/wasp/server/_types/serialization.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/server/_types/taggedEntities.ts b/waspc/data/Generator/templates/sdk/wasp/server/_types/taggedEntities.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/server/_types/taggedEntities.ts rename to waspc/data/Generator/templates/sdk/wasp/server/_types/taggedEntities.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/server/actions/index.ts b/waspc/data/Generator/templates/sdk/wasp/server/actions/index.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/server/actions/index.ts rename to waspc/data/Generator/templates/sdk/wasp/server/actions/index.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/server/actions/types.ts b/waspc/data/Generator/templates/sdk/wasp/server/actions/types.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/server/actions/types.ts rename to waspc/data/Generator/templates/sdk/wasp/server/actions/types.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/server/core/AuthError.js b/waspc/data/Generator/templates/sdk/wasp/server/core/AuthError.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/server/core/AuthError.js rename to waspc/data/Generator/templates/sdk/wasp/server/core/AuthError.js diff --git a/waspc/examples/todo-typescript/node_modules/wasp/server/core/HttpError.js b/waspc/data/Generator/templates/sdk/wasp/server/core/HttpError.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/server/core/HttpError.js rename to waspc/data/Generator/templates/sdk/wasp/server/core/HttpError.js diff --git a/waspc/examples/todo-typescript/node_modules/wasp/server/core/auth.js b/waspc/data/Generator/templates/sdk/wasp/server/core/auth.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/server/core/auth.js rename to waspc/data/Generator/templates/sdk/wasp/server/core/auth.js diff --git a/waspc/examples/todo-typescript/node_modules/wasp/server/core/auth/prismaMiddleware.js b/waspc/data/Generator/templates/sdk/wasp/server/core/auth/prismaMiddleware.js similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/server/core/auth/prismaMiddleware.js rename to waspc/data/Generator/templates/sdk/wasp/server/core/auth/prismaMiddleware.js diff --git a/waspc/examples/todo-typescript/node_modules/wasp/server/core/auth/validators.ts b/waspc/data/Generator/templates/sdk/wasp/server/core/auth/validators.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/server/core/auth/validators.ts rename to waspc/data/Generator/templates/sdk/wasp/server/core/auth/validators.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/server/dbClient.ts b/waspc/data/Generator/templates/sdk/wasp/server/dbClient.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/server/dbClient.ts rename to waspc/data/Generator/templates/sdk/wasp/server/dbClient.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/server/queries/index.ts b/waspc/data/Generator/templates/sdk/wasp/server/queries/index.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/server/queries/index.ts rename to waspc/data/Generator/templates/sdk/wasp/server/queries/index.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/server/queries/types.ts b/waspc/data/Generator/templates/sdk/wasp/server/queries/types.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/server/queries/types.ts rename to waspc/data/Generator/templates/sdk/wasp/server/queries/types.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/types/index.ts b/waspc/data/Generator/templates/sdk/wasp/types/index.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/types/index.ts rename to waspc/data/Generator/templates/sdk/wasp/types/index.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/universal/types.ts b/waspc/data/Generator/templates/sdk/wasp/universal/types.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/universal/types.ts rename to waspc/data/Generator/templates/sdk/wasp/universal/types.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/universal/url.ts b/waspc/data/Generator/templates/sdk/wasp/universal/url.ts similarity index 100% rename from waspc/examples/todo-typescript/node_modules/wasp/universal/url.ts rename to waspc/data/Generator/templates/sdk/wasp/universal/url.ts diff --git a/waspc/examples/todo-typescript/node_modules/wasp/package.json b/waspc/examples/todo-typescript/node_modules/wasp/package.json deleted file mode 100644 index 1290aa1f60..0000000000 --- a/waspc/examples/todo-typescript/node_modules/wasp/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "wasp", - "type": "module", - "version": "1.0.0", - "description": "", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "types": "tsc --declaration --emitDeclarationOnly --stripInternal --declarationDir dist" - }, - "exports": { - "./core/HttpError": "./core/HttpError.js", - "./core/config": "./core/config.js", - "./core/stitches.config": "./core/stitches.config.js", - "./core/storage": "./core/storage.ts", - "./rpc": "./rpc/index.ts", - "./rpc/queries": "./rpc/queries/index.ts", - "./rpc/actions": "./rpc/actions/index.ts", - "./rpc/queryClient": "./rpc/queryClient.ts", - "./types": "./types/index.ts", - "./auth/*": "./auth/*", - "./api": "./api/index.ts", - "./api/*": "./api/*", - "./operations": "./operations/index.ts", - "./operations/*": "./operations/*", - "./universal/url": "./universal/url.ts", - "./universal/types": "./universal/url.ts" - }, - "author": "", - "license": "ISC", - "include": [ - "src/**/*" - ] -} diff --git a/waspc/examples/todo-typescript/package.json b/waspc/examples/todo-typescript/package.json index b7219eda1d..3884f98818 100644 --- a/waspc/examples/todo-typescript/package.json +++ b/waspc/examples/todo-typescript/package.json @@ -1,3 +1,12 @@ { - "name": "prototype" + "name": "prototype", + "dependencies": { + "@prisma/client": "^4.16.2", + "react": "18.2.0", + "wasp": "file:.wasp/out/sdk/wasp" + }, + "devDependencies": { + "@types/react": "^18.0.37", + "prisma": "^4.16.2" + } } diff --git a/waspc/src/Wasp/Generator.hs b/waspc/src/Wasp/Generator.hs index ab60e1c1d0..8ba308cc03 100644 --- a/waspc/src/Wasp/Generator.hs +++ b/waspc/src/Wasp/Generator.hs @@ -21,6 +21,7 @@ import Wasp.Generator.DbGenerator (genDb) import Wasp.Generator.DockerGenerator (genDockerFiles) import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.Monad (Generator, GeneratorError, GeneratorWarning, runGenerator) +import Wasp.Generator.SdkGenerator (genSdk) import Wasp.Generator.ServerGenerator (genServer) import Wasp.Generator.Setup (runSetup) import qualified Wasp.Generator.Start @@ -54,6 +55,7 @@ genApp :: AppSpec -> Generator [FileDraft] genApp spec = genWebApp spec <++> genServer spec + <++> genSdk spec <++> genDb spec <++> genDockerFiles spec <++> genConfigFiles spec diff --git a/waspc/src/Wasp/Generator/SdkGenerator.hs b/waspc/src/Wasp/Generator/SdkGenerator.hs new file mode 100644 index 0000000000..0162a4dc7b --- /dev/null +++ b/waspc/src/Wasp/Generator/SdkGenerator.hs @@ -0,0 +1,89 @@ +module Wasp.Generator.SdkGenerator where + +import Data.Aeson (object) +import qualified Data.Aeson as Aeson +import Data.Aeson.Types ((.=)) +import GHC.IO (unsafePerformIO) +import StrongPath +import Wasp.AppSpec +import qualified Wasp.AppSpec.App.Dependency as AS.Dependency +import Wasp.AppSpec.Valid (isAuthEnabled) +import Wasp.Generator.Common (ProjectRootDir, prismaVersion) +import Wasp.Generator.FileDraft (FileDraft, createCopyDirFileDraft, createTemplateFileDraft) +import Wasp.Generator.FileDraft.CopyDirFileDraft (CopyDirFileDraftDstDirStrategy (RemoveExistingDstDir)) +import Wasp.Generator.Monad (Generator) +import qualified Wasp.Generator.NpmDependencies as N +import Wasp.Generator.Templates (TemplatesDir, getTemplatesDirAbsPath) +import qualified Wasp.SemanticVersion as SV + +genSdk :: AppSpec -> Generator [FileDraft] +genSdk spec = sequence [genSdkModules, genPackageJson spec] + +data SdkRootDir + +data SdkTemplatesDir + +genSdkModules :: Generator FileDraft +genSdkModules = + return $ + createCopyDirFileDraft + RemoveExistingDstDir + sdkRootDirInProjectRootDir + (unsafePerformIO getTemplatesDirAbsPath sdkTemplatesDirInTemplatesDir [reldir|wasp|]) + +genPackageJson :: AppSpec -> Generator FileDraft +genPackageJson spec = + return $ + mkTmplFdWithDstAndData + [relfile|package.json|] + [relfile|package.json|] + ( Just $ + object + [ "depsChunk" .= N.getDependenciesPackageJsonEntry npmDepsForSdk, + "devDepsChunk" .= N.getDevDependenciesPackageJsonEntry npmDepsForSdk + ] + ) + where + npmDepsForSdk = + N.NpmDepsForPackage + { N.dependencies = + AS.Dependency.fromList + [ ("@prisma/client", show prismaVersion), + ("prisma", show prismaVersion), + ("@tanstack/react-query", "^4.29.0"), + ("axios", "^1.4.0"), + ("express", "~4.18.1"), + ("jsonwebtoken", "^8.5.1"), + ("mitt", "3.0.0"), + ("react", "^18.2.0"), + ("react-router-dom", "^5.3.3"), + ("secure-password", "^4.0.0"), + ("superjson", "^1.12.2"), + ("@types/express-serve-static-core", "^4.17.13") + ] + ++ depsRequiredForAuth spec, + N.devDependencies = AS.Dependency.fromList [] + } + +depsRequiredForAuth :: AppSpec -> [AS.Dependency.Dependency] +depsRequiredForAuth spec = + [AS.Dependency.make ("@stitches/react", show versionRange) | isAuthEnabled spec] + where + versionRange = SV.Range [SV.backwardsCompatibleWith (SV.Version 1 2 8)] + +mkTmplFdWithDstAndData :: + Path' (Rel SdkTemplatesDir) File' -> + Path' (Rel SdkRootDir) File' -> + Maybe Aeson.Value -> + FileDraft +mkTmplFdWithDstAndData relSrcPath relDstPath tmplData = + createTemplateFileDraft + (sdkRootDirInProjectRootDir relDstPath) + (sdkTemplatesDirInTemplatesDir relSrcPath) + tmplData + +sdkRootDirInProjectRootDir :: Path' (Rel ProjectRootDir) (Dir SdkRootDir) +sdkRootDirInProjectRootDir = [reldir|sdk/wasp|] + +sdkTemplatesDirInTemplatesDir :: Path' (Rel TemplatesDir) (Dir SdkTemplatesDir) +sdkTemplatesDirInTemplatesDir = [reldir|sdk|] diff --git a/waspc/src/Wasp/Generator/WebAppGenerator.hs b/waspc/src/Wasp/Generator/WebAppGenerator.hs index 5cdc9e3546..c86d462837 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator.hs @@ -31,11 +31,10 @@ import qualified Wasp.AppSpec.App.Dependency as AS.Dependency import Wasp.AppSpec.App.WebSocket (WebSocket (..)) import qualified Wasp.AppSpec.Entity as AS.Entity import Wasp.AppSpec.ExternalCode (SourceExternalCodeDir) -import Wasp.AppSpec.Valid (getApp, isAuthEnabled) +import Wasp.AppSpec.Valid (getApp) import Wasp.Env (envVarsToDotEnvContent) import Wasp.Generator.Common ( makeJsonWithEntityData, - prismaVersion, ) import qualified Wasp.Generator.ConfigFile as G.CF import Wasp.Generator.ExternalCodeGenerator (genExternalCodeDir) @@ -62,7 +61,6 @@ import Wasp.JsImport makeJsImport, ) import qualified Wasp.Node.Version as NodeVersion -import qualified Wasp.SemanticVersion as SV import Wasp.Util ((<++>)) genWebApp :: AppSpec -> Generator [FileDraft] @@ -142,16 +140,11 @@ npmDepsForWasp spec = ("react-dom", "^18.2.0"), ("@tanstack/react-query", "^4.29.0"), ("react-router-dom", "^5.3.3"), - -- The web app only needs @prisma/client (we're using the server's - -- CLI to generate what's necessary, check the description in - -- https://github.com/wasp-lang/wasp/pull/962/ for details). - ("@prisma/client", show prismaVersion), ("superjson", "^1.12.2"), ("mitt", "3.0.0"), -- Used for Auth UI ("react-hook-form", "^7.45.4") ] - ++ depsRequiredForAuth spec ++ depsRequiredByTailwind spec ++ depsRequiredForWebSockets spec, N.waspDevDependencies = @@ -172,12 +165,6 @@ npmDepsForWasp spec = ++ depsRequiredForTesting } -depsRequiredForAuth :: AppSpec -> [AS.Dependency.Dependency] -depsRequiredForAuth spec = - [AS.Dependency.make ("@stitches/react", show versionRange) | isAuthEnabled spec] - where - versionRange = SV.Range [SV.backwardsCompatibleWith (SV.Version 1 2 8)] - depsRequiredByTailwind :: AppSpec -> [AS.Dependency.Dependency] depsRequiredByTailwind spec = if G.CF.isTailwindUsed spec diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index d822b24f02..0172b2f2da 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -270,6 +270,7 @@ library Wasp.Generator.AuthProviders.Email Wasp.Generator.Crud Wasp.Generator.Crud.Routes + Wasp.Generator.SdkGenerator Wasp.Generator.ServerGenerator Wasp.Generator.ServerGenerator.JsImport Wasp.Generator.ServerGenerator.ApiRoutesG From a02e0475a8375e039b879e7f411b218739302f9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Mon, 11 Dec 2023 15:50:33 +0100 Subject: [PATCH 19/54] Add project and sdk depdency installation --- waspc/src/Wasp/Generator/Job.hs | 2 +- .../Wasp/Generator/Job/IO/PrefixedWriter.hs | 7 +- waspc/src/Wasp/Generator/NpmInstall.hs | 98 +++++++++++++++---- waspc/src/Wasp/Generator/SdkGenerator.hs | 9 ++ waspc/src/Wasp/Generator/Setup.hs | 31 +----- 5 files changed, 94 insertions(+), 53 deletions(-) diff --git a/waspc/src/Wasp/Generator/Job.hs b/waspc/src/Wasp/Generator/Job.hs index f4859eb118..45730c29a2 100644 --- a/waspc/src/Wasp/Generator/Job.hs +++ b/waspc/src/Wasp/Generator/Job.hs @@ -28,4 +28,4 @@ data JobMessageData data JobOutputType = Stdout | Stderr deriving (Show, Eq) -data JobType = WebApp | Server | Db deriving (Show, Eq, Ord, Bounded, Enum) +data JobType = WebApp | Server | Db | Wasp deriving (Show, Eq, Ord, Bounded, Enum) diff --git a/waspc/src/Wasp/Generator/Job/IO/PrefixedWriter.hs b/waspc/src/Wasp/Generator/Job/IO/PrefixedWriter.hs index e0fbe83fab..2e8015f673 100644 --- a/waspc/src/Wasp/Generator/Job/IO/PrefixedWriter.hs +++ b/waspc/src/Wasp/Generator/Job/IO/PrefixedWriter.hs @@ -172,8 +172,8 @@ makeJobMessagePrefix jobMsg = (T.pack . buildPrefix . concat) [ [("[", jobStyles)], [(jobName, jobStyles)], - styledFlags, - [("]", jobStyles)] + [("]", jobStyles)], + styledFlags ] where buildPrefix :: [StyledText] -> String @@ -187,9 +187,10 @@ makeJobMessagePrefix jobMsg = minPrefixLength = 10 (jobName, jobStyles) = case J._jobType jobMsg of + J.Wasp -> (" Wasp ", [Term.Yellow]) J.Server -> ("Server", [Term.Magenta]) J.WebApp -> ("Client", [Term.Cyan]) - J.Db -> ("Db", [Term.Blue]) + J.Db -> (" Db ", [Term.Blue]) styledFlags :: [StyledText] styledFlags = diff --git a/waspc/src/Wasp/Generator/NpmInstall.hs b/waspc/src/Wasp/Generator/NpmInstall.hs index ec837a3884..353fe845e9 100644 --- a/waspc/src/Wasp/Generator/NpmInstall.hs +++ b/waspc/src/Wasp/Generator/NpmInstall.hs @@ -4,26 +4,31 @@ module Wasp.Generator.NpmInstall ) where -import Control.Concurrent (Chan, newChan, readChan) +import Control.Concurrent (Chan, newChan, readChan, threadDelay, writeChan) import Control.Concurrent.Async (concurrently) import Control.Monad (when) import Control.Monad.IO.Class (liftIO) import qualified Data.Aeson as Aeson import qualified Data.ByteString.Lazy as B +import qualified Data.Text as T import StrongPath (Abs, Dir, File', Path', Rel, relfile, ()) import qualified StrongPath as SP import System.Directory (doesFileExist, removeFile) import System.Exit (ExitCode (..)) +import UnliftIO (race) import Wasp.AppSpec (AppSpec) import Wasp.Generator.Common (ProjectRootDir) +import Wasp.Generator.Job (Job, JobMessage, JobType) import qualified Wasp.Generator.Job as J import Wasp.Generator.Job.IO.PrefixedWriter (PrefixedWriter, printJobMessagePrefixed, runPrefixedWriter) import Wasp.Generator.Monad (GeneratorError (..), GeneratorWarning (..)) import qualified Wasp.Generator.NpmDependencies as N +import qualified Wasp.Generator.SdkGenerator as SdkGenerator import Wasp.Generator.ServerGenerator as SG import qualified Wasp.Generator.ServerGenerator.Setup as ServerSetup import Wasp.Generator.WebAppGenerator as WG import qualified Wasp.Generator.WebAppGenerator.Setup as WebAppSetup +import Wasp.Project.Common (WaspProjectDir) -- | Figure out if npm install is needed. -- @@ -59,14 +64,18 @@ isNpmInstallNeeded spec dstDir = do -- Run npm install for desired AppSpec dependencies, recording what we installed -- Installation may fail, in which the installation record is removed. -installNpmDependenciesWithInstallRecord :: N.NpmDepsForFullStack -> Path' Abs (Dir ProjectRootDir) -> IO ([GeneratorWarning], [GeneratorError]) -installNpmDependenciesWithInstallRecord npmDepsForFullStack dstDir = do +installNpmDependenciesWithInstallRecord :: + N.NpmDepsForFullStack -> + Path' Abs (Dir WaspProjectDir) -> + Path' Abs (Dir ProjectRootDir) -> + IO ([GeneratorWarning], [GeneratorError]) +installNpmDependenciesWithInstallRecord npmDepsForFullStack waspProjectDir dstDir = do -- in case anything fails during installation that would leave node modules in -- a broken state, we remove the file before we start npm install fileExists <- doesFileExist dependenciesInstalledFp when fileExists $ removeFile dependenciesInstalledFp -- now actually do the installation - npmInstallResult <- installNpmDependencies dstDir + npmInstallResult <- installNpmDependencies waspProjectDir dstDir case npmInstallResult of Left npmInstallError -> do return ([], [GenericGeneratorError $ "npm install failed: " ++ npmInstallError]) @@ -100,34 +109,81 @@ loadInstalledFullStackNpmDependencies dstDir = do return (Aeson.decode fileContents :: Maybe N.NpmDepsForFullStack) else return Nothing +reportInstallationProgress :: Chan JobMessage -> JobType -> IO () +reportInstallationProgress chan jobType = reportPeriodically allPossibleMessages + where + reportPeriodically messages = do + threadDelay $ secToMicroSec 5 + writeChan chan $ J.JobMessage {J._data = J.JobOutput (T.append (head messages) "\n") J.Stdout, J._jobType = jobType} + threadDelay $ secToMicroSec 5 + reportPeriodically (if hasLessThan2Elems messages then messages else drop 1 messages) + secToMicroSec = (* 1000000) + hasLessThan2Elems = null . drop 1 + allPossibleMessages = + [ "Still installing npm dependencies!", + "Installation going great - we'll get there soon!", + "The installation is taking a while, but we'll get there!", + "Yup, still not done installing.", + "We're getting closer and closer, everything will be installed soon!", + "Still waiting for the installation to finish? You should! We got too far to give up now!", + "You've been waiting so patiently, just wait a little longer (for the installation to finish)..." + ] + +installNpmDependenciesAndReport :: Job -> Chan JobMessage -> JobType -> IO ExitCode +installNpmDependenciesAndReport installF chan jobType = do + writeChan chan $ J.JobMessage {J._data = J.JobOutput "Starting npm install\n" J.Stdout, J._jobType = jobType} + result <- installF chan `race` reportInstallationProgress chan jobType + case result of + Left exitCode -> return exitCode + Right _ -> error "This should be impossible" + -- Run the individual `npm install` commands for both server and webapp projects -- It runs these concurrently, collects the output produced by these commands -- to pass them along to IO with a prefix -installNpmDependencies :: Path' Abs (Dir ProjectRootDir) -> IO (Either String ()) -installNpmDependencies projectDir = do - chan <- newChan - let runSetupJobs = - ServerSetup.installNpmDependencies projectDir chan - `concurrently` WebAppSetup.installNpmDependencies projectDir chan - (_, result) <- concurrently (handleJobMessages chan) runSetupJobs - case result of - (ExitSuccess, ExitSuccess) -> return $ Right () - exitCodes -> return $ Left $ setupFailedMessage exitCodes +installNpmDependencies :: Path' Abs (Dir WaspProjectDir) -> Path' Abs (Dir ProjectRootDir) -> IO (Either String ()) +installNpmDependencies projectDir dstDir = do + messagesChan <- newChan + (_, exitCode) <- + concurrently + (handleProjectInstallMessage messagesChan) + (installNpmDependenciesAndReport (SdkGenerator.installNpmDependencies projectDir) messagesChan J.Wasp) + case exitCode of + ExitFailure code -> return $ Left $ "Project setup failed with exit code " ++ show code ++ "." + _ -> do + let handleMessagesJob = handleJobMessages messagesChan + let runSetupJobs = + concurrently + (installNpmDependenciesAndReport (ServerSetup.installNpmDependencies dstDir) messagesChan J.Server) + (installNpmDependenciesAndReport (WebAppSetup.installNpmDependencies dstDir) messagesChan J.WebApp) + (_, results) <- concurrently handleMessagesJob runSetupJobs + case results of + (ExitSuccess, ExitSuccess) -> return $ Right () + exitCodes -> return $ Left $ setupFailedMessage exitCodes where - handleJobMessages = runPrefixedWriter . go (False, False) + handleProjectInstallMessage :: Chan J.JobMessage -> IO () + handleProjectInstallMessage = runPrefixedWriter . processMessages + where + processMessages :: Chan J.JobMessage -> PrefixedWriter () + processMessages chan = do + jobMsg <- liftIO $ readChan chan + case J._data jobMsg of + J.JobOutput {} -> printJobMessagePrefixed jobMsg >> processMessages chan + J.JobExit {} -> return () + handleJobMessages = runPrefixedWriter . processMessages (False, False) where - go :: (Bool, Bool) -> Chan J.JobMessage -> PrefixedWriter () - go (True, True) _ = return () - go (isWebAppDone, isServerDone) chan = do + processMessages :: (Bool, Bool) -> Chan J.JobMessage -> PrefixedWriter () + processMessages (True, True) _ = return () + processMessages (isWebAppDone, isServerDone) chan = do jobMsg <- liftIO $ readChan chan case J._data jobMsg of J.JobOutput {} -> printJobMessagePrefixed jobMsg - >> go (isWebAppDone, isServerDone) chan + >> processMessages (isWebAppDone, isServerDone) chan J.JobExit {} -> case J._jobType jobMsg of - J.WebApp -> go (True, isServerDone) chan - J.Server -> go (isWebAppDone, True) chan + J.WebApp -> processMessages (True, isServerDone) chan + J.Server -> processMessages (isWebAppDone, True) chan J.Db -> error "This should never happen. No db job should be active." + J.Wasp -> error "This should never happen. No db job should be active." setupFailedMessage (serverExitCode, webAppExitCode) = let serverErrorMessage = case serverExitCode of diff --git a/waspc/src/Wasp/Generator/SdkGenerator.hs b/waspc/src/Wasp/Generator/SdkGenerator.hs index 0162a4dc7b..e622e21c5d 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator.hs @@ -11,9 +11,12 @@ import Wasp.AppSpec.Valid (isAuthEnabled) import Wasp.Generator.Common (ProjectRootDir, prismaVersion) import Wasp.Generator.FileDraft (FileDraft, createCopyDirFileDraft, createTemplateFileDraft) import Wasp.Generator.FileDraft.CopyDirFileDraft (CopyDirFileDraftDstDirStrategy (RemoveExistingDstDir)) +import qualified Wasp.Generator.Job as J +import Wasp.Generator.Job.Process (runNodeCommandAsJob) import Wasp.Generator.Monad (Generator) import qualified Wasp.Generator.NpmDependencies as N import Wasp.Generator.Templates (TemplatesDir, getTemplatesDirAbsPath) +import Wasp.Project.Common (WaspProjectDir) import qualified Wasp.SemanticVersion as SV genSdk :: AppSpec -> Generator [FileDraft] @@ -87,3 +90,9 @@ sdkRootDirInProjectRootDir = [reldir|sdk/wasp|] sdkTemplatesDirInTemplatesDir :: Path' (Rel TemplatesDir) (Dir SdkTemplatesDir) sdkTemplatesDirInTemplatesDir = [reldir|sdk|] + +-- todo(filip): figure out where this belongs +-- also, fix imports for wasp project +installNpmDependencies :: Path' Abs (Dir WaspProjectDir) -> J.Job +installNpmDependencies projectDir = + runNodeCommandAsJob projectDir "npm" ["install"] J.Wasp diff --git a/waspc/src/Wasp/Generator/Setup.hs b/waspc/src/Wasp/Generator/Setup.hs index 05ba4912e2..347184ad7c 100644 --- a/waspc/src/Wasp/Generator/Setup.hs +++ b/waspc/src/Wasp/Generator/Setup.hs @@ -3,17 +3,14 @@ module Wasp.Generator.Setup ) where -import Control.Concurrent (threadDelay) -import Control.Concurrent.Async (race) import Control.Monad (when) import StrongPath (Abs, Dir, Path') -import Wasp.AppSpec (AppSpec) +import Wasp.AppSpec (AppSpec (waspProjectDir)) import Wasp.Generator.Common (ProjectRootDir) import qualified Wasp.Generator.DbGenerator as DbGenerator import Wasp.Generator.Monad (GeneratorError (..), GeneratorWarning (..)) import Wasp.Generator.NpmInstall (installNpmDependenciesWithInstallRecord, isNpmInstallNeeded) import qualified Wasp.Message as Msg -import qualified Wasp.Util.Terminal as Term runSetup :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> Msg.SendMessage -> IO ([GeneratorWarning], [GeneratorError]) runSetup spec dstDir sendMessage = do @@ -29,32 +26,10 @@ runNpmInstallIfNeeded spec dstDir sendMessage = do Nothing -> return ([], []) Just fullStackDeps -> do sendMessage $ Msg.Start "Starting npm install..." - (Left (npmInstallWarnings, npmInstallErrors)) <- - installNpmDependenciesWithInstallRecord fullStackDeps dstDir - `race` reportInstallationProgress reportInstallationProgressMessages + (npmInstallWarnings, npmInstallErrors) <- + installNpmDependenciesWithInstallRecord fullStackDeps (waspProjectDir spec) dstDir when (null npmInstallErrors) (sendMessage $ Msg.Success "Successfully completed npm install.") return (npmInstallWarnings, npmInstallErrors) - where - reportInstallationProgress :: [String] -> IO () - reportInstallationProgress messages = do - threadDelay $ secToMicroSec 5 - putStrLn $ Term.applyStyles [Term.Yellow] $ "\n\n ..." ++ head messages - threadDelay $ secToMicroSec 5 - reportInstallationProgress $ if hasLessThan2Elems messages then messages else drop 1 messages - - reportInstallationProgressMessages = - [ "Still installing npm dependencies!", - "Installation going great - we'll get there soon!", - "The installation is taking a while, but we'll get there!", - "Yup, still not done installing.", - "We're getting closer and closer, everything will be installed soon!", - "Still waiting for the installation to finish? You should! We got too far to give up now!", - "You've been waiting so patiently, just wait a little longer (for the installation to finish)..." - ] - - secToMicroSec = (* 1000000) - - hasLessThan2Elems = null . drop 1 setUpDatabase :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> Msg.SendMessage -> IO ([GeneratorWarning], [GeneratorError]) setUpDatabase spec dstDir sendMessage = do From fbbda45940a6cdfe1ab224e78f17c6b9469f1e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Mon, 11 Dec 2023 15:59:47 +0100 Subject: [PATCH 20/54] Remove redundant npm install message --- waspc/src/Wasp/Generator/Setup.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/waspc/src/Wasp/Generator/Setup.hs b/waspc/src/Wasp/Generator/Setup.hs index 347184ad7c..f680b9f728 100644 --- a/waspc/src/Wasp/Generator/Setup.hs +++ b/waspc/src/Wasp/Generator/Setup.hs @@ -25,7 +25,6 @@ runNpmInstallIfNeeded spec dstDir sendMessage = do Right maybeFullStackDeps -> case maybeFullStackDeps of Nothing -> return ([], []) Just fullStackDeps -> do - sendMessage $ Msg.Start "Starting npm install..." (npmInstallWarnings, npmInstallErrors) <- installNpmDependenciesWithInstallRecord fullStackDeps (waspProjectDir spec) dstDir when (null npmInstallErrors) (sendMessage $ Msg.Success "Successfully completed npm install.") From 3606f45a7eb99b3e7492d156a96ad06d8992685a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Thu, 14 Dec 2023 14:07:33 +0100 Subject: [PATCH 21/54] Add package.json to AppSpec --- waspc/src/Wasp/AppSpec.hs | 3 +++ waspc/src/Wasp/AppSpec/PackageJson.hs | 17 ++++++++++++ waspc/src/Wasp/Project/Analyze.hs | 37 ++++++++++++++++++++++----- waspc/src/Wasp/Util/IO.hs | 5 ++++ waspc/waspc.cabal | 1 + 5 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 waspc/src/Wasp/AppSpec/PackageJson.hs diff --git a/waspc/src/Wasp/AppSpec.hs b/waspc/src/Wasp/AppSpec.hs index f99203e8be..840b7d211e 100644 --- a/waspc/src/Wasp/AppSpec.hs +++ b/waspc/src/Wasp/AppSpec.hs @@ -42,6 +42,7 @@ import qualified Wasp.AppSpec.ExternalCode as ExternalCode import Wasp.AppSpec.Job (Job) import Wasp.AppSpec.Operation (Operation) import qualified Wasp.AppSpec.Operation as AS.Operation +import Wasp.AppSpec.PackageJson (PackageJson) import Wasp.AppSpec.Page (Page) import Wasp.AppSpec.Query (Query) import Wasp.AppSpec.Route (Route) @@ -56,6 +57,8 @@ import Wasp.Project.Db.Migrations (DbMigrationsDir) data AppSpec = AppSpec { -- | List of declarations like App, Page, Route, ... that describe the web app. decls :: [Decl], + -- | The contents of the package.json file found in the root directory of the wasp project. + packageJson :: PackageJson, -- | Absolute path to the directory containing the wasp project. waspProjectDir :: Path' Abs (Dir WaspProjectDir), -- | List of external server code files (they are referenced/used in the declarations). diff --git a/waspc/src/Wasp/AppSpec/PackageJson.hs b/waspc/src/Wasp/AppSpec/PackageJson.hs new file mode 100644 index 0000000000..a71404d6ad --- /dev/null +++ b/waspc/src/Wasp/AppSpec/PackageJson.hs @@ -0,0 +1,17 @@ +{-# LANGUAGE DeriveGeneric #-} + +module Wasp.AppSpec.PackageJson where + +import Data.Aeson (FromJSON) +import Data.Map (Map) +import GHC.Generics (Generic) + +data PackageJson = PackageJson + { name :: String, + -- todo(filip): do this properly once you merge martin's PR + dependencies :: Map String String, + devDependencies :: Map String String + } + deriving (Show, Generic) + +instance FromJSON PackageJson \ No newline at end of file diff --git a/waspc/src/Wasp/Project/Analyze.hs b/waspc/src/Wasp/Project/Analyze.hs index 4fc532083d..325e12a398 100644 --- a/waspc/src/Wasp/Project/Analyze.hs +++ b/waspc/src/Wasp/Project/Analyze.hs @@ -1,15 +1,21 @@ +{-# LANGUAGE DeriveGeneric #-} + module Wasp.Project.Analyze ( analyzeWaspProject, + readPackageJsonFile, ) where import Control.Arrow (ArrowChoice (left)) import Control.Monad.Except (ExceptT (ExceptT), runExceptT) +import qualified Data.Aeson as Aeson import Data.List (find, isSuffixOf) import StrongPath (Abs, Dir, File', Path', toFilePath, ()) +import StrongPath.TH (relfile) import qualified Wasp.Analyzer as Analyzer import Wasp.Analyzer.AnalyzeError (getErrorMessageAndCtx) import qualified Wasp.AppSpec as AS +import Wasp.AppSpec.PackageJson (PackageJson) import Wasp.AppSpec.Valid (validateAppSpec) import Wasp.CompileOptions (CompileOptions) import qualified Wasp.CompileOptions as CompileOptions @@ -17,7 +23,7 @@ import qualified Wasp.ConfigFile as CF import Wasp.Error (showCompilerErrorForTerminal) import qualified Wasp.ExternalCode as ExternalCode import qualified Wasp.Generator.ConfigFile as G.CF -import Wasp.Project.Common (CompileError, WaspProjectDir) +import Wasp.Project.Common (CompileError, WaspProjectDir, findFileInWaspProjectDir) import Wasp.Project.Db (makeDevDatabaseUrl) import Wasp.Project.Db.Migrations (findMigrationsDir) import Wasp.Project.Deployment (loadUserDockerfileContents) @@ -31,9 +37,12 @@ analyzeWaspProject :: CompileOptions -> IO (Either [CompileError] AS.AppSpec) analyzeWaspProject waspDir options = runExceptT $ do - waspFilePath <- ExceptT $ Control.Arrow.left pure <$> findWaspFile waspDir + waspFilePath <- ExceptT $ maybeToEither [fileNotFoundMessage] <$> findWaspFile waspDir declarations <- ExceptT $ analyzeWaspFileContent waspFilePath - ExceptT $ constructAppSpec waspDir options declarations + packageJsonContent <- ExceptT $ analyzePackageJsonContent waspDir + ExceptT $ constructAppSpec waspDir options packageJsonContent declarations + where + fileNotFoundMessage = "Couldn't find the *.wasp file in the " ++ toFilePath waspDir ++ " directory" analyzeWaspFileContent :: Path' Abs File' -> IO (Either [CompileError] [AS.Decl]) analyzeWaspFileContent waspFilePath = do @@ -47,9 +56,10 @@ analyzeWaspFileContent waspFilePath = do constructAppSpec :: Path' Abs (Dir WaspProjectDir) -> CompileOptions -> + PackageJson -> [AS.Decl] -> IO (Either [CompileError] AS.AppSpec) -constructAppSpec waspDir options decls = do +constructAppSpec waspDir options packageJson decls = do externalServerCodeFiles <- ExternalCode.readFiles (CompileOptions.externalServerCodeDirPath options) @@ -69,6 +79,7 @@ constructAppSpec waspDir options decls = do let appSpec = AS.AppSpec { AS.decls = decls, + AS.packageJson = packageJson, AS.waspProjectDir = waspDir, AS.externalClientFiles = externalClientCodeFiles, AS.externalServerFiles = externalServerCodeFiles, @@ -86,12 +97,26 @@ constructAppSpec waspDir options decls = do [] -> Right appSpec validationErrors -> Left $ map show validationErrors -findWaspFile :: Path' Abs (Dir WaspProjectDir) -> IO (Either String (Path' Abs File')) +findWaspFile :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe (Path' Abs File')) findWaspFile waspDir = do files <- fst <$> IOUtil.listDirectory waspDir - return $ maybeToEither "Couldn't find a single *.wasp file." $ (waspDir ) <$> find isWaspFile files + return $ (waspDir ) <$> find isWaspFile files where isWaspFile path = ".wasp" `isSuffixOf` toFilePath path && (length (toFilePath path) > length (".wasp" :: String)) + +analyzePackageJsonContent :: Path' Abs (Dir WaspProjectDir) -> IO (Either [CompileError] PackageJson) +analyzePackageJsonContent waspProjectDir = + findPackageJsonFile >>= \case + Just packageJsonFile -> readPackageJsonFile packageJsonFile + Nothing -> return $ Left [fileNotFoundMessage] + where + fileNotFoundMessage = "couldn't find package.json file in the " ++ toFilePath waspProjectDir ++ " directory" + findPackageJsonFile = findFileInWaspProjectDir waspProjectDir [relfile|package.json|] + +readPackageJsonFile :: Path' Abs File' -> IO (Either [CompileError] PackageJson) +readPackageJsonFile packageJsonFile = do + byteString <- IOUtil.readFileBytes packageJsonFile + return $ maybeToEither ["Error reading the package.json file"] $ Aeson.decode byteString diff --git a/waspc/src/Wasp/Util/IO.hs b/waspc/src/Wasp/Util/IO.hs index 2c4b38686d..2dfd0f95e7 100644 --- a/waspc/src/Wasp/Util/IO.hs +++ b/waspc/src/Wasp/Util/IO.hs @@ -12,11 +12,13 @@ module Wasp.Util.IO removeFile, isDirectoryEmpty, writeFileFromText, + readFileBytes, ) where import Control.Monad (filterM, when) import Control.Monad.Extra (whenM) +import qualified Data.ByteString.Lazy as B import Data.Text (Text) import qualified Data.Text.IO as T.IO import StrongPath (Abs, Dir, Dir', File, Path', Rel, basename, parseRelDir, parseRelFile, toFilePath, ()) @@ -95,6 +97,9 @@ doesFileExist = SD.doesFileExist . SP.fromAbsFile readFile :: Path' Abs (File f) -> IO String readFile = P.readFile . SP.fromAbsFile +readFileBytes :: Path' Abs (File f) -> IO B.ByteString +readFileBytes = B.readFile . SP.fromAbsFile + readFileStrict :: Path' Abs (File f) -> IO Text readFileStrict = T.IO.readFile . SP.toFilePath diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index 0172b2f2da..62f95d7a1a 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -223,6 +223,7 @@ library Wasp.AppSpec.Job Wasp.AppSpec.JSON Wasp.AppSpec.Operation + Wasp.AppSpec.PackageJson Wasp.AppSpec.Page Wasp.AppSpec.Query Wasp.AppSpec.Route From 6ece919910422c224cdff1fec8398d909f5a54b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Fri, 15 Dec 2023 12:04:56 +0100 Subject: [PATCH 22/54] Handle dependencies through package.json --- waspc/data/Cli/templates/basic/package.json | 6 +++-- .../Generator/templates/sdk/dependencies.txt | 5 ++++ waspc/examples/todo-typescript/package.json | 8 +++---- waspc/src/Wasp/AppSpec/App.hs | 2 -- waspc/src/Wasp/AppSpec/PackageJson.hs | 24 +++++++++++++++---- waspc/src/Wasp/AppSpec/Valid.hs | 16 ++++++------- waspc/src/Wasp/Generator/NpmDependencies.hs | 9 +++---- 7 files changed, 45 insertions(+), 25 deletions(-) diff --git a/waspc/data/Cli/templates/basic/package.json b/waspc/data/Cli/templates/basic/package.json index 9657a7af1e..dce94f95cb 100644 --- a/waspc/data/Cli/templates/basic/package.json +++ b/waspc/data/Cli/templates/basic/package.json @@ -2,9 +2,11 @@ "name": "prototype", "dependencies": { "wasp": "file:.wasp/out/sdk/wasp", - "react": "18.2.0" + "@prisma/client": "4.16.2", + "react": "^18.2.0" }, "devDependencies": { - "@types/react": "^18.0.37" + "@types/react": "^18.0.37", + "prisma": "4.16.2" } } diff --git a/waspc/data/Generator/templates/sdk/dependencies.txt b/waspc/data/Generator/templates/sdk/dependencies.txt index c15f2b5a77..56e1643232 100644 --- a/waspc/data/Generator/templates/sdk/dependencies.txt +++ b/waspc/data/Generator/templates/sdk/dependencies.txt @@ -125,3 +125,8 @@ depsRequiredByWebSockets spec, ("@types/cors", "^2.8.5") ] } + + +LOG: +- react moved from web-app to project package.json +- react-dom moved from web-app to project package.json diff --git a/waspc/examples/todo-typescript/package.json b/waspc/examples/todo-typescript/package.json index 3884f98818..dce94f95cb 100644 --- a/waspc/examples/todo-typescript/package.json +++ b/waspc/examples/todo-typescript/package.json @@ -1,12 +1,12 @@ { "name": "prototype", "dependencies": { - "@prisma/client": "^4.16.2", - "react": "18.2.0", - "wasp": "file:.wasp/out/sdk/wasp" + "wasp": "file:.wasp/out/sdk/wasp", + "@prisma/client": "4.16.2", + "react": "^18.2.0" }, "devDependencies": { "@types/react": "^18.0.37", - "prisma": "^4.16.2" + "prisma": "4.16.2" } } diff --git a/waspc/src/Wasp/AppSpec/App.hs b/waspc/src/Wasp/AppSpec/App.hs index 9e48ce9dfc..ebb145e7a5 100644 --- a/waspc/src/Wasp/AppSpec/App.hs +++ b/waspc/src/Wasp/AppSpec/App.hs @@ -6,7 +6,6 @@ import Data.Data (Data) import Wasp.AppSpec.App.Auth (Auth) import Wasp.AppSpec.App.Client (Client) import Wasp.AppSpec.App.Db (Db) -import Wasp.AppSpec.App.Dependency (Dependency) import Wasp.AppSpec.App.EmailSender (EmailSender) import Wasp.AppSpec.App.Server (Server) import Wasp.AppSpec.App.Wasp (Wasp) @@ -22,7 +21,6 @@ data App = App client :: Maybe Client, db :: Maybe Db, emailSender :: Maybe EmailSender, - dependencies :: Maybe [Dependency], webSocket :: Maybe WebSocket } deriving (Show, Eq, Data) diff --git a/waspc/src/Wasp/AppSpec/PackageJson.hs b/waspc/src/Wasp/AppSpec/PackageJson.hs index a71404d6ad..3e90b4f600 100644 --- a/waspc/src/Wasp/AppSpec/PackageJson.hs +++ b/waspc/src/Wasp/AppSpec/PackageJson.hs @@ -1,17 +1,31 @@ {-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE TemplateHaskell #-} module Wasp.AppSpec.PackageJson where -import Data.Aeson (FromJSON) +import Control.Applicative (liftA2) +import Data.Aeson.TH import Data.Map (Map) +import qualified Data.Map as M import GHC.Generics (Generic) +import Wasp.AppSpec.App.Dependency (Dependency) +import qualified Wasp.AppSpec.App.Dependency as D data PackageJson = PackageJson - { name :: String, + { _name :: !String, -- todo(filip): do this properly once you merge martin's PR - dependencies :: Map String String, - devDependencies :: Map String String + _dependencies :: !(Map String String), + _devDependencies :: !(Map String String) } deriving (Show, Generic) -instance FromJSON PackageJson \ No newline at end of file +$(deriveJSON defaultOptions {fieldLabelModifier = drop 1} ''PackageJson) + +dependencies :: PackageJson -> [Dependency] +dependencies packageJson = D.fromList $ M.toList $ _dependencies packageJson + +devDependencies :: PackageJson -> [Dependency] +devDependencies packageJson = D.fromList $ M.toList $ _devDependencies packageJson + +allDependencies :: PackageJson -> [Dependency] +allDependencies = liftA2 (++) dependencies devDependencies diff --git a/waspc/src/Wasp/AppSpec/Valid.hs b/waspc/src/Wasp/AppSpec/Valid.hs index dcf8a58a42..7637d05c8d 100644 --- a/waspc/src/Wasp/AppSpec/Valid.hs +++ b/waspc/src/Wasp/AppSpec/Valid.hs @@ -174,13 +174,13 @@ validateAuthUserEntityHasCorrectFieldsIfUsernameAndPasswordAuthIsUsed spec = cas usernameAttributeValidationErrors | isFieldUnique "username" userEntity == Just True = [] | otherwise = - [ GenericValidationError $ - "The field 'username' on entity '" - ++ userEntityName - ++ "' (referenced by " - ++ authUserEntityPath - ++ ") must be marked with the '@unique' attribute." - ] + [ GenericValidationError $ + "The field 'username' on entity '" + ++ userEntityName + ++ "' (referenced by " + ++ authUserEntityPath + ++ ") must be marked with the '@unique' attribute." + ] userEntityFields = Entity.getFields userEntity authUserEntityPath = "app.auth.userEntity" (userEntityName, userEntity) = AS.resolveRef spec (Auth.userEntity auth) @@ -370,7 +370,7 @@ validateWebAppBaseDir :: AppSpec -> [ValidationError] validateWebAppBaseDir spec = case maybeBaseDir of Just baseDir | not (startsWithSlash baseDir) -> - [GenericValidationError "The app.client.baseDir should start with a slash e.g. \"/test\""] + [GenericValidationError "The app.client.baseDir should start with a slash e.g. \"/test\""] _anyOtherCase -> [] where maybeBaseDir = Client.baseDir =<< AS.App.client (snd $ getApp spec) diff --git a/waspc/src/Wasp/Generator/NpmDependencies.hs b/waspc/src/Wasp/Generator/NpmDependencies.hs index cb6015d45a..6b2bbb89be 100644 --- a/waspc/src/Wasp/Generator/NpmDependencies.hs +++ b/waspc/src/Wasp/Generator/NpmDependencies.hs @@ -23,9 +23,9 @@ import Data.Maybe (fromMaybe) import qualified Data.Maybe as Maybe import GHC.Generics import Wasp.AppSpec (AppSpec) -import qualified Wasp.AppSpec.App as AS.App +import qualified Wasp.AppSpec as AS import qualified Wasp.AppSpec.App.Dependency as D -import qualified Wasp.AppSpec.Valid as ASV +import qualified Wasp.AppSpec.PackageJson as AS.PackageJson import Wasp.Generator.Monad (Generator, GeneratorError (..), logAndThrowGeneratorError) data NpmDepsForFullStack = NpmDepsForFullStack @@ -108,9 +108,10 @@ buildNpmDepsForFullStack spec forServer forWebApp = getUserNpmDepsForPackage :: AppSpec -> NpmDepsForUser getUserNpmDepsForPackage spec = NpmDepsForUser - { userDependencies = fromMaybe [] $ AS.App.dependencies $ snd $ ASV.getApp spec, + { -- todo(filip): what if package.json has no dependencies field? + userDependencies = AS.PackageJson.dependencies $ AS.packageJson spec, -- Should we allow user devDependencies? https://github.com/wasp-lang/wasp/issues/456 - userDevDependencies = [] + userDevDependencies = AS.PackageJson.devDependencies $ AS.packageJson spec } conflictErrorToMessage :: DependencyConflictError -> String From 6ad7c54d6fca97ea1086f7c7c834ce00a759c0ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Fri, 15 Dec 2023 12:50:17 +0100 Subject: [PATCH 23/54] Implement wasp reset --- waspc/cli/exe/Main.hs | 4 +++ .../src/Wasp/Cli/Command/BashCompletion.hs | 3 +- waspc/cli/src/Wasp/Cli/Command/Call.hs | 1 + waspc/cli/src/Wasp/Cli/Command/Clean.hs | 19 ++---------- waspc/cli/src/Wasp/Cli/Command/Common.hs | 30 +++++++++++++++++-- waspc/cli/src/Wasp/Cli/Command/Reset.hs | 24 +++++++++++++++ waspc/cli/src/Wasp/Cli/Common.hs | 6 ++++ waspc/src/Wasp/Util/IO.hs | 16 ++++++---- waspc/waspc.cabal | 1 + 9 files changed, 79 insertions(+), 25 deletions(-) create mode 100644 waspc/cli/src/Wasp/Cli/Command/Reset.hs diff --git a/waspc/cli/exe/Main.hs b/waspc/cli/exe/Main.hs index 7b3e1f0d85..c5b6483aff 100644 --- a/waspc/cli/exe/Main.hs +++ b/waspc/cli/exe/Main.hs @@ -25,6 +25,7 @@ import Wasp.Cli.Command.Deploy (deploy) import Wasp.Cli.Command.Deps (deps) import Wasp.Cli.Command.Dockerfile (printDockerfile) import Wasp.Cli.Command.Info (info) +import Wasp.Cli.Command.Reset (reset) import Wasp.Cli.Command.Start (start) import qualified Wasp.Cli.Command.Start.Db as Command.Start.Db import Wasp.Cli.Command.Studio (studio) @@ -47,6 +48,7 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do ("new" : newArgs) -> Command.Call.New newArgs ["start"] -> Command.Call.Start ["start", "db"] -> Command.Call.StartDb + ["reset"] -> Command.Call.Reset ["clean"] -> Command.Call.Clean ["compile"] -> Command.Call.Compile ("db" : dbArgs) -> Command.Call.Db dbArgs @@ -83,6 +85,7 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do Command.Call.Start -> runCommand start Command.Call.StartDb -> runCommand Command.Start.Db.start Command.Call.Clean -> runCommand clean + Command.Call.Reset -> runCommand reset Command.Call.Compile -> runCommand compile Command.Call.Db dbArgs -> dbCli dbArgs Command.Call.Version -> printVersion @@ -138,6 +141,7 @@ printUsage = cmd " start db Starts managed development database for you.", cmd " db [args] Executes a database command. Run 'wasp db' for more info.", cmd " clean Deletes all generated code and other cached artifacts.", + cmd " reset Deletes all generated code, all cached artifacts, and the node_modules directory.", " Wasp equivalent of 'have you tried closing and opening it again?'.", cmd " build Generates full web app code, ready for deployment. Use when deploying or ejecting.", cmd " deploy Deploys your Wasp app to cloud hosting providers.", diff --git a/waspc/cli/src/Wasp/Cli/Command/BashCompletion.hs b/waspc/cli/src/Wasp/Cli/Command/BashCompletion.hs index 1e90148e57..3f297c99ac 100644 --- a/waspc/cli/src/Wasp/Cli/Command/BashCompletion.hs +++ b/waspc/cli/src/Wasp/Cli/Command/BashCompletion.hs @@ -26,7 +26,8 @@ bashCompletion = do ["db", cmdPrefix] -> listMatchingCommands cmdPrefix dbSubCommands _ -> liftIO . putStrLn $ "" where - commands = ["new", "version", "waspls", "start", "db", "clean", "uninstall", "build", "telemetry", "deps", "info", "completion", "completion:generate"] + -- todo(filip): remove duplication + commands = ["new", "version", "waspls", "start", "db", "clean", "reset", "uninstall", "build", "telemetry", "deps", "info", "completion", "completion:generate"] dbSubCommands = ["migrate-dev", "studio"] listMatchingCommands :: String -> [String] -> Command () listMatchingCommands cmdPrefix cmdList = listCommands $ filter (cmdPrefix `isPrefixOf`) cmdList diff --git a/waspc/cli/src/Wasp/Cli/Command/Call.hs b/waspc/cli/src/Wasp/Cli/Command/Call.hs index eeeabc7eb5..fd2b67578a 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Call.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Call.hs @@ -5,6 +5,7 @@ data Call | Start | StartDb | Clean + | Reset | Uninstall | Compile | Db Arguments -- db args diff --git a/waspc/cli/src/Wasp/Cli/Command/Clean.hs b/waspc/cli/src/Wasp/Cli/Command/Clean.hs index 87a3b1346e..74fd92a369 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Clean.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Clean.hs @@ -3,26 +3,11 @@ module Wasp.Cli.Command.Clean ) where -import Control.Monad.IO.Class (liftIO) -import qualified StrongPath as SP -import System.Directory - ( doesDirectoryExist, - removeDirectoryRecursive, - ) import Wasp.Cli.Command (Command) -import Wasp.Cli.Command.Message (cliSendMessageC) +import Wasp.Cli.Command.Common (deleteDotWaspDirIfExists) import Wasp.Cli.Command.Require (InWaspProject (InWaspProject), require) -import qualified Wasp.Cli.Common as Common -import qualified Wasp.Message as Msg clean :: Command () clean = do InWaspProject waspProjectDir <- require - let dotWaspDirFp = SP.toFilePath $ waspProjectDir SP. Common.dotWaspDirInWaspProjectDir - cliSendMessageC $ Msg.Start "Deleting .wasp/ directory..." - doesDotWaspDirExist <- liftIO $ doesDirectoryExist dotWaspDirFp - if doesDotWaspDirExist - then do - liftIO $ removeDirectoryRecursive dotWaspDirFp - cliSendMessageC $ Msg.Success "Deleted .wasp/ directory." - else cliSendMessageC $ Msg.Success "Nothing to delete: .wasp directory does not exist." + deleteDotWaspDirIfExists waspProjectDir diff --git a/waspc/cli/src/Wasp/Cli/Command/Common.hs b/waspc/cli/src/Wasp/Cli/Command/Common.hs index 8f40ab7249..a1abd809a4 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Common.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Common.hs @@ -1,17 +1,24 @@ module Wasp.Cli.Command.Common ( readWaspCompileInfo, throwIfExeIsNotAvailable, + deleteDotWaspDirIfExists, + deleteDirectoryIfExists, ) where import Control.Monad.Except import qualified Control.Monad.Except as E import StrongPath (Abs, Dir, Path') +import qualified StrongPath as SP import StrongPath.Operations -import System.Directory (findExecutable) +import System.Directory + ( findExecutable, + ) import Wasp.Cli.Command (Command, CommandError (..)) +import Wasp.Cli.Command.Message (cliSendMessageC) +import Wasp.Cli.Common (WaspProjectDir) import qualified Wasp.Cli.Common as Cli.Common -import Wasp.Project (WaspProjectDir) +import qualified Wasp.Message as Msg import Wasp.Util (ifM) import qualified Wasp.Util.IO as IOUtil @@ -34,3 +41,22 @@ throwIfExeIsNotAvailable exeName explanationMsg = do Nothing -> E.throwError $ CommandError ("Couldn't find `" <> exeName <> "` executable") explanationMsg + +deleteDirectoryIfExists :: Path' Abs (Dir d) -> Command () +deleteDirectoryIfExists dir = do + cliSendMessageC $ Msg.Start $ "Deleting the " ++ dirName ++ " directory..." + dirExist <- liftIO $ IOUtil.doesDirectoryExist dir + if dirExist + then deleteDir + else -- todo(filip): do we need to report this? If not, we can simply call the function from IOUtil. + cliSendMessageC $ Msg.Success $ "Nothing to delete: The " ++ dirName ++ " directory does not exist." + where + dirName = SP.toFilePath $ basename dir + deleteDir = do + liftIO $ IOUtil.removeDirectory dir + cliSendMessageC $ Msg.Success $ "Deleted the " ++ dirName ++ " directory." + +deleteDotWaspDirIfExists :: Path' Abs (Dir WaspProjectDir) -> Command () +deleteDotWaspDirIfExists waspProjectDir = deleteDirectoryIfExists dotWaspDir + where + dotWaspDir = waspProjectDir SP. Cli.Common.dotWaspDirInWaspProjectDir \ No newline at end of file diff --git a/waspc/cli/src/Wasp/Cli/Command/Reset.hs b/waspc/cli/src/Wasp/Cli/Command/Reset.hs new file mode 100644 index 0000000000..d49f1a6d45 --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/Reset.hs @@ -0,0 +1,24 @@ +module Wasp.Cli.Command.Reset + ( reset, + ) +where + +import StrongPath (Dir, Path') +import qualified StrongPath as SP +import StrongPath.Types (Abs) +import Wasp.Cli.Command (Command) +import Wasp.Cli.Command.Common (deleteDirectoryIfExists, deleteDotWaspDirIfExists) +import Wasp.Cli.Command.Require (InWaspProject (InWaspProject), require) +import Wasp.Cli.Common (WaspProjectDir) +import qualified Wasp.Cli.Common as Common + +reset :: Command () +reset = do + InWaspProject waspProjectDir <- require + deleteDotWaspDirIfExists waspProjectDir + deleteNodeModulesDirIfExists waspProjectDir + +deleteNodeModulesDirIfExists :: Path' Abs (Dir WaspProjectDir) -> Command () +deleteNodeModulesDirIfExists waspProjectDir = deleteDirectoryIfExists nodeModulesDir + where + nodeModulesDir = waspProjectDir SP. Common.nodeModulesDirInWaspProjectDir \ No newline at end of file diff --git a/waspc/cli/src/Wasp/Cli/Common.hs b/waspc/cli/src/Wasp/Cli/Common.hs index fe4a66858c..54b3bd4942 100644 --- a/waspc/cli/src/Wasp/Cli/Common.hs +++ b/waspc/cli/src/Wasp/Cli/Common.hs @@ -5,6 +5,7 @@ module Wasp.Cli.Common dotWaspDirInWaspProjectDir, dotWaspRootFileInWaspProjectDir, dotWaspInfoFileInGeneratedCodeDir, + nodeModulesDirInWaspProjectDir, extServerCodeDirInWaspProjectDir, extClientCodeDirInWaspProjectDir, extSharedCodeDirInWaspProjectDir, @@ -24,12 +25,17 @@ import qualified Wasp.Util.Terminal as Term data DotWaspDir -- Here we put everything that wasp generates. +data NodeModulesDir + data CliTemplatesDir -- TODO: SHould this be renamed to include word "root"? dotWaspDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir DotWaspDir) dotWaspDirInWaspProjectDir = [reldir|.wasp|] +nodeModulesDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir NodeModulesDir) +nodeModulesDirInWaspProjectDir = [reldir|node_modules|] + -- TODO: Hm this has different name than it has in Generator. generatedCodeDirInDotWaspDir :: Path' (Rel DotWaspDir) (Dir Wasp.Generator.Common.ProjectRootDir) generatedCodeDirInDotWaspDir = [reldir|out|] diff --git a/waspc/src/Wasp/Util/IO.hs b/waspc/src/Wasp/Util/IO.hs index 2dfd0f95e7..323cc9563b 100644 --- a/waspc/src/Wasp/Util/IO.hs +++ b/waspc/src/Wasp/Util/IO.hs @@ -6,17 +6,19 @@ module Wasp.Util.IO deleteDirectoryIfExists, deleteFileIfExists, doesFileExist, + doesDirectoryExist, readFile, readFileStrict, writeFile, removeFile, + removeDirectory, isDirectoryEmpty, writeFileFromText, readFileBytes, ) where -import Control.Monad (filterM, when) +import Control.Monad (filterM) import Control.Monad.Extra (whenM) import qualified Data.ByteString.Lazy as B import Data.Text (Text) @@ -83,10 +85,8 @@ listDirectory absDirPath = do -- with relative paths, define a new function (e.g., `readFileRel`). deleteDirectoryIfExists :: Path' Abs (Dir d) -> IO () -deleteDirectoryIfExists dirPath = do - let dirPathStr = SP.fromAbsDir dirPath - exists <- SD.doesDirectoryExist dirPathStr - when exists $ SD.removeDirectoryRecursive dirPathStr +deleteDirectoryIfExists dirPath = + whenM (doesDirectoryExist dirPath) (removeDirectory dirPath) deleteFileIfExists :: Path' Abs (File f) -> IO () deleteFileIfExists filePath = whenM (doesFileExist filePath) $ removeFile filePath @@ -94,6 +94,9 @@ deleteFileIfExists filePath = whenM (doesFileExist filePath) $ removeFile filePa doesFileExist :: Path' Abs (File f) -> IO Bool doesFileExist = SD.doesFileExist . SP.fromAbsFile +doesDirectoryExist :: Path' Abs (Dir f) -> IO Bool +doesDirectoryExist = SD.doesDirectoryExist . SP.fromAbsDir + readFile :: Path' Abs (File f) -> IO String readFile = P.readFile . SP.fromAbsFile @@ -112,6 +115,9 @@ writeFileFromText = T.IO.writeFile . SP.fromAbsFile removeFile :: Path' Abs (File f) -> IO () removeFile = SD.removeFile . SP.fromAbsFile +removeDirectory :: Path' Abs (Dir d) -> IO () +removeDirectory = SD.removeDirectoryRecursive . SP.fromAbsDir + isDirectoryEmpty :: Path' Abs (Dir d) -> IO Bool isDirectoryEmpty dirPath = do (files, dirs) <- listDirectory dirPath diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index 62f95d7a1a..96d9fafe47 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -478,6 +478,7 @@ library cli-lib Wasp.Cli.Command.Info Wasp.Cli.Command.Studio Wasp.Cli.Command.Require + Wasp.Cli.Command.Reset Wasp.Cli.Command.Start Wasp.Cli.Command.Start.Db Wasp.Cli.Command.Telemetry From fba7e16becbea158816ee157b21f7feb3a99ae58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Fri, 15 Dec 2023 12:50:38 +0100 Subject: [PATCH 24/54] Remove redundant import --- waspc/src/Wasp/Generator/NpmDependencies.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/waspc/src/Wasp/Generator/NpmDependencies.hs b/waspc/src/Wasp/Generator/NpmDependencies.hs index 6b2bbb89be..51131b9af9 100644 --- a/waspc/src/Wasp/Generator/NpmDependencies.hs +++ b/waspc/src/Wasp/Generator/NpmDependencies.hs @@ -19,7 +19,6 @@ where import Data.Aeson import Data.List (intercalate, sort) import qualified Data.Map as Map -import Data.Maybe (fromMaybe) import qualified Data.Maybe as Maybe import GHC.Generics import Wasp.AppSpec (AppSpec) From d8f9ee2c243a8ad1050e4ffd32979ff79b70baab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Mon, 18 Dec 2023 15:02:09 +0100 Subject: [PATCH 25/54] Fix double installation of dependencies --- waspc/src/Wasp/Generator/NpmDependencies.hs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/waspc/src/Wasp/Generator/NpmDependencies.hs b/waspc/src/Wasp/Generator/NpmDependencies.hs index 51131b9af9..2f66df485a 100644 --- a/waspc/src/Wasp/Generator/NpmDependencies.hs +++ b/waspc/src/Wasp/Generator/NpmDependencies.hs @@ -141,9 +141,11 @@ combineNpmDepsForPackage npmDepsForWasp npmDepsForUser = if null conflictErrors && null devConflictErrors then Right $ + -- todo(filip): check whether dependency updates and npm install work properly + -- todo(filip): reconsider whether we want to change the {sever,web-app}/package.json dynamically NpmDepsForPackage - { dependencies = waspDependencies npmDepsForWasp ++ remainingUserDeps, - devDependencies = waspDevDependencies npmDepsForWasp ++ remainingUserDevDeps + { dependencies = Map.elems remainingWapsDeps, + devDependencies = Map.elems remainingWaspDevDeps } else Left $ @@ -159,8 +161,8 @@ combineNpmDepsForPackage npmDepsForWasp npmDepsForUser = allWaspDepsByName = waspDepsByName `Map.union` waspDevDepsByName conflictErrors = determineConflictErrors allWaspDepsByName userDepsByName devConflictErrors = determineConflictErrors allWaspDepsByName userDevDepsByName - remainingUserDeps = getRemainingUserDeps allWaspDepsByName userDepsByName - remainingUserDevDeps = getRemainingUserDeps allWaspDepsByName userDevDepsByName + remainingWapsDeps = allWaspDepsByName `Map.difference` userDepsByName + remainingWaspDevDeps = allWaspDepsByName `Map.difference` userDevDepsByName type DepsByName = Map.Map String D.Dependency @@ -179,12 +181,6 @@ determineConflictErrors waspDepsByName userDepsByName = then Just $ DependencyConflictError waspDep userDep else Nothing --- Given a map of wasp dependencies and a map of user dependencies, construct a --- a list of user dependencies that remain once any overlapping wasp dependencies --- have been removed. This assumes conflict detection was already passed. -getRemainingUserDeps :: DepsByName -> DepsByName -> [D.Dependency] -getRemainingUserDeps waspDepsByName userDepsByName = Map.elems $ userDepsByName `Map.difference` waspDepsByName - -- Construct a map of dependency keyed by dependency name. makeDepsByName :: [D.Dependency] -> DepsByName makeDepsByName = Map.fromList . fmap (\d -> (D.name d, d)) From 52c547b3f2ecf9918fba1268f5e413b8dd68e91f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Mon, 18 Dec 2023 17:57:14 +0100 Subject: [PATCH 26/54] Move prisma client to top-level node_modules --- waspc/data/Generator/templates/Dockerfile | 2 +- .../data/Generator/templates/db/schema.prisma | 1 - waspc/src/Wasp/Generator/DbGenerator.hs | 16 +-- .../src/Wasp/Generator/DbGenerator/Common.hs | 24 +--- waspc/src/Wasp/Generator/DbGenerator/Jobs.hs | 125 +++++++++--------- .../Wasp/Generator/DbGenerator/Operations.hs | 39 ++---- waspc/src/Wasp/Generator/DockerGenerator.hs | 3 - waspc/src/Wasp/Generator/ServerGenerator.hs | 3 - 8 files changed, 88 insertions(+), 125 deletions(-) diff --git a/waspc/data/Generator/templates/Dockerfile b/waspc/data/Generator/templates/Dockerfile index 8194f2916f..23833aabee 100644 --- a/waspc/data/Generator/templates/Dockerfile +++ b/waspc/data/Generator/templates/Dockerfile @@ -26,7 +26,7 @@ COPY server/ ./server/ RUN cd server && npm install {=# usingPrisma =} COPY db/schema.prisma ./db/ -RUN cd server && {= serverPrismaClientOutputDirEnv =} npx prisma generate --schema='{= dbSchemaFileFromServerDir =}' +RUN cd server && npx prisma generate --schema='{= dbSchemaFileFromServerDir =}' {=/ usingPrisma =} # Building the server should come after Prisma generation. RUN cd server && npm run build diff --git a/waspc/data/Generator/templates/db/schema.prisma b/waspc/data/Generator/templates/db/schema.prisma index ababaed1fe..d56db09e21 100644 --- a/waspc/data/Generator/templates/db/schema.prisma +++ b/waspc/data/Generator/templates/db/schema.prisma @@ -10,7 +10,6 @@ datasource db { generator client { provider = "prisma-client-js" - output = {=& prismaClientOutputDir =} {=# prismaPreviewFeatures =} previewFeatures = {=& . =} {=/ prismaPreviewFeatures =} diff --git a/waspc/src/Wasp/Generator/DbGenerator.hs b/waspc/src/Wasp/Generator/DbGenerator.hs index 6c6ef0cad1..0118b256c3 100644 --- a/waspc/src/Wasp/Generator/DbGenerator.hs +++ b/waspc/src/Wasp/Generator/DbGenerator.hs @@ -29,7 +29,6 @@ import Wasp.Generator.DbGenerator.Common dbSchemaFileInDbTemplatesDir, dbSchemaFileInProjectRootDir, dbTemplatesDirInTemplatesDir, - prismaClientOutputDirEnvVar, ) import qualified Wasp.Generator.DbGenerator.Operations as DbOps import Wasp.Generator.FileDraft (FileDraft, createCopyDirFileDraft, createTemplateFileDraft) @@ -68,7 +67,6 @@ genPrismaSchema spec = do [ "modelSchemas" .= map entityToPslModelSchema (AS.getDecls @AS.Entity.Entity spec), "datasourceProvider" .= datasourceProvider, "datasourceUrl" .= datasourceUrl, - "prismaClientOutputDir" .= makeEnvVarField Wasp.Generator.DbGenerator.Common.prismaClientOutputDirEnvVar, "prismaPreviewFeatures" .= prismaPreviewFeatures, "dbExtensions" .= dbExtensions ] @@ -95,7 +93,7 @@ genMigrationsDir spec = return $ createCopyDirFileDraft RemoveExistingDstDir gen postWriteDbGeneratorActions :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO ([GeneratorWarning], [GeneratorError]) postWriteDbGeneratorActions spec dstDir = do dbGeneratorWarnings <- maybeToList <$> warnIfDbNeedsMigration spec dstDir - dbGeneratorErrors <- maybeToList <$> genPrismaClients spec dstDir + dbGeneratorErrors <- maybeToList <$> generatePrismaClient spec dstDir return (dbGeneratorWarnings, dbGeneratorErrors) -- | Checks if user needs to run `wasp db migrate-dev` due to changes in schema.prisma, and if so, returns a warning. @@ -173,12 +171,12 @@ warnProjectDiffersFromDb projectRootDir = do "Wasp was unable to verify your database is up to date." <> " Running `wasp db migrate-dev` may fix this and will provide more info." -genPrismaClients :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO (Maybe GeneratorError) -genPrismaClients spec projectRootDir = +generatePrismaClient :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO (Maybe GeneratorError) +generatePrismaClient spec projectRootDir = ifM wasCurrentSchemaAlreadyGenerated (return Nothing) - generatePrismaClientsIfEntitiesExist + generatePrismaClientIfEntitiesExist where wasCurrentSchemaAlreadyGenerated :: IO Bool wasCurrentSchemaAlreadyGenerated = @@ -186,10 +184,10 @@ genPrismaClients spec projectRootDir = projectRootDir Wasp.Generator.DbGenerator.Common.dbSchemaChecksumOnLastGenerateFileProjectRootDir - generatePrismaClientsIfEntitiesExist :: IO (Maybe GeneratorError) - generatePrismaClientsIfEntitiesExist + generatePrismaClientIfEntitiesExist :: IO (Maybe GeneratorError) + generatePrismaClientIfEntitiesExist | entitiesExist = - either (Just . GenericGeneratorError) (const Nothing) <$> DbOps.generatePrismaClients projectRootDir + either (Just . GenericGeneratorError) (const Nothing) <$> DbOps.generatePrismaClient projectRootDir | otherwise = return Nothing entitiesExist = not . null $ getEntities spec diff --git a/waspc/src/Wasp/Generator/DbGenerator/Common.hs b/waspc/src/Wasp/Generator/DbGenerator/Common.hs index 7fb95ecf7a..6db5e574a1 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Common.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Common.hs @@ -1,8 +1,5 @@ module Wasp.Generator.DbGenerator.Common ( dbMigrationsDirInDbRootDir, - serverPrismaClientOutputDirEnv, - webAppPrismaClientOutputDirEnv, - prismaClientOutputDirInAppComponentDir, dbSchemaFileFromAppComponentDir, dbRootDirInProjectRootDir, dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir, @@ -19,15 +16,15 @@ module Wasp.Generator.DbGenerator.Common serverRootDirFromDbRootDir, webAppRootDirFromDbRootDir, dbSchemaFileInProjectRootDir, - prismaClientOutputDirEnvVar, + waspProjectDirFromProjectRootDir, DbSchemaChecksumFile, ) where import StrongPath (Dir, File, File', Path', Rel, reldir, relfile, ()) -import qualified StrongPath as SP import Wasp.Generator.Common (AppComponentRootDir, DbRootDir, ProjectRootDir, ServerRootDir) import Wasp.Generator.Templates (TemplatesDir) +import Wasp.Project.Common (WaspProjectDir) import Wasp.Project.Db.Migrations (DbMigrationsDir) data DbTemplatesDir @@ -93,21 +90,8 @@ dbSchemaChecksumOnLastGenerateFileInDbRootDir = [relfile|schema.prisma.wasp-gene dbSchemaChecksumOnLastGenerateFileProjectRootDir :: Path' (Rel ProjectRootDir) (File DbSchemaChecksumOnLastGenerateFile) dbSchemaChecksumOnLastGenerateFileProjectRootDir = dbRootDirInProjectRootDir dbSchemaChecksumOnLastGenerateFileInDbRootDir -prismaClientOutputDirEnvVar :: String -prismaClientOutputDirEnvVar = "PRISMA_CLIENT_OUTPUT_DIR" - -prismaClientOutputDirInAppComponentDir :: AppComponentRootDir d => Path' (Rel d) (Dir ServerRootDir) -prismaClientOutputDirInAppComponentDir = [reldir|node_modules/.prisma/client|] - -serverPrismaClientOutputDirEnv :: (String, String) -serverPrismaClientOutputDirEnv = appComponentPrismaClientOutputDirEnv serverRootDirFromDbRootDir - -webAppPrismaClientOutputDirEnv :: (String, String) -webAppPrismaClientOutputDirEnv = appComponentPrismaClientOutputDirEnv webAppRootDirFromDbRootDir - -appComponentPrismaClientOutputDirEnv :: AppComponentRootDir d => Path' (Rel DbRootDir) (Dir d) -> (String, String) -appComponentPrismaClientOutputDirEnv appComponentDirFromDbRootDir = - (prismaClientOutputDirEnvVar, SP.fromRelDir $ appComponentDirFromDbRootDir prismaClientOutputDirInAppComponentDir) +waspProjectDirFromProjectRootDir :: Path' (Rel ProjectRootDir) (Dir WaspProjectDir) +waspProjectDirFromProjectRootDir = [reldir|../../|] data MigrateArgs = MigrateArgs { _migrationName :: Maybe String, diff --git a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs index 9c4aec1849..8b2a219645 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs @@ -13,24 +13,24 @@ module Wasp.Generator.DbGenerator.Jobs ) where -import StrongPath (Abs, Dir, File, File', Path', ()) +import StrongPath (Abs, Dir, File', Path', ()) import qualified StrongPath as SP import StrongPath.TH (relfile) import qualified System.Info import Wasp.Generator.Common (ProjectRootDir) import Wasp.Generator.DbGenerator.Common ( MigrateArgs (..), - PrismaDbSchema, dbSchemaFileInProjectRootDir, + waspProjectDirFromProjectRootDir, ) -import Wasp.Generator.Job (JobType) import qualified Wasp.Generator.Job as J import Wasp.Generator.Job.Process (runNodeCommandAsJob, runNodeCommandAsJobWithExtraEnv) import Wasp.Generator.ServerGenerator.Common (serverRootDirInProjectRootDir) import Wasp.Generator.ServerGenerator.Db.Seed (dbSeedNameEnvVarName) +import Wasp.Project.Common (WaspProjectDir) migrateDev :: Path' Abs (Dir ProjectRootDir) -> MigrateArgs -> J.Job -migrateDev projectDir migrateArgs = +migrateDev projectRootDir migrateArgs = -- NOTE(matija): We are running this command from server's root dir since that is where -- Prisma packages (cli and client) are currently installed. -- NOTE(martin): `prisma migrate dev` refuses to execute when interactivity is needed if stdout is being piped, @@ -40,8 +40,8 @@ migrateDev projectDir migrateArgs = -- we are using `script` to trick Prisma into thinking it is running in TTY (interactively). runNodeCommandAsJob serverDir "script" scriptArgs J.Db where - serverDir = projectDir serverRootDirInProjectRootDir - schemaFile = projectDir dbSchemaFileInProjectRootDir + serverDir = projectRootDir serverRootDirInProjectRootDir + schemaFile = projectRootDir dbSchemaFileInProjectRootDir scriptArgs = if System.Info.os == "darwin" @@ -57,7 +57,7 @@ migrateDev projectDir migrateArgs = -- * Linux - we are passing the command as one argument, so we MUST quote the paths. buildPrismaMigrateCmd :: (String -> String) -> [String] buildPrismaMigrateCmd argQuoter = - [ argQuoter $ absPrismaExecutableFp projectDir, + [ argQuoter $ absPrismaExecutableFp (projectRootDir waspProjectDirFromProjectRootDir), "migrate", "dev", "--schema", @@ -85,15 +85,19 @@ asPrismaCliArgs migrateArgs = do -- Because of the --exit-code flag, it changes the exit code behavior -- to signal if the diff is empty or not (Empty: 0, Error: 1, Not empty: 2) migrateDiff :: Path' Abs (Dir ProjectRootDir) -> J.Job -migrateDiff projectDir = runPrismaCommandAsDbJob projectDir $ \schema -> - [ "migrate", - "diff", - "--from-schema-datamodel", - SP.fromAbsFile schema, - "--to-schema-datasource", - SP.fromAbsFile schema, - "--exit-code" - ] +migrateDiff projectRootDir = + runPrismaCommandAsJob + projectRootDir + [ "migrate", + "diff", + "--from-schema-datamodel", + SP.fromAbsFile schema, + "--to-schema-datasource", + SP.fromAbsFile schema, + "--exit-code" + ] + where + schema = projectRootDir dbSchemaFileInProjectRootDir -- | Checks to see if all migrations are applied to the DB. -- An exit code of 0 means we successfully verified all migrations are applied. @@ -101,27 +105,34 @@ migrateDiff projectDir = runPrismaCommandAsDbJob projectDir $ \schema -> -- or (b) there are pending migrations to apply. -- Therefore, this should be checked **after** a command that ensures connectivity. migrateStatus :: Path' Abs (Dir ProjectRootDir) -> J.Job -migrateStatus projectDir = runPrismaCommandAsDbJob projectDir $ \schema -> - ["migrate", "status", "--schema", SP.fromAbsFile schema] +migrateStatus projectRootDir = + runPrismaCommandAsJob + projectRootDir + ["migrate", "status", "--schema", SP.fromAbsFile schema] + where + schema = projectRootDir dbSchemaFileInProjectRootDir -- | Runs `prisma migrate reset`, which drops the tables (so schemas and data is lost) and then -- reapplies all the migrations. reset :: Path' Abs (Dir ProjectRootDir) -> J.Job -reset projectDir = runPrismaCommandAsDbJob projectDir $ \schema -> - -- NOTE(martin): We do "--skip-seed" here because I just think seeding happening automatically on - -- reset is too aggressive / confusing. - ["migrate", "reset", "--schema", SP.fromAbsFile schema, "--skip-generate", "--skip-seed"] +reset projectRootDir = + runPrismaCommandAsJob + projectRootDir + -- NOTE(martin): We do "--skip-seed" here because I just think seeding happening automatically on + -- reset is too aggressive / confusing. + ["migrate", "reset", "--schema", SP.fromAbsFile schema, "--skip-generate", "--skip-seed"] + where + schema = projectRootDir dbSchemaFileInProjectRootDir -- | Runs `prisma db seed`, which executes the seeding script specified in package.json in -- prisma.seed field. seed :: Path' Abs (Dir ProjectRootDir) -> String -> J.Job -- NOTE: Since v 0.3, Prisma doesn't use --schema parameter for `db seed`. -seed projectDir seedName = +seed projectRootDir seedName = runPrismaCommandAsJobWithExtraEnv - J.Db [(dbSeedNameEnvVarName, seedName)] - projectDir - (const ["db", "seed"]) + projectRootDir + ["db", "seed"] -- | Checks if the DB is running and connectable by running -- `prisma db execute --stdin --schema `. @@ -130,56 +141,48 @@ seed projectDir seedName = -- Since nothing is passed to stdin, `prisma db execute` just runs an empty -- SQL command, which works perfectly for checking if the database is running. dbExecuteTest :: Path' Abs (Dir ProjectRootDir) -> J.Job -dbExecuteTest projectDir = - let absSchemaPath = projectDir dbSchemaFileInProjectRootDir - in runPrismaCommandAsDbJob - projectDir - (const ["db", "execute", "--stdin", "--schema", SP.fromAbsFile absSchemaPath]) +dbExecuteTest projectRootDir = + let absSchemaPath = projectRootDir dbSchemaFileInProjectRootDir + in runPrismaCommandAsJob + projectRootDir + ["db", "execute", "--stdin", "--schema", SP.fromAbsFile absSchemaPath] -- | Runs `prisma studio` - Prisma's db inspector. runStudio :: Path' Abs (Dir ProjectRootDir) -> J.Job -runStudio projectDir = runPrismaCommandAsDbJob projectDir $ \schema -> - ["studio", "--schema", SP.fromAbsFile schema] - -generatePrismaClient :: Path' Abs (Dir ProjectRootDir) -> (String, String) -> JobType -> J.Job -generatePrismaClient projectDir prismaClientOutputDirEnv jobType = - runPrismaCommandAsJobWithExtraEnv jobType envVars projectDir $ \schema -> - ["generate", "--schema", SP.fromAbsFile schema] +runStudio projectRootDir = + runPrismaCommandAsJob projectRootDir ["studio", "--schema", SP.fromAbsFile schema] where - envVars = [prismaClientOutputDirEnv] + schema = projectRootDir dbSchemaFileInProjectRootDir -runPrismaCommandAsDbJob :: - Path' Abs (Dir ProjectRootDir) -> - (Path' Abs (File PrismaDbSchema) -> [String]) -> - J.Job -runPrismaCommandAsDbJob projectDir makeCmdArgs = - runPrismaCommandAsJob J.Db projectDir makeCmdArgs +generatePrismaClient :: Path' Abs (Dir ProjectRootDir) -> J.Job +generatePrismaClient projectRootDir = + runPrismaCommandAsJob projectRootDir args + where + args = + [ "generate", + "--schema", + SP.fromAbsFile $ projectRootDir dbSchemaFileInProjectRootDir + ] -runPrismaCommandAsJob :: - JobType -> - Path' Abs (Dir ProjectRootDir) -> - (Path' Abs (File PrismaDbSchema) -> [String]) -> - J.Job -runPrismaCommandAsJob jobType projectDir makeCmdArgs = - runPrismaCommandAsJobWithExtraEnv jobType [] projectDir makeCmdArgs +runPrismaCommandAsJob :: Path' Abs (Dir ProjectRootDir) -> [String] -> J.Job +runPrismaCommandAsJob projectRootDir args = + runPrismaCommandAsJobWithExtraEnv [] projectRootDir args runPrismaCommandAsJobWithExtraEnv :: - JobType -> [(String, String)] -> Path' Abs (Dir ProjectRootDir) -> - (Path' Abs (File PrismaDbSchema) -> [String]) -> + [String] -> J.Job -runPrismaCommandAsJobWithExtraEnv jobType envVars projectDir makeCmdArgs = - runNodeCommandAsJobWithExtraEnv envVars serverDir (absPrismaExecutableFp projectDir) (makeCmdArgs schemaFile) jobType +runPrismaCommandAsJobWithExtraEnv envVars projectRootDir args = + runNodeCommandAsJobWithExtraEnv envVars waspProjectDir (absPrismaExecutableFp waspProjectDir) args J.Db where - serverDir = projectDir serverRootDirInProjectRootDir - schemaFile = projectDir dbSchemaFileInProjectRootDir + waspProjectDir = projectRootDir waspProjectDirFromProjectRootDir -- | NOTE: The expectation is that `npm install` was already executed -- such that we can use the locally installed package. -- This assumption is ok since it happens during compilation now. -absPrismaExecutableFp :: Path' Abs (Dir ProjectRootDir) -> FilePath -absPrismaExecutableFp projectDir = SP.fromAbsFile prismaExecutableAbs +absPrismaExecutableFp :: Path' Abs (Dir WaspProjectDir) -> FilePath +absPrismaExecutableFp waspProjectDir = SP.fromAbsFile prismaExecutableAbs where prismaExecutableAbs :: Path' Abs File' - prismaExecutableAbs = projectDir serverRootDirInProjectRootDir [relfile|./node_modules/.bin/prisma|] + prismaExecutableAbs = waspProjectDir [relfile|./node_modules/.bin/prisma|] diff --git a/waspc/src/Wasp/Generator/DbGenerator/Operations.hs b/waspc/src/Wasp/Generator/DbGenerator/Operations.hs index eea120200b..8596c1a418 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Operations.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Operations.hs @@ -1,6 +1,6 @@ module Wasp.Generator.DbGenerator.Operations ( migrateDevAndCopyToSource, - generatePrismaClients, + generatePrismaClient, doesSchemaMatchDb, writeDbSchemaChecksumToFile, areAllMigrationsAppliedToDb, @@ -12,13 +12,10 @@ module Wasp.Generator.DbGenerator.Operations ) where -import Control.Applicative (liftA2) import Control.Concurrent (newChan) import Control.Concurrent.Async (concurrently) -import Control.Monad (when) import Control.Monad.Catch (catch) import Control.Monad.Extra (whenM) -import Data.Either (isRight) import qualified Data.Text as T import qualified Path as P import StrongPath (Abs, Dir, File, Path', Rel, ()) @@ -36,8 +33,6 @@ import Wasp.Generator.DbGenerator.Common dbSchemaChecksumOnLastGenerateFileProjectRootDir, dbSchemaFileInProjectRootDir, getOnLastDbConcurrenceChecksumFileRefreshAction, - serverPrismaClientOutputDirEnv, - webAppPrismaClientOutputDirEnv, ) import qualified Wasp.Generator.DbGenerator.Jobs as DbJobs import Wasp.Generator.FileDraft.WriteableMonad (WriteableMonad (copyDirectoryRecursive, doesDirectoryExist)) @@ -184,31 +179,21 @@ isDbConnectionPossible DbConnectionSuccess = True isDbConnectionPossible DbNotCreated = True isDbConnectionPossible _ = False -generatePrismaClients :: Path' Abs (Dir ProjectRootDir) -> IO (Either String ()) -generatePrismaClients projectRootDir = do - generateResult <- liftA2 (>>) generatePrismaClientForServer generatePrismaClientForWebApp projectRootDir - when (isRight generateResult) updateDbSchemaChecksumOnLastGenerate - return generateResult - where - generatePrismaClientForServer = generatePrismaClient serverPrismaClientOutputDirEnv J.Server - generatePrismaClientForWebApp = generatePrismaClient webAppPrismaClientOutputDirEnv J.WebApp - updateDbSchemaChecksumOnLastGenerate = - writeDbSchemaChecksumToFile projectRootDir dbSchemaChecksumOnLastGenerateFileProjectRootDir - -generatePrismaClient :: - (String, String) -> - J.JobType -> - Path' Abs (Dir ProjectRootDir) -> - IO (Either String ()) -generatePrismaClient prismaClientOutputDirEnv jobType projectRootDir = do +generatePrismaClient :: Path' Abs (Dir ProjectRootDir) -> IO (Either String ()) +generatePrismaClient projectRootDir = do chan <- newChan (_, exitCode) <- concurrently (readJobMessagesAndPrintThemPrefixed chan) - (DbJobs.generatePrismaClient projectRootDir prismaClientOutputDirEnv jobType chan) - return $ case exitCode of - ExitSuccess -> Right () - ExitFailure code -> Left $ "Prisma client generation failed with exit code: " ++ show code + (DbJobs.generatePrismaClient projectRootDir chan) + case exitCode of + ExitFailure code -> return $ Left $ "Prisma client generation failed with exit code: " ++ show code + ExitSuccess -> do + updateDbSchemaChecksumOnLastGenerate + return $ Right () + where + updateDbSchemaChecksumOnLastGenerate = + writeDbSchemaChecksumToFile projectRootDir dbSchemaChecksumOnLastGenerateFileProjectRootDir -- | Checks `prisma migrate diff` exit code to determine if schema.prisma is -- different than the DB. Returns Nothing on error as we do not know the current state. diff --git a/waspc/src/Wasp/Generator/DockerGenerator.hs b/waspc/src/Wasp/Generator/DockerGenerator.hs index 0654ecfe61..41817099b8 100644 --- a/waspc/src/Wasp/Generator/DockerGenerator.hs +++ b/waspc/src/Wasp/Generator/DockerGenerator.hs @@ -23,7 +23,6 @@ import Wasp.Generator.Common import Wasp.Generator.DbGenerator.Common ( PrismaDbSchema, dbSchemaFileFromAppComponentDir, - serverPrismaClientOutputDirEnv, ) import Wasp.Generator.FileDraft (FileDraft (..), createTemplateFileDraft) import qualified Wasp.Generator.FileDraft.TemplateFileDraft as TmplFD @@ -31,7 +30,6 @@ import Wasp.Generator.Monad (Generator, GeneratorError, runGenerator) import Wasp.Generator.Templates (TemplatesDir, compileAndRenderTemplate) import Wasp.Node.Version (latestMajorNodeVersion) import qualified Wasp.SemanticVersion as SV -import Wasp.Util (getEnvVarDefinition) genDockerFiles :: AppSpec -> Generator [FileDraft] genDockerFiles spec = sequence [genDockerfile spec, genDockerignore spec] @@ -47,7 +45,6 @@ genDockerfile spec = do ( Just $ object [ "usingPrisma" .= not (null $ AS.getDecls @AS.Entity.Entity spec), - "serverPrismaClientOutputDirEnv" .= getEnvVarDefinition serverPrismaClientOutputDirEnv, "dbSchemaFileFromServerDir" .= SP.fromRelFile dbSchemaFileFromServerDir, "nodeMajorVersion" .= show (SV.major latestMajorNodeVersion), "userDockerfile" .= fromMaybe "" (AS.userDockerfileContents spec) diff --git a/waspc/src/Wasp/Generator/ServerGenerator.hs b/waspc/src/Wasp/Generator/ServerGenerator.hs index e3db939cef..e16ca7a342 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator.hs @@ -45,7 +45,6 @@ import Wasp.Env (envVarsToDotEnvContent) import Wasp.Generator.Common ( ServerRootDir, makeJsonWithEntityData, - prismaVersion, ) import Wasp.Generator.FileDraft (FileDraft, createTextFileDraft) import Wasp.Generator.Monad (Generator) @@ -152,7 +151,6 @@ npmDepsForWasp spec = ("cors", "^2.8.5"), ("express", "~4.18.1"), ("morgan", "~1.10.0"), - ("@prisma/client", show prismaVersion), ("jsonwebtoken", "^8.5.1"), -- NOTE: secure-password has a package.json override for sodium-native. ("secure-password", "^4.0.0"), @@ -172,7 +170,6 @@ npmDepsForWasp spec = AS.Dependency.fromList [ ("nodemon", "^2.0.19"), ("standard", "^17.0.0"), - ("prisma", show prismaVersion), -- TODO: Allow users to choose whether they want to use TypeScript -- in their projects and install these dependencies accordingly. ("typescript", "^5.1.0"), From a1dfcddaf514bd58bf364d9e05c21ee136dc7939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Tue, 19 Dec 2023 13:15:11 +0100 Subject: [PATCH 27/54] Rename args to cmdArgs --- waspc/examples/todo-typescript/package.json | 1 - waspc/src/Wasp/Generator/DbGenerator/Jobs.hs | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/waspc/examples/todo-typescript/package.json b/waspc/examples/todo-typescript/package.json index dce94f95cb..7e6222db84 100644 --- a/waspc/examples/todo-typescript/package.json +++ b/waspc/examples/todo-typescript/package.json @@ -2,7 +2,6 @@ "name": "prototype", "dependencies": { "wasp": "file:.wasp/out/sdk/wasp", - "@prisma/client": "4.16.2", "react": "^18.2.0" }, "devDependencies": { diff --git a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs index 8b2a219645..94990324e0 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs @@ -156,25 +156,25 @@ runStudio projectRootDir = generatePrismaClient :: Path' Abs (Dir ProjectRootDir) -> J.Job generatePrismaClient projectRootDir = - runPrismaCommandAsJob projectRootDir args + runPrismaCommandAsJob projectRootDir cmdArgs where - args = + cmdArgs = [ "generate", "--schema", SP.fromAbsFile $ projectRootDir dbSchemaFileInProjectRootDir ] runPrismaCommandAsJob :: Path' Abs (Dir ProjectRootDir) -> [String] -> J.Job -runPrismaCommandAsJob projectRootDir args = - runPrismaCommandAsJobWithExtraEnv [] projectRootDir args +runPrismaCommandAsJob projectRootDir cmdArgs = + runPrismaCommandAsJobWithExtraEnv [] projectRootDir cmdArgs runPrismaCommandAsJobWithExtraEnv :: [(String, String)] -> Path' Abs (Dir ProjectRootDir) -> [String] -> J.Job -runPrismaCommandAsJobWithExtraEnv envVars projectRootDir args = - runNodeCommandAsJobWithExtraEnv envVars waspProjectDir (absPrismaExecutableFp waspProjectDir) args J.Db +runPrismaCommandAsJobWithExtraEnv envVars projectRootDir cmdArgs = + runNodeCommandAsJobWithExtraEnv envVars waspProjectDir (absPrismaExecutableFp waspProjectDir) cmdArgs J.Db where waspProjectDir = projectRootDir waspProjectDirFromProjectRootDir From a9854463755afb6022d50860e908ebaaae44d074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Tue, 19 Dec 2023 13:17:48 +0100 Subject: [PATCH 28/54] Fix formatting --- waspc/cli/src/Wasp/Cli/Command/Common.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/waspc/cli/src/Wasp/Cli/Command/Common.hs b/waspc/cli/src/Wasp/Cli/Command/Common.hs index a1abd809a4..bcee71ee83 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Common.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Common.hs @@ -59,4 +59,4 @@ deleteDirectoryIfExists dir = do deleteDotWaspDirIfExists :: Path' Abs (Dir WaspProjectDir) -> Command () deleteDotWaspDirIfExists waspProjectDir = deleteDirectoryIfExists dotWaspDir where - dotWaspDir = waspProjectDir SP. Cli.Common.dotWaspDirInWaspProjectDir \ No newline at end of file + dotWaspDir = waspProjectDir SP. Cli.Common.dotWaspDirInWaspProjectDir From 53ac4505514607ac8dbb6bea2c8615c440320a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Tue, 19 Dec 2023 13:19:09 +0100 Subject: [PATCH 29/54] Fix formatting --- waspc/cli/src/Wasp/Cli/Command/Reset.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/waspc/cli/src/Wasp/Cli/Command/Reset.hs b/waspc/cli/src/Wasp/Cli/Command/Reset.hs index d49f1a6d45..24612826bc 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Reset.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Reset.hs @@ -21,4 +21,4 @@ reset = do deleteNodeModulesDirIfExists :: Path' Abs (Dir WaspProjectDir) -> Command () deleteNodeModulesDirIfExists waspProjectDir = deleteDirectoryIfExists nodeModulesDir where - nodeModulesDir = waspProjectDir SP. Common.nodeModulesDirInWaspProjectDir \ No newline at end of file + nodeModulesDir = waspProjectDir SP. Common.nodeModulesDirInWaspProjectDir From ea5a4c145835e92d37ab408dc05ccfdc877c88f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Tue, 19 Dec 2023 14:58:41 +0100 Subject: [PATCH 30/54] Remove @prisma/client from project deps --- waspc/data/Cli/templates/basic/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/waspc/data/Cli/templates/basic/package.json b/waspc/data/Cli/templates/basic/package.json index dce94f95cb..7e6222db84 100644 --- a/waspc/data/Cli/templates/basic/package.json +++ b/waspc/data/Cli/templates/basic/package.json @@ -2,7 +2,6 @@ "name": "prototype", "dependencies": { "wasp": "file:.wasp/out/sdk/wasp", - "@prisma/client": "4.16.2", "react": "^18.2.0" }, "devDependencies": { From a5a7382a3042322443dc653c047bb5845445d536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Mon, 8 Jan 2024 14:42:04 +0100 Subject: [PATCH 31/54] Make prototype work with inject auth changes --- .../Generator/templates/sdk/wasp/api/index.ts | 13 +- .../templates/sdk/wasp/auth/forms/Auth.tsx | 42 ++- .../templates/sdk/wasp/auth/forms/Login.tsx | 7 +- .../templates/sdk/wasp/auth/forms/Signup.tsx | 15 +- .../sdk/wasp/auth/forms/internal/Form.tsx | 14 +- .../forms/internal/common/LoginSignupForm.tsx | 207 +++++++++---- .../useUsernameAndPassword.ts | 19 +- .../templates/sdk/wasp/auth/forms/types.ts | 22 ++ .../templates/sdk/wasp/auth/helpers/user.ts | 16 +- .../templates/sdk/wasp/auth/login.ts | 7 +- .../auth/pages/createAuthRequiredPage.jsx | 28 +- .../sdk/wasp/auth/providers/types.ts | 38 +++ .../templates/sdk/wasp/auth/signup.ts | 7 +- .../sdk/wasp/auth/stitches.config.js | 31 -- .../templates/sdk/wasp/auth/types.ts | 3 +- .../templates/sdk/wasp/auth/useAuth.ts | 11 +- .../Generator/templates/sdk/wasp/auth/user.ts | 27 ++ .../templates/sdk/wasp/auth/utils.ts | 290 ++++++++++++++++++ .../templates/sdk/wasp/auth/validation.ts | 77 +++++ .../Generator/templates/sdk/wasp/core/auth.js | 27 +- .../sdk/wasp/core/auth/prismaMiddleware.js | 84 ----- .../sdk/wasp/core/auth/validators.ts | 5 - .../templates/sdk/wasp/core/config.js | 4 +- .../sdk/wasp/core/stitches.config.js | 2 + .../templates/sdk/wasp/entities/index.ts | 2 + .../templates/sdk/wasp/operations/index.ts | 11 +- .../sdk/wasp/operations/resources.js | 14 +- .../templates/sdk/wasp/rpc/actions/core.js | 7 +- .../templates/sdk/wasp/rpc/queries/core.js | 2 +- .../templates/sdk/wasp/server/_types/index.ts | 99 +++--- .../sdk/wasp/server/_types/serialization.ts | 42 ++- .../sdk/wasp/server/_types/taggedEntities.ts | 20 +- .../sdk/wasp/server/core/AuthError.js | 17 - .../sdk/wasp/server/core/HttpError.js | 22 -- .../templates/sdk/wasp/server/core/auth.js | 185 ----------- .../wasp/server/core/auth/prismaMiddleware.js | 84 ----- .../sdk/wasp/server/core/auth/validators.ts | 5 - .../templates/sdk/wasp/server/dbClient.ts | 3 - .../templates/sdk/wasp/server/utils.ts | 66 ++++ waspc/examples/todo-typescript/main.wasp | 2 - .../examples/todo-typescript/src/MainPage.tsx | 6 +- waspc/src/Wasp/Generator/SdkGenerator.hs | 1 + 42 files changed, 881 insertions(+), 703 deletions(-) create mode 100644 waspc/data/Generator/templates/sdk/wasp/auth/providers/types.ts delete mode 100644 waspc/data/Generator/templates/sdk/wasp/auth/stitches.config.js create mode 100644 waspc/data/Generator/templates/sdk/wasp/auth/user.ts create mode 100644 waspc/data/Generator/templates/sdk/wasp/auth/utils.ts create mode 100644 waspc/data/Generator/templates/sdk/wasp/auth/validation.ts delete mode 100644 waspc/data/Generator/templates/sdk/wasp/core/auth/prismaMiddleware.js delete mode 100644 waspc/data/Generator/templates/sdk/wasp/core/auth/validators.ts delete mode 100644 waspc/data/Generator/templates/sdk/wasp/server/core/AuthError.js delete mode 100644 waspc/data/Generator/templates/sdk/wasp/server/core/HttpError.js delete mode 100644 waspc/data/Generator/templates/sdk/wasp/server/core/auth.js delete mode 100644 waspc/data/Generator/templates/sdk/wasp/server/core/auth/prismaMiddleware.js delete mode 100644 waspc/data/Generator/templates/sdk/wasp/server/core/auth/validators.ts create mode 100644 waspc/data/Generator/templates/sdk/wasp/server/utils.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/api/index.ts b/waspc/data/Generator/templates/sdk/wasp/api/index.ts index 7c3b69d863..9aad1ead59 100644 --- a/waspc/data/Generator/templates/sdk/wasp/api/index.ts +++ b/waspc/data/Generator/templates/sdk/wasp/api/index.ts @@ -4,7 +4,6 @@ import config from 'wasp/core/config' import { storage } from 'wasp/core/storage' import { apiEventsEmitter } from 'wasp/api/events' -console.log('Top level node module') const api = axios.create({ baseURL: config.apiUrl, }) @@ -71,9 +70,7 @@ window.addEventListener('storage', (event) => { * standard format to be further used by the client. It is also assumed that given API * error has been formatted as implemented by HttpError on the server. */ -export function handleApiError( - error: AxiosError<{ message?: string; data?: unknown }> -): void { +export function handleApiError(error: AxiosError<{ message?: string, data?: unknown }>): void { if (error?.response) { // If error came from HTTP response, we capture most informative message // and also add .statusCode information to it. @@ -84,11 +81,7 @@ export function handleApiError( // That would require copying HttpError code to web-app also and using it here. const responseJson = error.response?.data const responseStatusCode = error.response.status - throw new WaspHttpError( - responseStatusCode, - responseJson?.message ?? error.message, - responseJson - ) + throw new WaspHttpError(responseStatusCode, responseJson?.message ?? error.message, responseJson) } else { // If any other error, we just propagate it. throw error @@ -100,7 +93,7 @@ class WaspHttpError extends Error { data: unknown - constructor(statusCode: number, message: string, data: unknown) { + constructor (statusCode: number, message: string, data: unknown) { super(message) this.statusCode = statusCode this.data = data diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/forms/Auth.tsx b/waspc/data/Generator/templates/sdk/wasp/auth/forms/Auth.tsx index e532fd5d59..92c58131f6 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/forms/Auth.tsx +++ b/waspc/data/Generator/templates/sdk/wasp/auth/forms/Auth.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { useState, createContext } from 'react' import { createTheme } from '@stitches/react' import { styled } from 'wasp/core/stitches.config' @@ -7,12 +6,13 @@ import { type State, type CustomizationOptions, type ErrorMessage, + type AdditionalSignupFields, } from './types' import { LoginSignupForm } from './internal/common/LoginSignupForm' import { MessageError, MessageSuccess } from './internal/Message' const logoStyle = { - height: '3rem', + height: '3rem' } const Container = styled('div', { @@ -23,9 +23,10 @@ const Container = styled('div', { const HeaderText = styled('h2', { fontSize: '1.875rem', fontWeight: '700', - marginTop: '1.5rem', + marginTop: '1.5rem' }) + export const AuthContext = createContext({ isLoading: false, setIsLoading: (isLoading: boolean) => {}, @@ -33,17 +34,14 @@ export const AuthContext = createContext({ setSuccessMessage: (successMessage: string | null) => {}, }) -export function Auth({ - state, - appearance, - logo, - socialLayout = 'horizontal', -}: { - state: State -} & CustomizationOptions) { - const [errorMessage, setErrorMessage] = useState(null) - const [successMessage, setSuccessMessage] = useState(null) - const [isLoading, setIsLoading] = useState(false) +function Auth ({ state, appearance, logo, socialLayout = 'horizontal', additionalSignupFields }: { + state: State; +} & CustomizationOptions & { + additionalSignupFields?: AdditionalSignupFields; +}) { + const [errorMessage, setErrorMessage] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + const [isLoading, setIsLoading] = useState(false); // TODO(matija): this is called on every render, is it a problem? // If we do it in useEffect(), then there is a glitch between the default color and the @@ -56,34 +54,32 @@ export function Auth({ } const title = titles[state] - const socialButtonsDirection = - socialLayout === 'vertical' ? 'vertical' : 'horizontal' + const socialButtonsDirection = socialLayout === 'vertical' ? 'vertical' : 'horizontal' return (
    - {logo && Your Company} + {logo && (Your Company)} {title}
    {errorMessage && ( - {errorMessage.title} - {errorMessage.description && ': '} - {errorMessage.description} + {errorMessage.title}{errorMessage.description && ': '}{errorMessage.description} )} {successMessage && {successMessage}} - + {(state === 'login' || state === 'signup') && ( )}
    ) } + +export default Auth; diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/forms/Login.tsx b/waspc/data/Generator/templates/sdk/wasp/auth/forms/Login.tsx index 19ff0fa583..2ea532d9c5 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/forms/Login.tsx +++ b/waspc/data/Generator/templates/sdk/wasp/auth/forms/Login.tsx @@ -1,6 +1,5 @@ -import React from "react"; -import { Auth } from "./Auth"; -import { type CustomizationOptions, State } from "./types"; +import Auth from './Auth' +import { type CustomizationOptions, State } from './types' export function LoginForm({ appearance, @@ -14,5 +13,5 @@ export function LoginForm({ socialLayout={socialLayout} state={State.Login} /> - ); + ) } diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/forms/Signup.tsx b/waspc/data/Generator/templates/sdk/wasp/auth/forms/Signup.tsx index 8978d891d9..66ffab4503 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/forms/Signup.tsx +++ b/waspc/data/Generator/templates/sdk/wasp/auth/forms/Signup.tsx @@ -1,18 +1,23 @@ -import React from "react"; -import { Auth } from "./Auth"; -import { type CustomizationOptions, State } from "./types"; +import Auth from './Auth' +import { + type CustomizationOptions, + type AdditionalSignupFields, + State, +} from './types' export function SignupForm({ appearance, logo, socialLayout, -}: CustomizationOptions) { + additionalFields, +}: CustomizationOptions & { additionalFields?: AdditionalSignupFields; }) { return ( - ); + ) } diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/Form.tsx b/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/Form.tsx index 0feab2d646..781c75a0ae 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/Form.tsx +++ b/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/Form.tsx @@ -14,9 +14,10 @@ export const FormLabel = styled('label', { display: 'block', fontSize: '$sm', fontWeight: '500', + marginBottom: '0.5rem', }) -export const FormInput = styled('input', { +const commonInputStyles = { display: 'block', lineHeight: '1.5rem', fontSize: '$sm', @@ -44,7 +45,18 @@ export const FormInput = styled('input', { paddingBottom: '0.375rem', paddingLeft: '0.75rem', paddingRight: '0.75rem', + margin: 0, +} +export const FormInput = styled('input', commonInputStyles) + +export const FormTextarea = styled('textarea', commonInputStyles) + +export const FormError = styled('div', { + display: 'block', + fontSize: '$sm', + fontWeight: '500', + color: '$formErrorText', marginTop: '0.5rem', }) diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/common/LoginSignupForm.tsx b/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/common/LoginSignupForm.tsx index 084d91c802..30665b4759 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/common/LoginSignupForm.tsx +++ b/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/common/LoginSignupForm.tsx @@ -1,4 +1,5 @@ -import { useContext, type FormEvent } from 'react' +import { useContext } from 'react' +import { useForm, UseFormReturn } from 'react-hook-form' import { styled } from 'wasp/core/stitches.config' import config from 'wasp/core/config' @@ -8,82 +9,170 @@ import { FormInput, FormItemGroup, FormLabel, + FormError, + FormTextarea, SubmitButton, } from '../Form' +import type { + AdditionalSignupFields, + AdditionalSignupField, + AdditionalSignupFieldRenderFn, + FormState, +} from '../../types' import { useHistory } from 'react-router-dom' import { useUsernameAndPassword } from '../usernameAndPassword/useUsernameAndPassword' + +export type LoginSignupFormFields = { + [key: string]: string; +} + export const LoginSignupForm = ({ - state, - socialButtonsDirection = 'horizontal', + state, + socialButtonsDirection = 'horizontal', + additionalSignupFields, }: { - state: 'login' | 'signup' - socialButtonsDirection?: 'horizontal' | 'vertical' + state: 'login' | 'signup' + socialButtonsDirection?: 'horizontal' | 'vertical' + additionalSignupFields?: AdditionalSignupFields }) => { - const { isLoading, setErrorMessage, setSuccessMessage, setIsLoading } = - useContext(AuthContext) - const cta = state === 'login' ? 'Log in' : 'Sign up' - const history = useHistory() - const onErrorHandler = (error) => { - setErrorMessage({ - title: error.message, - description: error.data?.data?.message, - }) - } const { - handleSubmit, - usernameFieldVal, - passwordFieldVal, - setUsernameFieldVal, - setPasswordFieldVal, - } = useUsernameAndPassword({ - isLogin: state === 'login', + isLoading, + setErrorMessage, + setSuccessMessage, + setIsLoading, + } = useContext(AuthContext) + const isLogin = state === 'login' + const cta = isLogin ? 'Log in' : 'Sign up'; + const history = useHistory(); + const onErrorHandler = (error) => { + setErrorMessage({ title: error.message, description: error.data?.data?.message }) + }; + const hookForm = useForm() + const { register, formState: { errors }, handleSubmit: hookFormHandleSubmit } = hookForm + const { handleSubmit } = useUsernameAndPassword({ + isLogin, onError: onErrorHandler, onSuccess() { history.push('/') }, - }) - async function onSubmit(event: FormEvent) { - event.preventDefault() - setIsLoading(true) - setErrorMessage(null) - setSuccessMessage(null) + }); + async function onSubmit (data) { + setIsLoading(true); + setErrorMessage(null); + setSuccessMessage(null); try { - await handleSubmit() + await handleSubmit(data); } finally { - setIsLoading(false) + setIsLoading(false); } } - return ( - <> -
    - - Username - setUsernameFieldVal(e.target.value)} - disabled={isLoading} - /> - - - Password - setPasswordFieldVal(e.target.value)} - disabled={isLoading} + return (<> + + + Username + + {errors.username && {errors.username.message}} + + + Password + + {errors.password && {errors.password.message}} + + - - - - {cta} - - -
    - + + {cta} + + + ) +} + +function AdditionalFormFields({ + hookForm, + formState: { isLoading }, + additionalSignupFields, +}: { + hookForm: UseFormReturn; + formState: FormState; + additionalSignupFields: AdditionalSignupFields; +}) { + const { + register, + formState: { errors }, + } = hookForm; + + function renderField>( + field: AdditionalSignupField, + // Ideally we would use ComponentType here, but it doesn't work with react-hook-form + Component: any, + props?: React.ComponentProps + ) { + return ( + + {field.label} + + {errors[field.name] && ( + {errors[field.name].message} + )} + + ); + } + + if (areAdditionalFieldsRenderFn(additionalSignupFields)) { + return additionalSignupFields(hookForm, { isLoading }) + } + + return ( + additionalSignupFields && + additionalSignupFields.map((field) => { + if (isFieldRenderFn(field)) { + return field(hookForm, { isLoading }) + } + switch (field.type) { + case 'input': + return renderField(field, FormInput, { + type: 'text', + }) + case 'textarea': + return renderField(field, FormTextarea) + default: + throw new Error( + `Unsupported additional signup field type: ${field.type}` + ) + } + }) ) } + +function isFieldRenderFn( + additionalSignupField: AdditionalSignupField | AdditionalSignupFieldRenderFn +): additionalSignupField is AdditionalSignupFieldRenderFn { + return typeof additionalSignupField === 'function' +} + +function areAdditionalFieldsRenderFn( + additionalSignupFields: AdditionalSignupFields +): additionalSignupFields is AdditionalSignupFieldRenderFn { + return typeof additionalSignupFields === 'function' +} diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts b/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts index 3c27fc84a3..247c1faeb4 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts +++ b/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts @@ -1,4 +1,3 @@ -import { useState } from 'react' import signup from '../../../signup' import login from '../../../login' @@ -11,21 +10,13 @@ export function useUsernameAndPassword({ onSuccess: () => void isLogin: boolean }) { - const [usernameFieldVal, setUsernameFieldVal] = useState('') - const [passwordFieldVal, setPasswordFieldVal] = useState('') - - async function handleSubmit() { + async function handleSubmit(data) { try { if (!isLogin) { - await signup({ - username: usernameFieldVal, - password: passwordFieldVal, - }) + await signup(data) } - await login(usernameFieldVal, passwordFieldVal) + await login(data.username, data.password) - setUsernameFieldVal('') - setPasswordFieldVal('') onSuccess() } catch (err: unknown) { onError(err as Error) @@ -34,9 +25,5 @@ export function useUsernameAndPassword({ return { handleSubmit, - usernameFieldVal, - passwordFieldVal, - setUsernameFieldVal, - setPasswordFieldVal, } } diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/forms/types.ts b/waspc/data/Generator/templates/sdk/wasp/auth/forms/types.ts index 296fe1a98f..14d61ad51e 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/forms/types.ts +++ b/waspc/data/Generator/templates/sdk/wasp/auth/forms/types.ts @@ -1,4 +1,6 @@ import { createTheme } from '@stitches/react' +import { UseFormReturn, RegisterOptions } from 'react-hook-form' +import type { LoginSignupFormFields } from './internal/common/LoginSignupForm' export enum State { Login = 'login', @@ -15,3 +17,23 @@ export type ErrorMessage = { title: string description?: string } + +export type FormState = { + isLoading: boolean +} + +export type AdditionalSignupFieldRenderFn = ( + hookForm: UseFormReturn, + formState: FormState +) => React.ReactNode + +export type AdditionalSignupField = { + name: string + label: string + type: 'input' | 'textarea' + validations?: RegisterOptions +} + +export type AdditionalSignupFields = + | (AdditionalSignupField | AdditionalSignupFieldRenderFn)[] + | AdditionalSignupFieldRenderFn diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/helpers/user.ts b/waspc/data/Generator/templates/sdk/wasp/auth/helpers/user.ts index 5669f2f00e..c3e6a4072b 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/helpers/user.ts +++ b/waspc/data/Generator/templates/sdk/wasp/auth/helpers/user.ts @@ -2,13 +2,13 @@ import { setAuthToken } from 'wasp/api' import { invalidateAndRemoveQueries } from 'wasp/operations/resources' export async function initSession(token: string): Promise { - setAuthToken(token) - // We need to invalidate queries after login in order to get the correct user - // data in the React components (using `useAuth`). - // Redirects after login won't work properly without this. + setAuthToken(token) + // We need to invalidate queries after login in order to get the correct user + // data in the React components (using `useAuth`). + // Redirects after login won't work properly without this. - // TODO(filip): We are currently removing all the queries, but we should - // remove only non-public, user-dependent queries - public queries are - // expected not to change in respect to the currently logged in user. - await invalidateAndRemoveQueries() + // TODO(filip): We are currently removing all the queries, but we should + // remove only non-public, user-dependent queries - public queries are + // expected not to change in respect to the currently logged in user. + await invalidateAndRemoveQueries() } diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/login.ts b/waspc/data/Generator/templates/sdk/wasp/auth/login.ts index 3ba94755e5..487b45b981 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/login.ts +++ b/waspc/data/Generator/templates/sdk/wasp/auth/login.ts @@ -1,13 +1,10 @@ import api, { handleApiError } from 'wasp/api' import { initSession } from './helpers/user' -export default async function login( - username: string, - password: string -): Promise { +export default async function login(username: string, password: string): Promise { try { const args = { username, password } - const response = await api.post('/auth/local/login', args) + const response = await api.post('/auth/username/login', args) await initSession(response.data.token) } catch (error) { diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/pages/createAuthRequiredPage.jsx b/waspc/data/Generator/templates/sdk/wasp/auth/pages/createAuthRequiredPage.jsx index 33cb648ed1..621ef393d9 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/pages/createAuthRequiredPage.jsx +++ b/waspc/data/Generator/templates/sdk/wasp/auth/pages/createAuthRequiredPage.jsx @@ -1,26 +1,30 @@ -import React from "react"; +import React from 'react' + +import { Redirect } from 'react-router-dom' +import useAuth from '../useAuth' -import { Redirect } from "react-router-dom"; -import useAuth from "../useAuth"; const createAuthRequiredPage = (Page) => { return (props) => { - const { data: user, isError, isSuccess, isLoading } = useAuth(); + const { data: user, isError, isSuccess, isLoading } = useAuth() if (isSuccess) { if (user) { - return ; + return ( + + ) } else { - return ; + return } } else if (isLoading) { - return Loading...; + return Loading... } else if (isError) { - return An error ocurred. Please refresh the page.; + return An error ocurred. Please refresh the page. } else { - return An unknown error ocurred. Please refresh the page.; + return An unknown error ocurred. Please refresh the page. } - }; -}; + } +} + +export default createAuthRequiredPage -export default createAuthRequiredPage; diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/providers/types.ts b/waspc/data/Generator/templates/sdk/wasp/auth/providers/types.ts new file mode 100644 index 0000000000..5bbc99ca83 --- /dev/null +++ b/waspc/data/Generator/templates/sdk/wasp/auth/providers/types.ts @@ -0,0 +1,38 @@ +import type { Router, Request } from 'express' +import type { Prisma } from '@prisma/client' +import type { Expand } from 'wasp/universal/types' +import type { ProviderName } from '../utils' + +type UserEntityCreateInput = Prisma.UserCreateInput + +export type ProviderConfig = { + // Unique provider identifier, used as part of URL paths + id: ProviderName; + displayName: string; + // Each provider config can have an init method which is ran on setup time + // e.g. for oAuth providers this is the time when the Passport strategy is registered. + init?(provider: ProviderConfig): Promise; + // Every provider must have a setupRouter method which returns the Express router. + // In this function we are flexibile to do what ever is necessary to make the provider work. + createRouter(provider: ProviderConfig, initData: InitData): Router; +}; + +export type InitData = { + [key: string]: any; +} + +export type RequestWithWasp = Request & { wasp?: { [key: string]: any } } + +export type PossibleAdditionalSignupFields = Expand> + +export function defineAdditionalSignupFields(config: { + [key in keyof PossibleAdditionalSignupFields]: FieldGetter< + PossibleAdditionalSignupFields[key] + > +}) { + return config +} + +type FieldGetter = ( + data: { [key: string]: unknown } +) => Promise | T | undefined diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/signup.ts b/waspc/data/Generator/templates/sdk/wasp/auth/signup.ts index 86eff253e1..bde50c5ebd 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/signup.ts +++ b/waspc/data/Generator/templates/sdk/wasp/auth/signup.ts @@ -1,11 +1,8 @@ import api, { handleApiError } from 'wasp/api' -export default async function signup(userFields: { - username: string - password: string -}): Promise { +export default async function signup(userFields: { username: string; password: string }): Promise { try { - await api.post('/auth/local/signup', userFields) + await api.post('/auth/username/signup', userFields) } catch (error) { handleApiError(error) } diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/stitches.config.js b/waspc/data/Generator/templates/sdk/wasp/auth/stitches.config.js deleted file mode 100644 index 741c205603..0000000000 --- a/waspc/data/Generator/templates/sdk/wasp/auth/stitches.config.js +++ /dev/null @@ -1,31 +0,0 @@ -import { createStitches } from '@stitches/react' - -export const { - styled, - css -} = createStitches({ - theme: { - colors: { - waspYellow: '#ffcc00', - gray700: '#a1a5ab', - gray600: '#d1d5db', - gray500: 'gainsboro', - gray400: '#f0f0f0', - red: '#FED7D7', - green: '#C6F6D5', - - brand: '$waspYellow', - brandAccent: '#ffdb46', - errorBackground: '$red', - errorText: '#2D3748', - successBackground: '$green', - successText: '#2D3748', - - submitButtonText: 'black', - - }, - fontSizes: { - sm: '0.875rem' - } - } -}) diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/types.ts b/waspc/data/Generator/templates/sdk/wasp/auth/types.ts index 861a4aefb2..9240b4e4b0 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/types.ts +++ b/waspc/data/Generator/templates/sdk/wasp/auth/types.ts @@ -1 +1,2 @@ -export { type SanitizedUser as User } from 'wasp/server/_types/' +// todo(filip): turn into a proper import/path +export type { SanitizedUser as User, ProviderName, DeserializedAuthEntity } from 'wasp/server/_types/' diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/useAuth.ts b/waspc/data/Generator/templates/sdk/wasp/auth/useAuth.ts index ab40a41edb..29b95f62a0 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/useAuth.ts +++ b/waspc/data/Generator/templates/sdk/wasp/auth/useAuth.ts @@ -9,28 +9,25 @@ export const getMe = createUserGetter() export default function useAuth(queryFnArgs?: unknown, config?: any) { return useQuery(getMe, queryFnArgs, config) -} +} function createUserGetter() { const getMeRelativePath = 'auth/me' - /* filip */ console.log('tu smo') const getMeRoute = { method: HttpMethod.Get, path: `/${getMeRelativePath}` } async function getMe(): Promise { try { const response = await api.get(getMeRoute.path) - - /* filip */ console.log('uspio') + return superjsonDeserialize(response.data) } catch (error) { - /* filip */ console.log('failao') if (error.response?.status === 401) { + return null } else { handleApiError(error) } - return null } } - + addMetadataToQuery(getMe, { relativeQueryPath: getMeRelativePath, queryRoute: getMeRoute, diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/user.ts b/waspc/data/Generator/templates/sdk/wasp/auth/user.ts new file mode 100644 index 0000000000..5799c71ea7 --- /dev/null +++ b/waspc/data/Generator/templates/sdk/wasp/auth/user.ts @@ -0,0 +1,27 @@ +// We decided not to deduplicate these helper functions in the server and the client. +// We have them duplicated in this file and in data/Generator/templates/server/src/auth/user.ts +// If you are changing the logic here, make sure to change it there as well. + +import type { User, ProviderName, DeserializedAuthEntity } from './types' + +export function getEmail(user: User): string | null { + return findUserIdentity(user, "email")?.providerUserId ?? null; +} + +export function getUsername(user: User): string | null { + return findUserIdentity(user, "username")?.providerUserId ?? null; +} + +export function getFirstProviderUserId(user?: User): string | null { + if (!user || !user.auth || !user.auth.identities || user.auth.identities.length === 0) { + return null; + } + + return user.auth.identities[0].providerUserId ?? null; +} + +export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthEntity | undefined { + return user.auth.identities.find( + (identity) => identity.providerName === providerName + ); +} diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/utils.ts b/waspc/data/Generator/templates/sdk/wasp/auth/utils.ts new file mode 100644 index 0000000000..7a180abdc6 --- /dev/null +++ b/waspc/data/Generator/templates/sdk/wasp/auth/utils.ts @@ -0,0 +1,290 @@ +import { hashPassword, sign, verify } from 'wasp/core/auth' +import AuthError from '../core/AuthError.js' +import HttpError from '../core/HttpError.js' +import prisma from '../server/dbClient.js' +import { sleep } from '../server/utils' +import { + type User, + type Auth, + type AuthIdentity, +} from '../entities' +import { Prisma } from '@prisma/client'; + +import { throwValidationError } from './validation.js' + + +import { defineAdditionalSignupFields, type PossibleAdditionalSignupFields } from './providers/types.js' +const _waspAdditionalSignupFieldsConfig = {} as ReturnType + +export type EmailProviderData = { + hashedPassword: string; + isEmailVerified: boolean; + emailVerificationSentAt: string | null; + passwordResetSentAt: string | null; +} + +export type UsernameProviderData = { + hashedPassword: string; +} + +export type OAuthProviderData = {} + +/** + * This type is used for type-level programming e.g. to enumerate + * all possible provider data types. + * + * The keys of this type are the names of the providers and the values + * are the types of the provider data. + */ +export type PossibleProviderData = { + email: EmailProviderData; + username: UsernameProviderData; + google: OAuthProviderData; + github: OAuthProviderData; +} + +export type ProviderName = keyof PossibleProviderData + +export const contextWithUserEntity = { + entities: { + User: prisma.user + } +} + +export const authConfig = { + failureRedirectPath: "/login", + successRedirectPath: "/", +} + +/** + * ProviderId uniquely identifies an auth identity e.g. + * "email" provider with user id "test@test.com" or + * "google" provider with user id "1234567890". + * + * We use this type to avoid passing the providerName and providerUserId + * separately. Also, we can normalize the providerUserId to make sure it's + * consistent across different DB operations. + */ +export type ProviderId = { + providerName: ProviderName; + providerUserId: string; +} + +export function createProviderId(providerName: ProviderName, providerUserId: string): ProviderId { + return { + providerName, + providerUserId: providerUserId.toLowerCase(), + } +} + +export async function findAuthIdentity(providerId: ProviderId): Promise { + return prisma.authIdentity.findUnique({ + where: { + providerName_providerUserId: providerId, + } + }); +} + +/** + * Updates the provider data for the given auth identity. + * + * This function performs data sanitization and serialization. + * Sanitization is done by hashing the password, so this function + * expects the password received in the `providerDataUpdates` + * **not to be hashed**. + */ +export async function updateAuthIdentityProviderData( + providerId: ProviderId, + existingProviderData: PossibleProviderData[PN], + providerDataUpdates: Partial, +): Promise { + // We are doing the sanitization here only on updates to avoid + // hashing the password multiple times. + const sanitizedProviderDataUpdates = await sanitizeProviderData(providerDataUpdates); + const newProviderData = { + ...existingProviderData, + ...sanitizedProviderDataUpdates, + } + const serializedProviderData = await serializeProviderData(newProviderData); + return prisma.authIdentity.update({ + where: { + providerName_providerUserId: providerId, + }, + data: { providerData: serializedProviderData }, + }); +} + +type FindAuthWithUserResult = Auth & { + user: User +} + +export async function findAuthWithUserBy( + where: Prisma.AuthWhereInput +): Promise { + return prisma.auth.findFirst({ where, include: { user: true }}); +} + +export async function createUser( + providerId: ProviderId, + serializedProviderData?: string, + userFields?: PossibleAdditionalSignupFields, +): Promise { + return prisma.user.create({ + data: { + // Using any here to prevent type errors when userFields are not + // defined. We want Prisma to throw an error in that case. + ...(userFields ?? {} as any), + auth: { + create: { + identities: { + create: { + providerName: providerId.providerName, + providerUserId: providerId.providerUserId, + providerData: serializedProviderData, + }, + }, + } + }, + } + }) +} + +export async function deleteUserByAuthId(authId: string): Promise<{ count: number }> { + return prisma.user.deleteMany({ where: { auth: { + id: authId, + } } }) +} + +export async function createAuthToken( + userId: User['id'] +): Promise { + return sign(userId); +} + +export async function verifyToken(token: string): Promise { + return verify(token); +} + +// If an user exists, we don't want to leak information +// about it. Pretending that we're doing some work +// will make it harder for an attacker to determine +// if a user exists or not. +// NOTE: Attacker measuring time to response can still determine +// if a user exists or not. We'll be able to avoid it when +// we implement e-mail sending via jobs. +export async function doFakeWork(): Promise { + const timeToWork = Math.floor(Math.random() * 1000) + 1000; + return sleep(timeToWork); +} + +export function rethrowPossibleAuthError(e: unknown): void { + if (e instanceof AuthError) { + throwValidationError(e.message); + } + + // Prisma code P2002 is for unique constraint violations. + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') { + throw new HttpError(422, 'Save failed', { + message: `user with the same identity already exists`, + }) + } + + if (e instanceof Prisma.PrismaClientValidationError) { + // NOTE: Logging the error since this usually means that there are + // required fields missing in the request, we want the developer + // to know about it. + console.error(e) + throw new HttpError(422, 'Save failed', { + message: 'there was a database error' + }) + } + + // Prisma code P2021 is for missing table errors. + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2021') { + // NOTE: Logging the error since this usually means that the database + // migrations weren't run, we want the developer to know about it. + console.error(e) + console.info('🐝 This error can happen if you did\'t run the database migrations.') + throw new HttpError(500, 'Save failed', { + message: `there was a database error`, + }) + } + + // Prisma code P2003 is for foreign key constraint failure + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2003') { + console.error(e) + console.info(`🐝 This error can happen if you have some relation on your User entity + but you didn't specify the "onDelete" behaviour to either "Cascade" or "SetNull". + Read more at: https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/referential-actions`) + throw new HttpError(500, 'Save failed', { + message: `there was a database error`, + }) + } + + throw e +} + +export async function validateAndGetAdditionalFields(data: { + [key: string]: unknown +}): Promise> { + const { + password: _password, + ...sanitizedData + } = data; + const result: Record = {}; + for (const [field, getFieldValue] of Object.entries(_waspAdditionalSignupFieldsConfig)) { + try { + const value = await getFieldValue(sanitizedData) + result[field] = value + } catch (e) { + throwValidationError(e.message) + } + } + return result; +} + +export function deserializeAndSanitizeProviderData( + providerData: string, + { shouldRemovePasswordField = false }: { shouldRemovePasswordField?: boolean } = {}, +): PossibleProviderData[PN] { + // NOTE: We are letting JSON.parse throw an error if the providerData is not valid JSON. + let data = JSON.parse(providerData) as PossibleProviderData[PN]; + + if (providerDataHasPasswordField(data) && shouldRemovePasswordField) { + delete data.hashedPassword; + } + + return data; +} + +export async function sanitizeAndSerializeProviderData( + providerData: PossibleProviderData[PN], +): Promise { + return serializeProviderData( + await sanitizeProviderData(providerData) + ); +} + +function serializeProviderData(providerData: PossibleProviderData[PN]): string { + return JSON.stringify(providerData); +} + +async function sanitizeProviderData( + providerData: PossibleProviderData[PN], +): Promise { + const data = { + ...providerData, + }; + if (providerDataHasPasswordField(data)) { + data.hashedPassword = await hashPassword(data.hashedPassword); + } + + return data; +} + + +function providerDataHasPasswordField( + providerData: PossibleProviderData[keyof PossibleProviderData], +): providerData is { hashedPassword: string } { + return 'hashedPassword' in providerData; +} diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/validation.ts b/waspc/data/Generator/templates/sdk/wasp/auth/validation.ts new file mode 100644 index 0000000000..f384a28c87 --- /dev/null +++ b/waspc/data/Generator/templates/sdk/wasp/auth/validation.ts @@ -0,0 +1,77 @@ +import HttpError from '../core/HttpError.js'; + +export const PASSWORD_FIELD = 'password'; +const USERNAME_FIELD = 'username'; +const EMAIL_FIELD = 'email'; +const TOKEN_FIELD = 'token'; + +export function ensureValidEmail(args: unknown): void { + validate(args, [ + { validates: EMAIL_FIELD, message: 'email must be present', validator: email => !!email }, + { validates: EMAIL_FIELD, message: 'email must be a valid email', validator: email => isValidEmail(email) }, + ]); +} + +export function ensureValidUsername(args: unknown): void { + validate(args, [ + { validates: USERNAME_FIELD, message: 'username must be present', validator: username => !!username } + ]); +} + +export function ensurePasswordIsPresent(args: unknown): void { + validate(args, [ + { validates: PASSWORD_FIELD, message: 'password must be present', validator: password => !!password }, + ]); +} + +export function ensureValidPassword(args: unknown): void { + validate(args, [ + { validates: PASSWORD_FIELD, message: 'password must be at least 8 characters', validator: password => isMinLength(password, 8) }, + { validates: PASSWORD_FIELD, message: 'password must contain a number', validator: password => containsNumber(password) }, + ]); +} + +export function ensureTokenIsPresent(args: unknown): void { + validate(args, [ + { validates: TOKEN_FIELD, message: 'token must be present', validator: token => !!token }, + ]); +} + +export function throwValidationError(message: string): void { + throw new HttpError(422, 'Validation failed', { message }) +} + +function validate(args: unknown, validators: { validates: string, message: string, validator: (value: unknown) => boolean }[]): void { + for (const { validates, message, validator } of validators) { + if (!validator(args[validates])) { + throwValidationError(message); + } + } +} + +// NOTE(miho): it would be good to replace our custom validations with e.g. Zod + +const validEmailRegex = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/ +function isValidEmail(input: unknown): boolean { + if (typeof input !== 'string') { + return false + } + + return input.match(validEmailRegex) !== null +} + +function isMinLength(input: unknown, minLength: number): boolean { + if (typeof input !== 'string') { + return false + } + + return input.length >= minLength +} + +function containsNumber(input: unknown): boolean { + if (typeof input !== 'string') { + return false + } + + return /\d/.test(input) +} diff --git a/waspc/data/Generator/templates/sdk/wasp/core/auth.js b/waspc/data/Generator/templates/sdk/wasp/core/auth.js index fad124965a..75e77a7fb9 100644 --- a/waspc/data/Generator/templates/sdk/wasp/core/auth.js +++ b/waspc/data/Generator/templates/sdk/wasp/core/auth.js @@ -4,16 +4,18 @@ import util from 'util' import { randomInt } from 'node:crypto' import prisma from '@server/dbClient.js' -import { handleRejection } from '../utils.js' +import { handleRejection } from '../server/utils' import HttpError from './HttpError.js' import config from '../config.js' +import { deserializeAndSanitizeProviderData } from 'wasp/auth/utils' const jwtSign = util.promisify(jwt.sign) const jwtVerify = util.promisify(jwt.verify) const JWT_SECRET = config.auth.jwtSecret -export const sign = (id, options) => jwtSign({ id }, JWT_SECRET, options) +export const signData = (data, options) => jwtSign(data, JWT_SECRET, options) +export const sign = (id, options) => signData({ id }, options) export const verify = (token) => jwtVerify(token, JWT_SECRET) const auth = handleRejection(async (req, res, next) => { @@ -47,7 +49,17 @@ export async function getUserFromToken(token) { } } - const user = await prisma.user.findUnique({ where: { id: userIdFromToken } }) + const user = await prisma.user + .findUnique({ + where: { id: userIdFromToken }, + include: { + auth: { + include: { + identities: true + } + } + } + }) if (!user) { throwInvalidCredentialsError() } @@ -56,9 +68,12 @@ export async function getUserFromToken(token) { // password field from the object here, we must to do the same there). // Ideally, these two things would live in the same place: // https://github.com/wasp-lang/wasp/issues/965 - const { password, ...userView } = user - - return userView + let sanitizedUser = { ...user } + sanitizedUser.auth.identities = sanitizedUser.auth.identities.map(identity => { + identity.providerData = deserializeAndSanitizeProviderData(identity.providerData, { shouldRemovePasswordField: true }) + return identity + }); + return sanitizedUser } const SP = new SecurePassword() diff --git a/waspc/data/Generator/templates/sdk/wasp/core/auth/prismaMiddleware.js b/waspc/data/Generator/templates/sdk/wasp/core/auth/prismaMiddleware.js deleted file mode 100644 index 53e60c5e1a..0000000000 --- a/waspc/data/Generator/templates/sdk/wasp/core/auth/prismaMiddleware.js +++ /dev/null @@ -1,84 +0,0 @@ -import { hashPassword } from '../auth.js' -import AuthError from '../AuthError.js' - -const USERNAME_FIELD = 'username' -const PASSWORD_FIELD = 'password' - -// Allows flexible validation of a user entity. -// Users can skip default validations by passing _waspSkipDefaultValidations = true -// Users can also add custom validations by passing an array of _waspCustomValidations -// with the same format as our default validations. -// Throws an AuthError on the first validation that fails. -const registerUserEntityValidation = (prismaClient) => { - prismaClient.$use(async (params, next) => { - if (params.model === 'User') { - if (['create', 'update', 'updateMany'].includes(params.action)) { - validateUser(params.args.data, params.args, params.action) - } else if (params.action === 'upsert') { - validateUser(params.args.create, params.args, 'create') - validateUser(params.args.update, params.args, 'update') - } - - // Remove from downstream Prisma processing to avoid "Unknown arg" error - delete params.args._waspSkipDefaultValidations - delete params.args._waspCustomValidations - } - - return next(params) - }) -} - -// Make sure password is always hashed before storing to the database. -const registerPasswordHashing = (prismaClient) => { - prismaClient.$use(async (params, next) => { - if (params.model === 'User') { - if (['create', 'update', 'updateMany'].includes(params.action)) { - if (params.args.data.hasOwnProperty(PASSWORD_FIELD)) { - params.args.data[PASSWORD_FIELD] = await hashPassword(params.args.data[PASSWORD_FIELD]) - } - } else if (params.action === 'upsert') { - if (params.args.create.hasOwnProperty(PASSWORD_FIELD)) { - params.args.create[PASSWORD_FIELD] = - await hashPassword(params.args.create[PASSWORD_FIELD]) - } - if (params.args.update.hasOwnProperty(PASSWORD_FIELD)) { - params.args.update[PASSWORD_FIELD] = - await hashPassword(params.args.update[PASSWORD_FIELD]) - } - } - } - - return next(params) - }) -} - -export const registerAuthMiddleware = (prismaClient) => { - // NOTE: registerUserEntityValidation must come before registerPasswordHashing. - registerUserEntityValidation(prismaClient) - registerPasswordHashing(prismaClient) -} - -const userValidations = [] -userValidations.push({ validates: USERNAME_FIELD, message: 'username must be present', validator: username => !!username }) -userValidations.push({ validates: PASSWORD_FIELD, message: 'password must be present', validator: password => !!password }) -userValidations.push({ validates: PASSWORD_FIELD, message: 'password must be at least 8 characters', validator: password => password.length >= 8 }) -userValidations.push({ validates: PASSWORD_FIELD, message: 'password must contain a number', validator: password => /\d/.test(password) }) - -const validateUser = (user, args, action) => { - user = user || {} - - const validations = [ - ...(args._waspSkipDefaultValidations ? [] : userValidations), - ...(args._waspCustomValidations || []) - ] - - // On 'create' validations run always, otherwise (on updates) - // they run only when the field they are validating is present. - for (const v of validations) { - if (action === 'create' || user.hasOwnProperty(v.validates)) { - if (!v.validator(user[v.validates])) { - throw new AuthError(v.message) - } - } - } -} diff --git a/waspc/data/Generator/templates/sdk/wasp/core/auth/validators.ts b/waspc/data/Generator/templates/sdk/wasp/core/auth/validators.ts deleted file mode 100644 index bfefc5fb78..0000000000 --- a/waspc/data/Generator/templates/sdk/wasp/core/auth/validators.ts +++ /dev/null @@ -1,5 +0,0 @@ -const validEmailRegex = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/ - -export function isValidEmail(input: string): boolean { - return input.match(validEmailRegex) !== null -} diff --git a/waspc/data/Generator/templates/sdk/wasp/core/config.js b/waspc/data/Generator/templates/sdk/wasp/core/config.js index fa4291d5e1..e9234e6f2a 100644 --- a/waspc/data/Generator/templates/sdk/wasp/core/config.js +++ b/waspc/data/Generator/templates/sdk/wasp/core/config.js @@ -1,8 +1,6 @@ import { stripTrailingSlash } from 'wasp/universal/url' -const apiUrl = - stripTrailingSlash(import.meta.env.REACT_APP_API_URL) || - 'http://localhost:3001' +const apiUrl = stripTrailingSlash(import.meta.env.REACT_APP_API_URL) || 'http://localhost:3001'; const config = { apiUrl, diff --git a/waspc/data/Generator/templates/sdk/wasp/core/stitches.config.js b/waspc/data/Generator/templates/sdk/wasp/core/stitches.config.js index 741c205603..c1d600a3f6 100644 --- a/waspc/data/Generator/templates/sdk/wasp/core/stitches.config.js +++ b/waspc/data/Generator/templates/sdk/wasp/core/stitches.config.js @@ -12,6 +12,7 @@ export const { gray500: 'gainsboro', gray400: '#f0f0f0', red: '#FED7D7', + darkRed: '#fa3838', green: '#C6F6D5', brand: '$waspYellow', @@ -23,6 +24,7 @@ export const { submitButtonText: 'black', + formErrorText: '$darkRed', }, fontSizes: { sm: '0.875rem' diff --git a/waspc/data/Generator/templates/sdk/wasp/entities/index.ts b/waspc/data/Generator/templates/sdk/wasp/entities/index.ts index 03a711ea4e..5febac3804 100644 --- a/waspc/data/Generator/templates/sdk/wasp/entities/index.ts +++ b/waspc/data/Generator/templates/sdk/wasp/entities/index.ts @@ -6,6 +6,8 @@ import { export { type User, type Task, + type Auth, + type AuthIdentity, } from "@prisma/client" export type Entity = diff --git a/waspc/data/Generator/templates/sdk/wasp/operations/index.ts b/waspc/data/Generator/templates/sdk/wasp/operations/index.ts index b9f0e2be4a..31e70ae98b 100644 --- a/waspc/data/Generator/templates/sdk/wasp/operations/index.ts +++ b/waspc/data/Generator/templates/sdk/wasp/operations/index.ts @@ -5,12 +5,9 @@ import { deserialize as superjsonDeserialize, } from 'superjson' -export type OperationRoute = { method: HttpMethod; path: string } +export type OperationRoute = { method: HttpMethod, path: string } -export async function callOperation( - operationRoute: OperationRoute & { method: HttpMethod.Post }, - args: any -) { +export async function callOperation(operationRoute: OperationRoute & { method: HttpMethod.Post }, args: any) { try { const superjsonArgs = superjsonSerialize(args) const response = await api.post(operationRoute.path, superjsonArgs) @@ -20,8 +17,6 @@ export async function callOperation( } } -export function makeOperationRoute( - relativeOperationRoute: string -): OperationRoute { +export function makeOperationRoute(relativeOperationRoute: string): OperationRoute { return { method: HttpMethod.Post, path: `/${relativeOperationRoute}` } } diff --git a/waspc/data/Generator/templates/sdk/wasp/operations/resources.js b/waspc/data/Generator/templates/sdk/wasp/operations/resources.js index d7efa3fc1d..5261654600 100644 --- a/waspc/data/Generator/templates/sdk/wasp/operations/resources.js +++ b/waspc/data/Generator/templates/sdk/wasp/operations/resources.js @@ -26,15 +26,13 @@ export function addResourcesUsedByQuery(queryCacheKey, resources) { } export function registerActionInProgress(optimisticUpdateTuples) { - optimisticUpdateTuples.forEach(({ queryKey, updateQuery }) => - updateHandlers.add(queryKey, updateQuery) + optimisticUpdateTuples.forEach( + ({ queryKey, updateQuery }) => updateHandlers.add(queryKey, updateQuery) ) } export async function registerActionDone(resources, optimisticUpdateTuples) { - optimisticUpdateTuples.forEach(({ queryKey }) => - updateHandlers.remove(queryKey) - ) + optimisticUpdateTuples.forEach(({ queryKey }) => updateHandlers.remove(queryKey)) await invalidateQueriesUsing(resources) } @@ -43,7 +41,6 @@ export function getActiveOptimisticUpdates(queryKey) { } export async function invalidateAndRemoveQueries() { - console.log(queryClientInitialized) const queryClient = await queryClientInitialized // If we don't reset the queries before removing them, Wasp will stay on // the same page. The user would have to manually refresh the page to "finish" @@ -63,12 +60,11 @@ export async function invalidateAndRemoveQueries() { * @param {string[]} resources - Names of resources. */ async function invalidateQueriesUsing(resources) { - console.log(queryClientInitialized) const queryClient = await queryClientInitialized const queryCacheKeysToInvalidate = getQueriesUsingResources(resources) - queryCacheKeysToInvalidate.forEach((queryCacheKey) => - queryClient.invalidateQueries(queryCacheKey) + queryCacheKeysToInvalidate.forEach( + queryCacheKey => queryClient.invalidateQueries(queryCacheKey) ) } diff --git a/waspc/data/Generator/templates/sdk/wasp/rpc/actions/core.js b/waspc/data/Generator/templates/sdk/wasp/rpc/actions/core.js index 62c65ff4a3..cd1c60ecef 100644 --- a/waspc/data/Generator/templates/sdk/wasp/rpc/actions/core.js +++ b/waspc/data/Generator/templates/sdk/wasp/rpc/actions/core.js @@ -4,7 +4,7 @@ import { registerActionDone, } from 'wasp/operations/resources' -// todo - turn helpers and core into the same thing +// todo(filip) - turn helpers and core into the same thing export function createAction(relativeActionRoute, entitiesUsed) { const actionRoute = makeOperationRoute(relativeActionRoute) @@ -17,10 +17,7 @@ export function createAction(relativeActionRoute, entitiesUsed) { // registering the action as done. return await callOperation(actionRoute, args) } finally { - await registerActionDone( - entitiesUsed, - specificOptimisticUpdateDefinitions - ) + await registerActionDone(entitiesUsed, specificOptimisticUpdateDefinitions) } } diff --git a/waspc/data/Generator/templates/sdk/wasp/rpc/queries/core.js b/waspc/data/Generator/templates/sdk/wasp/rpc/queries/core.js index 68cc338a31..616fb82958 100644 --- a/waspc/data/Generator/templates/sdk/wasp/rpc/queries/core.js +++ b/waspc/data/Generator/templates/sdk/wasp/rpc/queries/core.js @@ -11,7 +11,7 @@ export function createQuery(relativeQueryPath, entitiesUsed) { const serverResult = await callOperation(queryRoute, queryArgs) return getActiveOptimisticUpdates(queryKey).reduce( (result, update) => update(result), - serverResult + serverResult, ) } diff --git a/waspc/data/Generator/templates/sdk/wasp/server/_types/index.ts b/waspc/data/Generator/templates/sdk/wasp/server/_types/index.ts index e6307559c9..ad1de55e09 100644 --- a/waspc/data/Generator/templates/sdk/wasp/server/_types/index.ts +++ b/waspc/data/Generator/templates/sdk/wasp/server/_types/index.ts @@ -1,48 +1,39 @@ +import { type Expand } from 'wasp/universal/types'; import { type Request, type Response } from 'express' +import { type ParamsDictionary as ExpressParams, type Query as ExpressQuery } from 'express-serve-static-core' +import prisma from "wasp/server/dbClient" import { - type ParamsDictionary as ExpressParams, - type Query as ExpressQuery, -} from 'express-serve-static-core' -import { type Expand } from 'wasp/universal/types.js' -import prisma from 'wasp/server/dbClient.js' -import { type User } from 'wasp/entities' -import { type _Entity } from './taggedEntities' -import { type Payload } from './serialization' - -export * from './taggedEntities' -export * from './serialization' - -export type Query< - Entities extends _Entity[], - Input extends Payload, - Output extends Payload -> = Operation + type User, + type Auth, + type AuthIdentity, +} from "wasp/entities" +import { + type EmailProviderData, + type UsernameProviderData, + type OAuthProviderData, + // todo(filip): marker +} from 'wasp/auth/utils' +import { type _Entity } from "./taggedEntities" +import { type Payload } from "./serialization"; -export type Action< - Entities extends _Entity[], - Input extends Payload, - Output extends Payload -> = Operation +export * from "./taggedEntities" +export * from "./serialization" -export type AuthenticatedQuery< - Entities extends _Entity[], - Input extends Payload, - Output extends Payload -> = AuthenticatedOperation +export type Query = + Operation -export type AuthenticatedAction< - Entities extends _Entity[], - Input extends Payload, - Output extends Payload -> = AuthenticatedOperation +export type Action = + Operation -type AuthenticatedOperation< - Entities extends _Entity[], - Input extends Payload, - Output extends Payload -> = ( +export type AuthenticatedQuery = + AuthenticatedOperation + +export type AuthenticatedAction = + AuthenticatedOperation + +type AuthenticatedOperation = ( args: Input, - context: ContextWithUser + context: ContextWithUser, ) => Output | Promise export type AuthenticatedApi< @@ -55,12 +46,12 @@ export type AuthenticatedApi< > = ( req: Request, res: Response, - context: ContextWithUser + context: ContextWithUser, ) => void type Operation = ( args: Input, - context: Context + context: Context, ) => Output | Promise export type Api< @@ -73,28 +64,38 @@ export type Api< > = ( req: Request, res: Response, - context: Context + context: Context, ) => void type EntityMap = { - [EntityName in Entities[number]['_entityName']]: PrismaDelegate[EntityName] + [EntityName in Entities[number]["_entityName"]]: PrismaDelegate[EntityName] } -type PrismaDelegate = { - User: typeof prisma.user - Task: typeof prisma.task +export type PrismaDelegate = { + "User": typeof prisma.user, + "Task": typeof prisma.task, } type Context = Expand<{ entities: Expand> }> -type ContextWithUser = Expand< - Context & { user?: SanitizedUser } -> +type ContextWithUser = Expand & { user?: SanitizedUser }> // TODO: This type must match the logic in core/auth.js (if we remove the // password field from the object there, we must do the same here). Ideally, // these two things would live in the same place: // https://github.com/wasp-lang/wasp/issues/965 -export type SanitizedUser = Omit + +export type DeserializedAuthEntity = Expand & { + providerData: Omit | Omit | OAuthProviderData +}> + +export type SanitizedUser = User & { + auth: Auth & { + identities: DeserializedAuthEntity[] + } | null +} + +// todo(filip): marker +export type { ProviderName } from 'wasp/auth/utils' diff --git a/waspc/data/Generator/templates/sdk/wasp/server/_types/serialization.ts b/waspc/data/Generator/templates/sdk/wasp/server/_types/serialization.ts index 5a55cadb2b..595b5ba69f 100644 --- a/waspc/data/Generator/templates/sdk/wasp/server/_types/serialization.ts +++ b/waspc/data/Generator/templates/sdk/wasp/server/_types/serialization.ts @@ -1,35 +1,43 @@ -export type Payload = void | SuperJSONValue; +export type Payload = void | SuperJSONValue // The part below was copied from SuperJSON and slightly modified: // https://github.com/blitz-js/superjson/blob/ae7dbcefe5d3ece5b04be0c6afe6b40f3a44a22a/src/types.ts // // We couldn't use SuperJSON's types directly because: // 1. They aren't exported publicly. -// 2. They have a werid quirk that turns `SuperJSONValue` into `any`. -// See why here: +// 2. They have a werid quirk that turns `SuperJSONValue` into `any`. +// See why here: // https://github.com/blitz-js/superjson/pull/36#issuecomment-669239876 // // We changed the code as little as possible to make future comparisons easier. +export type JSONValue = PrimitiveJSONValue | JSONArray | JSONObject -type PrimitiveJSONValue = string | number | boolean | undefined | null; - -type JSONValue = PrimitiveJSONValue | JSONArray | JSONObject; - -interface JSONArray extends Array { +export interface JSONObject { + [key: string]: JSONValue } -interface JSONObject { - [key: string]: JSONValue; -} +type PrimitiveJSONValue = string | number | boolean | undefined | null -type SerializableJSONValue = Symbol | Set | Map | undefined | bigint | Date | RegExp; +interface JSONArray extends Array {} -// Here's where we excluded `ClassInstance` (which was `any`) from the union. -type SuperJSONValue = JSONValue | SerializableJSONValue | SuperJSONArray | SuperJSONObject; +type SerializableJSONValue = + | Symbol + | Set + | Map + | undefined + | bigint + | Date + | RegExp -interface SuperJSONArray extends Array { -} +// Here's where we excluded `ClassInstance` (which was `any`) from the union. +type SuperJSONValue = + | JSONValue + | SerializableJSONValue + | SuperJSONArray + | SuperJSONObject + +interface SuperJSONArray extends Array {} interface SuperJSONObject { - [key: string]: SuperJSONValue; + [key: string]: SuperJSONValue } diff --git a/waspc/data/Generator/templates/sdk/wasp/server/_types/taggedEntities.ts b/waspc/data/Generator/templates/sdk/wasp/server/_types/taggedEntities.ts index fd0c82b272..3331b3f893 100644 --- a/waspc/data/Generator/templates/sdk/wasp/server/_types/taggedEntities.ts +++ b/waspc/data/Generator/templates/sdk/wasp/server/_types/taggedEntities.ts @@ -3,18 +3,20 @@ // // We must explicitly tag all entities with their name to avoid issues with // structural typing. See https://github.com/wasp-lang/wasp/pull/982 for details. -import { - type Entity, +import { + type Entity, type EntityName, type User, type Task, -} from "../../entities"; +} from '../../entities' -export type _User = WithName; -export type _Task = WithName; +export type _User = WithName +export type _Task = WithName -export type _Entity = _User | _Task | never; +export type _Entity = + | _User + | _Task + | never -type WithName = E & { - _entityName: Name; -}; +type WithName = + E & { _entityName: Name } diff --git a/waspc/data/Generator/templates/sdk/wasp/server/core/AuthError.js b/waspc/data/Generator/templates/sdk/wasp/server/core/AuthError.js deleted file mode 100644 index 2d965c168e..0000000000 --- a/waspc/data/Generator/templates/sdk/wasp/server/core/AuthError.js +++ /dev/null @@ -1,17 +0,0 @@ -class AuthError extends Error { - constructor (message, data, ...params) { - super(message, ...params) - - if (Error.captureStackTrace) { - Error.captureStackTrace(this, AuthError) - } - - this.name = this.constructor.name - - if (data) { - this.data = data - } - } -} - -export default AuthError diff --git a/waspc/data/Generator/templates/sdk/wasp/server/core/HttpError.js b/waspc/data/Generator/templates/sdk/wasp/server/core/HttpError.js deleted file mode 100644 index 8a2cb04db5..0000000000 --- a/waspc/data/Generator/templates/sdk/wasp/server/core/HttpError.js +++ /dev/null @@ -1,22 +0,0 @@ -class HttpError extends Error { - constructor (statusCode, message, data, ...params) { - super(message, ...params) - - if (Error.captureStackTrace) { - Error.captureStackTrace(this, HttpError) - } - - this.name = this.constructor.name - - if (!(Number.isInteger(statusCode) && statusCode >= 400 && statusCode < 600)) { - throw new Error('statusCode has to be integer in range [400, 600).') - } - this.statusCode = statusCode - - if (data) { - this.data = data - } - } -} - -export default HttpError diff --git a/waspc/data/Generator/templates/sdk/wasp/server/core/auth.js b/waspc/data/Generator/templates/sdk/wasp/server/core/auth.js deleted file mode 100644 index 11d331e48c..0000000000 --- a/waspc/data/Generator/templates/sdk/wasp/server/core/auth.js +++ /dev/null @@ -1,185 +0,0 @@ -import jwt from 'jsonwebtoken' -import SecurePassword from 'secure-password' -import util from 'util' -import { randomInt } from 'node:crypto' - -import prisma from 'wasp/server/dbClient.js' -import { handleRejection } from '../utils.js' -import HttpError from 'wasp/core/HttpError.js' -import config from '../config.js' - -const jwtSign = util.promisify(jwt.sign) -const jwtVerify = util.promisify(jwt.verify) - -const JWT_SECRET = config.auth.jwtSecret - -export const sign = (id, options) => jwtSign({ id }, JWT_SECRET, options) -export const verify = (token) => jwtVerify(token, JWT_SECRET) - -const auth = handleRejection(async (req, res, next) => { - const authHeader = req.get('Authorization') - if (!authHeader) { - // NOTE(matija): for now we let tokenless requests through and make it operation's - // responsibility to verify whether the request is authenticated or not. In the future - // we will develop our own system at Wasp-level for that. - return next() - } - - if (authHeader.startsWith('Bearer ')) { - const token = authHeader.substring(7, authHeader.length) - req.user = await getUserFromToken(token) - } else { - throwInvalidCredentialsError() - } - - next() -}) - -export async function getUserFromToken(token) { - let userIdFromToken - try { - userIdFromToken = (await verify(token)).id - } catch (error) { - if ( - ['TokenExpiredError', 'JsonWebTokenError', 'NotBeforeError'].includes( - error.name - ) - ) { - throwInvalidCredentialsError() - } else { - throw error - } - } - - const user = await prisma.user.findUnique({ where: { id: userIdFromToken } }) - if (!user) { - throwInvalidCredentialsError() - } - - // TODO: This logic must match the type in types/index.ts (if we remove the - // password field from the object here, we must to do the same there). - // Ideally, these two things would live in the same place: - // https://github.com/wasp-lang/wasp/issues/965 - const { password, ...userView } = user - - return userView -} - -const SP = new SecurePassword() - -export const hashPassword = async (password) => { - const hashedPwdBuffer = await SP.hash(Buffer.from(password)) - return hashedPwdBuffer.toString('base64') -} - -export const verifyPassword = async (hashedPassword, password) => { - const result = await SP.verify( - Buffer.from(password), - Buffer.from(hashedPassword, 'base64') - ) - if (result !== SecurePassword.VALID) { - throw new Error('Invalid password.') - } -} - -// Generates an unused username that looks similar to "quick-purple-sheep-91231". -// It generates several options and ensures it picks one that is not currently in use. -export function generateAvailableDictionaryUsername() { - const adjectives = [ - 'fuzzy', - 'tall', - 'short', - 'nice', - 'happy', - 'quick', - 'slow', - 'good', - 'new', - 'old', - 'first', - 'last', - 'old', - 'young', - ] - const colors = [ - 'red', - 'green', - 'blue', - 'white', - 'black', - 'brown', - 'purple', - 'orange', - 'yellow', - ] - const nouns = [ - 'wasp', - 'cat', - 'dog', - 'lion', - 'rabbit', - 'duck', - 'pig', - 'bee', - 'goat', - 'crab', - 'fish', - 'chicken', - 'horse', - 'llama', - 'camel', - 'sheep', - ] - - const potentialUsernames = [] - for (let i = 0; i < 10; i++) { - const potentialUsername = `${adjectives[randomInt(adjectives.length)]}-${ - colors[randomInt(colors.length)] - }-${nouns[randomInt(nouns.length)]}-${randomInt(100_000)}` - potentialUsernames.push(potentialUsername) - } - - return findAvailableUsername(potentialUsernames) -} - -// Generates an unused username based on an array of username segments and a separator. -// It generates several options and ensures it picks one that is not currently in use. -export function generateAvailableUsername(usernameSegments, config) { - const separator = config?.separator || '-' - const baseUsername = usernameSegments.join(separator) - - const potentialUsernames = [] - for (let i = 0; i < 10; i++) { - const potentialUsername = `${baseUsername}${separator}${randomInt(100_000)}` - potentialUsernames.push(potentialUsername) - } - - return findAvailableUsername(potentialUsernames) -} - -// Checks the database for an unused username from an array provided and returns first. -async function findAvailableUsername(potentialUsernames) { - const users = await prisma.user.findMany({ - where: { - username: { in: potentialUsernames }, - }, - }) - const takenUsernames = users.map((user) => user.username) - const availableUsernames = potentialUsernames.filter( - (username) => !takenUsernames.includes(username) - ) - - if (availableUsernames.length === 0) { - throw new Error( - 'Unable to generate a unique username. Please contact Wasp.' - ) - } - - return availableUsernames[0] -} - -export function throwInvalidCredentialsError(message) { - throw new HttpError(401, 'Invalid credentials', { message }) -} - -export default auth diff --git a/waspc/data/Generator/templates/sdk/wasp/server/core/auth/prismaMiddleware.js b/waspc/data/Generator/templates/sdk/wasp/server/core/auth/prismaMiddleware.js deleted file mode 100644 index 53e60c5e1a..0000000000 --- a/waspc/data/Generator/templates/sdk/wasp/server/core/auth/prismaMiddleware.js +++ /dev/null @@ -1,84 +0,0 @@ -import { hashPassword } from '../auth.js' -import AuthError from '../AuthError.js' - -const USERNAME_FIELD = 'username' -const PASSWORD_FIELD = 'password' - -// Allows flexible validation of a user entity. -// Users can skip default validations by passing _waspSkipDefaultValidations = true -// Users can also add custom validations by passing an array of _waspCustomValidations -// with the same format as our default validations. -// Throws an AuthError on the first validation that fails. -const registerUserEntityValidation = (prismaClient) => { - prismaClient.$use(async (params, next) => { - if (params.model === 'User') { - if (['create', 'update', 'updateMany'].includes(params.action)) { - validateUser(params.args.data, params.args, params.action) - } else if (params.action === 'upsert') { - validateUser(params.args.create, params.args, 'create') - validateUser(params.args.update, params.args, 'update') - } - - // Remove from downstream Prisma processing to avoid "Unknown arg" error - delete params.args._waspSkipDefaultValidations - delete params.args._waspCustomValidations - } - - return next(params) - }) -} - -// Make sure password is always hashed before storing to the database. -const registerPasswordHashing = (prismaClient) => { - prismaClient.$use(async (params, next) => { - if (params.model === 'User') { - if (['create', 'update', 'updateMany'].includes(params.action)) { - if (params.args.data.hasOwnProperty(PASSWORD_FIELD)) { - params.args.data[PASSWORD_FIELD] = await hashPassword(params.args.data[PASSWORD_FIELD]) - } - } else if (params.action === 'upsert') { - if (params.args.create.hasOwnProperty(PASSWORD_FIELD)) { - params.args.create[PASSWORD_FIELD] = - await hashPassword(params.args.create[PASSWORD_FIELD]) - } - if (params.args.update.hasOwnProperty(PASSWORD_FIELD)) { - params.args.update[PASSWORD_FIELD] = - await hashPassword(params.args.update[PASSWORD_FIELD]) - } - } - } - - return next(params) - }) -} - -export const registerAuthMiddleware = (prismaClient) => { - // NOTE: registerUserEntityValidation must come before registerPasswordHashing. - registerUserEntityValidation(prismaClient) - registerPasswordHashing(prismaClient) -} - -const userValidations = [] -userValidations.push({ validates: USERNAME_FIELD, message: 'username must be present', validator: username => !!username }) -userValidations.push({ validates: PASSWORD_FIELD, message: 'password must be present', validator: password => !!password }) -userValidations.push({ validates: PASSWORD_FIELD, message: 'password must be at least 8 characters', validator: password => password.length >= 8 }) -userValidations.push({ validates: PASSWORD_FIELD, message: 'password must contain a number', validator: password => /\d/.test(password) }) - -const validateUser = (user, args, action) => { - user = user || {} - - const validations = [ - ...(args._waspSkipDefaultValidations ? [] : userValidations), - ...(args._waspCustomValidations || []) - ] - - // On 'create' validations run always, otherwise (on updates) - // they run only when the field they are validating is present. - for (const v of validations) { - if (action === 'create' || user.hasOwnProperty(v.validates)) { - if (!v.validator(user[v.validates])) { - throw new AuthError(v.message) - } - } - } -} diff --git a/waspc/data/Generator/templates/sdk/wasp/server/core/auth/validators.ts b/waspc/data/Generator/templates/sdk/wasp/server/core/auth/validators.ts deleted file mode 100644 index bfefc5fb78..0000000000 --- a/waspc/data/Generator/templates/sdk/wasp/server/core/auth/validators.ts +++ /dev/null @@ -1,5 +0,0 @@ -const validEmailRegex = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/ - -export function isValidEmail(input: string): boolean { - return input.match(validEmailRegex) !== null -} diff --git a/waspc/data/Generator/templates/sdk/wasp/server/dbClient.ts b/waspc/data/Generator/templates/sdk/wasp/server/dbClient.ts index c0720735ed..66e7801be3 100644 --- a/waspc/data/Generator/templates/sdk/wasp/server/dbClient.ts +++ b/waspc/data/Generator/templates/sdk/wasp/server/dbClient.ts @@ -1,12 +1,9 @@ import Prisma from '@prisma/client' -import { registerAuthMiddleware } from './core/auth/prismaMiddleware.js' const createDbClient = () => { const prismaClient = new Prisma.PrismaClient() - registerAuthMiddleware(prismaClient) - return prismaClient } diff --git a/waspc/data/Generator/templates/sdk/wasp/server/utils.ts b/waspc/data/Generator/templates/sdk/wasp/server/utils.ts new file mode 100644 index 0000000000..a930149d08 --- /dev/null +++ b/waspc/data/Generator/templates/sdk/wasp/server/utils.ts @@ -0,0 +1,66 @@ +import crypto from 'crypto' +import { Request, Response, NextFunction } from 'express' + +import { readdir } from 'fs' +import { dirname } from 'path' +import { fileURLToPath } from 'url' + +import { type SanitizedUser } from './_types/index.js' + +type RequestWithExtraFields = Request & { + user?: SanitizedUser +} + +/** + * Decorator for async express middleware that handles promise rejections. + * @param {Func} middleware - Express middleware function. + * @returns Express middleware that is exactly the same as the given middleware but, + * if given middleware returns promise, reject of that promise will be correctly handled, + * meaning that error will be forwarded to next(). + */ +export const handleRejection = ( + middleware: ( + req: RequestWithExtraFields, + res: Response, + next: NextFunction + ) => any +) => +async (req: RequestWithExtraFields, res: Response, next: NextFunction) => { + try { + await middleware(req, res, next) + } catch (error) { + next(error) + } +} + +export const sleep = (ms: number): Promise => new Promise((r) => setTimeout(r, ms)) + +export function getDirPathFromFileUrl(fileUrl: string): string { + return fileURLToPath(dirname(fileUrl)) +} + +export async function importJsFilesFromDir( + pathToDir: string, + whitelistedFileNames: string[] | null = null +): Promise { + return new Promise((resolve, reject) => { + readdir(pathToDir, async (err, files) => { + if (err) { + return reject(err) + } + const importPromises = files + .filter((file) => file.endsWith('.js') && isWhitelistedFileName(file)) + .map((file) => import(`${pathToDir}/${file}`)) + resolve(Promise.all(importPromises)) + }) + }) + + function isWhitelistedFileName(fileName: string) { + // No whitelist means all files are whitelisted + if (!Array.isArray(whitelistedFileNames)) { + return true + } + + return whitelistedFileNames.some((whitelistedFileName) => fileName === whitelistedFileName) + } +} diff --git a/waspc/examples/todo-typescript/main.wasp b/waspc/examples/todo-typescript/main.wasp index 3b21ff7b22..a2dd70db1c 100644 --- a/waspc/examples/todo-typescript/main.wasp +++ b/waspc/examples/todo-typescript/main.wasp @@ -21,8 +21,6 @@ app TodoTypescript { // Then run `wasp db studio` to open Prisma Studio and view your db models entity User {=psl id Int @id @default(autoincrement()) - username String @unique - password String tasks Task[] psl=} diff --git a/waspc/examples/todo-typescript/src/MainPage.tsx b/waspc/examples/todo-typescript/src/MainPage.tsx index b74c3e2a42..3d43e28deb 100644 --- a/waspc/examples/todo-typescript/src/MainPage.tsx +++ b/waspc/examples/todo-typescript/src/MainPage.tsx @@ -5,7 +5,9 @@ import { useQuery, useAction } from 'wasp/rpc' // Wasp uses a thin wrapper aroun import { getTasks } from 'wasp/rpc/queries' import { createTask, updateTask, deleteTasks } from 'wasp/rpc/actions' import waspLogo from './waspLogo.png' -import type { Task, User } from 'wasp/entities' +import type { Task } from 'wasp/entities' +import type { User } from 'wasp/auth/types' +import { getUsername } from 'wasp/auth/user' export const MainPage = ({ user }: { user: User }) => { const { data: tasks, isLoading, error } = useQuery(getTasks) @@ -20,7 +22,7 @@ export const MainPage = ({ user }: { user: User }) => { wasp logo {user && (

    - {user.username} + {getUsername(user)} {`'s tasks :)`}

    )} diff --git a/waspc/src/Wasp/Generator/SdkGenerator.hs b/waspc/src/Wasp/Generator/SdkGenerator.hs index 0162a4dc7b..f092acc0a1 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator.hs @@ -57,6 +57,7 @@ genPackageJson spec = ("mitt", "3.0.0"), ("react", "^18.2.0"), ("react-router-dom", "^5.3.3"), + ("react-hook-form", "^7.45.4"), ("secure-password", "^4.0.0"), ("superjson", "^1.12.2"), ("@types/express-serve-static-core", "^4.17.13") From 1999b215ea7af48fb101d54e3515d638e43315bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Wed, 10 Jan 2024 12:48:05 +0100 Subject: [PATCH 32/54] Add fix and cleanstart scripts --- waspc/examples/todo-typescript/cleanstart | 1 + waspc/examples/todo-typescript/fix | 1 + 2 files changed, 2 insertions(+) create mode 100755 waspc/examples/todo-typescript/cleanstart create mode 100755 waspc/examples/todo-typescript/fix diff --git a/waspc/examples/todo-typescript/cleanstart b/waspc/examples/todo-typescript/cleanstart new file mode 100755 index 0000000000..3c7987deb5 --- /dev/null +++ b/waspc/examples/todo-typescript/cleanstart @@ -0,0 +1 @@ +rm -r .wasp node_modules package-lock.json migrations; cabal run wasp-cli db migrate-dev -- --name init && ./fix; cabal run wasp-cli start diff --git a/waspc/examples/todo-typescript/fix b/waspc/examples/todo-typescript/fix new file mode 100755 index 0000000000..4943774959 --- /dev/null +++ b/waspc/examples/todo-typescript/fix @@ -0,0 +1 @@ +rm -r .wasp/out/web-app/node_modules/{react,.vite} From 1a17a72cb7e556176b3da7e1781a3f3244b618f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Wed, 10 Jan 2024 17:06:53 +0100 Subject: [PATCH 33/54] Add generated sdk code to Git for easier diffing --- .../.wasp/out/sdk/wasp/api/events.ts | 11 + .../.wasp/out/sdk/wasp/api/index.ts | 103 ++++++ .../.wasp/out/sdk/wasp/auth/forms/Auth.tsx | 85 +++++ .../.wasp/out/sdk/wasp/auth/forms/Login.tsx | 17 + .../.wasp/out/sdk/wasp/auth/forms/Signup.tsx | 23 ++ .../out/sdk/wasp/auth/forms/internal/Form.tsx | 95 +++++ .../sdk/wasp/auth/forms/internal/Message.tsx | 18 + .../forms/internal/common/LoginSignupForm.tsx | 178 +++++++++ .../useUsernameAndPassword.ts | 29 ++ .../.wasp/out/sdk/wasp/auth/forms/types.ts | 39 ++ .../.wasp/out/sdk/wasp/auth/helpers/user.ts | 14 + .../.wasp/out/sdk/wasp/auth/login.ts | 13 + .../.wasp/out/sdk/wasp/auth/logout.ts | 9 + .../auth/pages/createAuthRequiredPage.jsx | 30 ++ .../out/sdk/wasp/auth/providers/types.ts | 38 ++ .../.wasp/out/sdk/wasp/auth/signup.ts | 9 + .../.wasp/out/sdk/wasp/auth/types.ts | 2 + .../.wasp/out/sdk/wasp/auth/useAuth.ts | 38 ++ .../.wasp/out/sdk/wasp/auth/user.ts | 27 ++ .../.wasp/out/sdk/wasp/auth/utils.ts | 290 +++++++++++++++ .../.wasp/out/sdk/wasp/auth/validation.ts | 77 ++++ .../.wasp/out/sdk/wasp/core/AuthError.js | 17 + .../.wasp/out/sdk/wasp/core/HttpError.js | 22 ++ .../.wasp/out/sdk/wasp/core/auth.js | 145 ++++++++ .../.wasp/out/sdk/wasp/core/config.js | 9 + .../out/sdk/wasp/core/stitches.config.js | 33 ++ .../.wasp/out/sdk/wasp/core/storage.ts | 50 +++ .../.wasp/out/sdk/wasp/entities/index.ts | 21 ++ .../.wasp/out/sdk/wasp/ext-src/actions.ts | 56 +++ .../.wasp/out/sdk/wasp/ext-src/queries.ts | 15 + .../.wasp/out/sdk/wasp/operations/index.ts | 22 ++ .../out/sdk/wasp/operations/resources.js | 81 +++++ .../sdk/wasp/operations/updateHandlersMap.js | 37 ++ .../.wasp/out/sdk/wasp/package.json | 49 +++ .../.wasp/out/sdk/wasp/rpc/actions/core.d.ts | 13 + .../.wasp/out/sdk/wasp/rpc/actions/core.js | 37 ++ .../.wasp/out/sdk/wasp/rpc/actions/index.ts | 14 + .../.wasp/out/sdk/wasp/rpc/index.ts | 338 ++++++++++++++++++ .../.wasp/out/sdk/wasp/rpc/queries/core.d.ts | 23 ++ .../.wasp/out/sdk/wasp/rpc/queries/core.js | 30 ++ .../.wasp/out/sdk/wasp/rpc/queries/index.ts | 6 + .../.wasp/out/sdk/wasp/rpc/queryClient.ts | 33 ++ .../.wasp/out/sdk/wasp/server/_types/index.ts | 101 ++++++ .../sdk/wasp/server/_types/serialization.ts | 43 +++ .../sdk/wasp/server/_types/taggedEntities.ts | 22 ++ .../out/sdk/wasp/server/actions/index.ts | 39 ++ .../out/sdk/wasp/server/actions/types.ts | 34 ++ .../.wasp/out/sdk/wasp/server/dbClient.ts | 12 + .../out/sdk/wasp/server/queries/index.ts | 13 + .../out/sdk/wasp/server/queries/types.ts | 6 + .../.wasp/out/sdk/wasp/server/utils.ts | 66 ++++ .../.wasp/out/sdk/wasp/types/index.ts | 9 + .../.wasp/out/sdk/wasp/universal/types.ts | 31 ++ .../.wasp/out/sdk/wasp/universal/url.ts | 3 + 54 files changed, 2575 insertions(+) create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/events.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/index.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/Auth.tsx create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/Login.tsx create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/Signup.tsx create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/internal/Form.tsx create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/internal/Message.tsx create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/internal/common/LoginSignupForm.tsx create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/types.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/helpers/user.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/login.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/logout.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/pages/createAuthRequiredPage.jsx create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/providers/types.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/signup.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/types.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/useAuth.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/user.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/utils.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/validation.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/AuthError.js create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/HttpError.js create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/auth.js create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/config.js create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/stitches.config.js create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/storage.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/entities/index.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/ext-src/actions.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/ext-src/queries.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/operations/index.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/operations/resources.js create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/operations/updateHandlersMap.js create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/actions/core.d.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/actions/core.js create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/actions/index.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/index.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/queries/core.d.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/queries/core.js create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/queries/index.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/queryClient.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/index.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/serialization.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/taggedEntities.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/actions/index.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/actions/types.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/dbClient.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/queries/index.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/queries/types.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/utils.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/types/index.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/universal/types.ts create mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/universal/url.ts diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/events.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/events.ts new file mode 100644 index 0000000000..9a59b366d3 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/events.ts @@ -0,0 +1,11 @@ +import mitt, { Emitter } from 'mitt'; + +type ApiEvents = { + // key: Event name + // type: Event payload type + 'authToken.set': void; + 'authToken.clear': void; +}; + +// Used to allow API clients to register for auth token change events. +export const apiEventsEmitter: Emitter = mitt(); diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/index.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/index.ts new file mode 100644 index 0000000000..9aad1ead59 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/index.ts @@ -0,0 +1,103 @@ +import axios, { type AxiosError } from 'axios' + +import config from 'wasp/core/config' +import { storage } from 'wasp/core/storage' +import { apiEventsEmitter } from 'wasp/api/events' + +const api = axios.create({ + baseURL: config.apiUrl, +}) + +const WASP_APP_AUTH_TOKEN_NAME = 'authToken' + +let authToken = storage.get(WASP_APP_AUTH_TOKEN_NAME) as string | undefined + +export function setAuthToken(token: string): void { + authToken = token + storage.set(WASP_APP_AUTH_TOKEN_NAME, token) + apiEventsEmitter.emit('authToken.set') +} + +export function getAuthToken(): string | undefined { + return authToken +} + +export function clearAuthToken(): void { + authToken = undefined + storage.remove(WASP_APP_AUTH_TOKEN_NAME) + apiEventsEmitter.emit('authToken.clear') +} + +export function removeLocalUserData(): void { + authToken = undefined + storage.clear() + apiEventsEmitter.emit('authToken.clear') +} + +api.interceptors.request.use((request) => { + if (authToken) { + request.headers['Authorization'] = `Bearer ${authToken}` + } + return request +}) + +api.interceptors.response.use(undefined, (error) => { + if (error.response?.status === 401) { + clearAuthToken() + } + return Promise.reject(error) +}) + +// This handler will run on other tabs (not the active one calling API functions), +// and will ensure they know about auth token changes. +// Ref: https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event +// "Note: This won't work on the same page that is making the changes — it is really a way +// for other pages on the domain using the storage to sync any changes that are made." +window.addEventListener('storage', (event) => { + if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_TOKEN_NAME)) { + if (!!event.newValue) { + authToken = event.newValue + apiEventsEmitter.emit('authToken.set') + } else { + authToken = undefined + apiEventsEmitter.emit('authToken.clear') + } + } +}) + +/** + * Takes an error returned by the app's API (as returned by axios), and transforms into a more + * standard format to be further used by the client. It is also assumed that given API + * error has been formatted as implemented by HttpError on the server. + */ +export function handleApiError(error: AxiosError<{ message?: string, data?: unknown }>): void { + if (error?.response) { + // If error came from HTTP response, we capture most informative message + // and also add .statusCode information to it. + // If error had JSON response, we assume it is of format { message, data } and + // add that info to the error. + // TODO: We might want to use HttpError here instead of just Error, since + // HttpError is also used on server to throw errors like these. + // That would require copying HttpError code to web-app also and using it here. + const responseJson = error.response?.data + const responseStatusCode = error.response.status + throw new WaspHttpError(responseStatusCode, responseJson?.message ?? error.message, responseJson) + } else { + // If any other error, we just propagate it. + throw error + } +} + +class WaspHttpError extends Error { + statusCode: number + + data: unknown + + constructor (statusCode: number, message: string, data: unknown) { + super(message) + this.statusCode = statusCode + this.data = data + } +} + +export default api diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/Auth.tsx b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/Auth.tsx new file mode 100644 index 0000000000..92c58131f6 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/Auth.tsx @@ -0,0 +1,85 @@ +import { useState, createContext } from 'react' +import { createTheme } from '@stitches/react' +import { styled } from 'wasp/core/stitches.config' + +import { + type State, + type CustomizationOptions, + type ErrorMessage, + type AdditionalSignupFields, +} from './types' +import { LoginSignupForm } from './internal/common/LoginSignupForm' +import { MessageError, MessageSuccess } from './internal/Message' + +const logoStyle = { + height: '3rem' +} + +const Container = styled('div', { + display: 'flex', + flexDirection: 'column', +}) + +const HeaderText = styled('h2', { + fontSize: '1.875rem', + fontWeight: '700', + marginTop: '1.5rem' +}) + + +export const AuthContext = createContext({ + isLoading: false, + setIsLoading: (isLoading: boolean) => {}, + setErrorMessage: (errorMessage: ErrorMessage | null) => {}, + setSuccessMessage: (successMessage: string | null) => {}, +}) + +function Auth ({ state, appearance, logo, socialLayout = 'horizontal', additionalSignupFields }: { + state: State; +} & CustomizationOptions & { + additionalSignupFields?: AdditionalSignupFields; +}) { + const [errorMessage, setErrorMessage] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + // TODO(matija): this is called on every render, is it a problem? + // If we do it in useEffect(), then there is a glitch between the default color and the + // user provided one. + const customTheme = createTheme(appearance ?? {}) + + const titles: Record = { + login: 'Log in to your account', + signup: 'Create a new account', + } + const title = titles[state] + + const socialButtonsDirection = socialLayout === 'vertical' ? 'vertical' : 'horizontal' + + return ( + +
    + {logo && (Your Company)} + {title} +
    + + {errorMessage && ( + + {errorMessage.title}{errorMessage.description && ': '}{errorMessage.description} + + )} + {successMessage && {successMessage}} + + {(state === 'login' || state === 'signup') && ( + + )} + +
    + ) +} + +export default Auth; diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/Login.tsx b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/Login.tsx new file mode 100644 index 0000000000..2ea532d9c5 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/Login.tsx @@ -0,0 +1,17 @@ +import Auth from './Auth' +import { type CustomizationOptions, State } from './types' + +export function LoginForm({ + appearance, + logo, + socialLayout, +}: CustomizationOptions) { + return ( + + ) +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/Signup.tsx b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/Signup.tsx new file mode 100644 index 0000000000..66ffab4503 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/Signup.tsx @@ -0,0 +1,23 @@ +import Auth from './Auth' +import { + type CustomizationOptions, + type AdditionalSignupFields, + State, +} from './types' + +export function SignupForm({ + appearance, + logo, + socialLayout, + additionalFields, +}: CustomizationOptions & { additionalFields?: AdditionalSignupFields; }) { + return ( + + ) +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/internal/Form.tsx b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/internal/Form.tsx new file mode 100644 index 0000000000..781c75a0ae --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/internal/Form.tsx @@ -0,0 +1,95 @@ +import { styled } from 'wasp/core/stitches.config' + +export const Form = styled('form', { + marginTop: '1.5rem', +}) + +export const FormItemGroup = styled('div', { + '& + div': { + marginTop: '1.5rem', + }, +}) + +export const FormLabel = styled('label', { + display: 'block', + fontSize: '$sm', + fontWeight: '500', + marginBottom: '0.5rem', +}) + +const commonInputStyles = { + display: 'block', + lineHeight: '1.5rem', + fontSize: '$sm', + borderWidth: '1px', + borderColor: '$gray600', + backgroundColor: '#f8f4ff', + boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', + '&:focus': { + borderWidth: '1px', + borderColor: '$gray700', + boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', + }, + '&:disabled': { + opacity: 0.5, + cursor: 'not-allowed', + backgroundColor: '$gray400', + borderColor: '$gray400', + color: '$gray500', + }, + + borderRadius: '0.375rem', + width: '100%', + + paddingTop: '0.375rem', + paddingBottom: '0.375rem', + paddingLeft: '0.75rem', + paddingRight: '0.75rem', + margin: 0, +} + +export const FormInput = styled('input', commonInputStyles) + +export const FormTextarea = styled('textarea', commonInputStyles) + +export const FormError = styled('div', { + display: 'block', + fontSize: '$sm', + fontWeight: '500', + color: '$formErrorText', + marginTop: '0.5rem', +}) + +export const SubmitButton = styled('button', { + display: 'flex', + justifyContent: 'center', + + width: '100%', + borderWidth: '1px', + borderColor: '$brand', + backgroundColor: '$brand', + color: '$submitButtonText', + + padding: '0.5rem 0.75rem', + boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', + + fontWeight: '600', + fontSize: '$sm', + lineHeight: '1.25rem', + borderRadius: '0.375rem', + + // TODO(matija): extract this into separate BaseButton component and then inherit it. + '&:hover': { + backgroundColor: '$brandAccent', + borderColor: '$brandAccent', + }, + '&:disabled': { + opacity: 0.5, + cursor: 'not-allowed', + backgroundColor: '$gray400', + borderColor: '$gray400', + color: '$gray500', + }, + transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)', + transitionDuration: '100ms', +}) diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/internal/Message.tsx b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/internal/Message.tsx new file mode 100644 index 0000000000..7279ed2525 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/internal/Message.tsx @@ -0,0 +1,18 @@ +import { styled } from 'wasp/core/stitches.config' + +export const Message = styled('div', { + padding: '0.5rem 0.75rem', + borderRadius: '0.375rem', + marginTop: '1rem', + background: '$gray400', +}) + +export const MessageError = styled(Message, { + background: '$errorBackground', + color: '$errorText', +}) + +export const MessageSuccess = styled(Message, { + background: '$successBackground', + color: '$successText', +}) diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/internal/common/LoginSignupForm.tsx b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/internal/common/LoginSignupForm.tsx new file mode 100644 index 0000000000..30665b4759 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/internal/common/LoginSignupForm.tsx @@ -0,0 +1,178 @@ +import { useContext } from 'react' +import { useForm, UseFormReturn } from 'react-hook-form' +import { styled } from 'wasp/core/stitches.config' +import config from 'wasp/core/config' + +import { AuthContext } from '../../Auth' +import { + Form, + FormInput, + FormItemGroup, + FormLabel, + FormError, + FormTextarea, + SubmitButton, +} from '../Form' +import type { + AdditionalSignupFields, + AdditionalSignupField, + AdditionalSignupFieldRenderFn, + FormState, +} from '../../types' +import { useHistory } from 'react-router-dom' +import { useUsernameAndPassword } from '../usernameAndPassword/useUsernameAndPassword' + + +export type LoginSignupFormFields = { + [key: string]: string; +} + +export const LoginSignupForm = ({ + state, + socialButtonsDirection = 'horizontal', + additionalSignupFields, +}: { + state: 'login' | 'signup' + socialButtonsDirection?: 'horizontal' | 'vertical' + additionalSignupFields?: AdditionalSignupFields +}) => { + const { + isLoading, + setErrorMessage, + setSuccessMessage, + setIsLoading, + } = useContext(AuthContext) + const isLogin = state === 'login' + const cta = isLogin ? 'Log in' : 'Sign up'; + const history = useHistory(); + const onErrorHandler = (error) => { + setErrorMessage({ title: error.message, description: error.data?.data?.message }) + }; + const hookForm = useForm() + const { register, formState: { errors }, handleSubmit: hookFormHandleSubmit } = hookForm + const { handleSubmit } = useUsernameAndPassword({ + isLogin, + onError: onErrorHandler, + onSuccess() { + history.push('/') + }, + }); + async function onSubmit (data) { + setIsLoading(true); + setErrorMessage(null); + setSuccessMessage(null); + try { + await handleSubmit(data); + } finally { + setIsLoading(false); + } + } + + return (<> +
    + + Username + + {errors.username && {errors.username.message}} + + + Password + + {errors.password && {errors.password.message}} + + + + {cta} + + + ) +} + +function AdditionalFormFields({ + hookForm, + formState: { isLoading }, + additionalSignupFields, +}: { + hookForm: UseFormReturn; + formState: FormState; + additionalSignupFields: AdditionalSignupFields; +}) { + const { + register, + formState: { errors }, + } = hookForm; + + function renderField>( + field: AdditionalSignupField, + // Ideally we would use ComponentType here, but it doesn't work with react-hook-form + Component: any, + props?: React.ComponentProps + ) { + return ( + + {field.label} + + {errors[field.name] && ( + {errors[field.name].message} + )} + + ); + } + + if (areAdditionalFieldsRenderFn(additionalSignupFields)) { + return additionalSignupFields(hookForm, { isLoading }) + } + + return ( + additionalSignupFields && + additionalSignupFields.map((field) => { + if (isFieldRenderFn(field)) { + return field(hookForm, { isLoading }) + } + switch (field.type) { + case 'input': + return renderField(field, FormInput, { + type: 'text', + }) + case 'textarea': + return renderField(field, FormTextarea) + default: + throw new Error( + `Unsupported additional signup field type: ${field.type}` + ) + } + }) + ) +} + +function isFieldRenderFn( + additionalSignupField: AdditionalSignupField | AdditionalSignupFieldRenderFn +): additionalSignupField is AdditionalSignupFieldRenderFn { + return typeof additionalSignupField === 'function' +} + +function areAdditionalFieldsRenderFn( + additionalSignupFields: AdditionalSignupFields +): additionalSignupFields is AdditionalSignupFieldRenderFn { + return typeof additionalSignupFields === 'function' +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts new file mode 100644 index 0000000000..247c1faeb4 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts @@ -0,0 +1,29 @@ +import signup from '../../../signup' +import login from '../../../login' + +export function useUsernameAndPassword({ + onError, + onSuccess, + isLogin, +}: { + onError: (error: Error) => void + onSuccess: () => void + isLogin: boolean +}) { + async function handleSubmit(data) { + try { + if (!isLogin) { + await signup(data) + } + await login(data.username, data.password) + + onSuccess() + } catch (err: unknown) { + onError(err as Error) + } + } + + return { + handleSubmit, + } +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/types.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/types.ts new file mode 100644 index 0000000000..14d61ad51e --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/forms/types.ts @@ -0,0 +1,39 @@ +import { createTheme } from '@stitches/react' +import { UseFormReturn, RegisterOptions } from 'react-hook-form' +import type { LoginSignupFormFields } from './internal/common/LoginSignupForm' + +export enum State { + Login = 'login', + Signup = 'signup', +} + +export type CustomizationOptions = { + logo?: string + socialLayout?: 'horizontal' | 'vertical' + appearance?: Parameters[0] +} + +export type ErrorMessage = { + title: string + description?: string +} + +export type FormState = { + isLoading: boolean +} + +export type AdditionalSignupFieldRenderFn = ( + hookForm: UseFormReturn, + formState: FormState +) => React.ReactNode + +export type AdditionalSignupField = { + name: string + label: string + type: 'input' | 'textarea' + validations?: RegisterOptions +} + +export type AdditionalSignupFields = + | (AdditionalSignupField | AdditionalSignupFieldRenderFn)[] + | AdditionalSignupFieldRenderFn diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/helpers/user.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/helpers/user.ts new file mode 100644 index 0000000000..c3e6a4072b --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/helpers/user.ts @@ -0,0 +1,14 @@ +import { setAuthToken } from 'wasp/api' +import { invalidateAndRemoveQueries } from 'wasp/operations/resources' + +export async function initSession(token: string): Promise { + setAuthToken(token) + // We need to invalidate queries after login in order to get the correct user + // data in the React components (using `useAuth`). + // Redirects after login won't work properly without this. + + // TODO(filip): We are currently removing all the queries, but we should + // remove only non-public, user-dependent queries - public queries are + // expected not to change in respect to the currently logged in user. + await invalidateAndRemoveQueries() +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/login.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/login.ts new file mode 100644 index 0000000000..487b45b981 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/login.ts @@ -0,0 +1,13 @@ +import api, { handleApiError } from 'wasp/api' +import { initSession } from './helpers/user' + +export default async function login(username: string, password: string): Promise { + try { + const args = { username, password } + const response = await api.post('/auth/username/login', args) + + await initSession(response.data.token) + } catch (error) { + handleApiError(error) + } +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/logout.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/logout.ts new file mode 100644 index 0000000000..340e9dec9c --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/logout.ts @@ -0,0 +1,9 @@ +import { removeLocalUserData } from 'wasp/api' +import { invalidateAndRemoveQueries } from 'wasp/operations/resources' + +export default async function logout(): Promise { + removeLocalUserData() + // TODO(filip): We are currently invalidating and removing all the queries, but + // we should remove only the non-public, user-dependent ones. + await invalidateAndRemoveQueries() +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/pages/createAuthRequiredPage.jsx b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/pages/createAuthRequiredPage.jsx new file mode 100644 index 0000000000..621ef393d9 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/pages/createAuthRequiredPage.jsx @@ -0,0 +1,30 @@ +import React from 'react' + +import { Redirect } from 'react-router-dom' +import useAuth from '../useAuth' + + +const createAuthRequiredPage = (Page) => { + return (props) => { + const { data: user, isError, isSuccess, isLoading } = useAuth() + + if (isSuccess) { + if (user) { + return ( + + ) + } else { + return + } + } else if (isLoading) { + return Loading... + } else if (isError) { + return An error ocurred. Please refresh the page. + } else { + return An unknown error ocurred. Please refresh the page. + } + } +} + +export default createAuthRequiredPage + diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/providers/types.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/providers/types.ts new file mode 100644 index 0000000000..5bbc99ca83 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/providers/types.ts @@ -0,0 +1,38 @@ +import type { Router, Request } from 'express' +import type { Prisma } from '@prisma/client' +import type { Expand } from 'wasp/universal/types' +import type { ProviderName } from '../utils' + +type UserEntityCreateInput = Prisma.UserCreateInput + +export type ProviderConfig = { + // Unique provider identifier, used as part of URL paths + id: ProviderName; + displayName: string; + // Each provider config can have an init method which is ran on setup time + // e.g. for oAuth providers this is the time when the Passport strategy is registered. + init?(provider: ProviderConfig): Promise; + // Every provider must have a setupRouter method which returns the Express router. + // In this function we are flexibile to do what ever is necessary to make the provider work. + createRouter(provider: ProviderConfig, initData: InitData): Router; +}; + +export type InitData = { + [key: string]: any; +} + +export type RequestWithWasp = Request & { wasp?: { [key: string]: any } } + +export type PossibleAdditionalSignupFields = Expand> + +export function defineAdditionalSignupFields(config: { + [key in keyof PossibleAdditionalSignupFields]: FieldGetter< + PossibleAdditionalSignupFields[key] + > +}) { + return config +} + +type FieldGetter = ( + data: { [key: string]: unknown } +) => Promise | T | undefined diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/signup.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/signup.ts new file mode 100644 index 0000000000..bde50c5ebd --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/signup.ts @@ -0,0 +1,9 @@ +import api, { handleApiError } from 'wasp/api' + +export default async function signup(userFields: { username: string; password: string }): Promise { + try { + await api.post('/auth/username/signup', userFields) + } catch (error) { + handleApiError(error) + } +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/types.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/types.ts new file mode 100644 index 0000000000..9240b4e4b0 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/types.ts @@ -0,0 +1,2 @@ +// todo(filip): turn into a proper import/path +export type { SanitizedUser as User, ProviderName, DeserializedAuthEntity } from 'wasp/server/_types/' diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/useAuth.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/useAuth.ts new file mode 100644 index 0000000000..29b95f62a0 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/useAuth.ts @@ -0,0 +1,38 @@ +import { deserialize as superjsonDeserialize } from 'superjson' +import { useQuery } from 'wasp/rpc' +import api, { handleApiError } from 'wasp/api' +import { HttpMethod } from 'wasp/types' +import type { User } from './types' +import { addMetadataToQuery } from 'wasp/rpc/queries' + +export const getMe = createUserGetter() + +export default function useAuth(queryFnArgs?: unknown, config?: any) { + return useQuery(getMe, queryFnArgs, config) +} + +function createUserGetter() { + const getMeRelativePath = 'auth/me' + const getMeRoute = { method: HttpMethod.Get, path: `/${getMeRelativePath}` } + async function getMe(): Promise { + try { + const response = await api.get(getMeRoute.path) + + return superjsonDeserialize(response.data) + } catch (error) { + if (error.response?.status === 401) { + return null + } else { + handleApiError(error) + } + } + } + + addMetadataToQuery(getMe, { + relativeQueryPath: getMeRelativePath, + queryRoute: getMeRoute, + entitiesUsed: ['User'], + }) + + return getMe +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/user.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/user.ts new file mode 100644 index 0000000000..5799c71ea7 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/user.ts @@ -0,0 +1,27 @@ +// We decided not to deduplicate these helper functions in the server and the client. +// We have them duplicated in this file and in data/Generator/templates/server/src/auth/user.ts +// If you are changing the logic here, make sure to change it there as well. + +import type { User, ProviderName, DeserializedAuthEntity } from './types' + +export function getEmail(user: User): string | null { + return findUserIdentity(user, "email")?.providerUserId ?? null; +} + +export function getUsername(user: User): string | null { + return findUserIdentity(user, "username")?.providerUserId ?? null; +} + +export function getFirstProviderUserId(user?: User): string | null { + if (!user || !user.auth || !user.auth.identities || user.auth.identities.length === 0) { + return null; + } + + return user.auth.identities[0].providerUserId ?? null; +} + +export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthEntity | undefined { + return user.auth.identities.find( + (identity) => identity.providerName === providerName + ); +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/utils.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/utils.ts new file mode 100644 index 0000000000..7a180abdc6 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/utils.ts @@ -0,0 +1,290 @@ +import { hashPassword, sign, verify } from 'wasp/core/auth' +import AuthError from '../core/AuthError.js' +import HttpError from '../core/HttpError.js' +import prisma from '../server/dbClient.js' +import { sleep } from '../server/utils' +import { + type User, + type Auth, + type AuthIdentity, +} from '../entities' +import { Prisma } from '@prisma/client'; + +import { throwValidationError } from './validation.js' + + +import { defineAdditionalSignupFields, type PossibleAdditionalSignupFields } from './providers/types.js' +const _waspAdditionalSignupFieldsConfig = {} as ReturnType + +export type EmailProviderData = { + hashedPassword: string; + isEmailVerified: boolean; + emailVerificationSentAt: string | null; + passwordResetSentAt: string | null; +} + +export type UsernameProviderData = { + hashedPassword: string; +} + +export type OAuthProviderData = {} + +/** + * This type is used for type-level programming e.g. to enumerate + * all possible provider data types. + * + * The keys of this type are the names of the providers and the values + * are the types of the provider data. + */ +export type PossibleProviderData = { + email: EmailProviderData; + username: UsernameProviderData; + google: OAuthProviderData; + github: OAuthProviderData; +} + +export type ProviderName = keyof PossibleProviderData + +export const contextWithUserEntity = { + entities: { + User: prisma.user + } +} + +export const authConfig = { + failureRedirectPath: "/login", + successRedirectPath: "/", +} + +/** + * ProviderId uniquely identifies an auth identity e.g. + * "email" provider with user id "test@test.com" or + * "google" provider with user id "1234567890". + * + * We use this type to avoid passing the providerName and providerUserId + * separately. Also, we can normalize the providerUserId to make sure it's + * consistent across different DB operations. + */ +export type ProviderId = { + providerName: ProviderName; + providerUserId: string; +} + +export function createProviderId(providerName: ProviderName, providerUserId: string): ProviderId { + return { + providerName, + providerUserId: providerUserId.toLowerCase(), + } +} + +export async function findAuthIdentity(providerId: ProviderId): Promise { + return prisma.authIdentity.findUnique({ + where: { + providerName_providerUserId: providerId, + } + }); +} + +/** + * Updates the provider data for the given auth identity. + * + * This function performs data sanitization and serialization. + * Sanitization is done by hashing the password, so this function + * expects the password received in the `providerDataUpdates` + * **not to be hashed**. + */ +export async function updateAuthIdentityProviderData( + providerId: ProviderId, + existingProviderData: PossibleProviderData[PN], + providerDataUpdates: Partial, +): Promise { + // We are doing the sanitization here only on updates to avoid + // hashing the password multiple times. + const sanitizedProviderDataUpdates = await sanitizeProviderData(providerDataUpdates); + const newProviderData = { + ...existingProviderData, + ...sanitizedProviderDataUpdates, + } + const serializedProviderData = await serializeProviderData(newProviderData); + return prisma.authIdentity.update({ + where: { + providerName_providerUserId: providerId, + }, + data: { providerData: serializedProviderData }, + }); +} + +type FindAuthWithUserResult = Auth & { + user: User +} + +export async function findAuthWithUserBy( + where: Prisma.AuthWhereInput +): Promise { + return prisma.auth.findFirst({ where, include: { user: true }}); +} + +export async function createUser( + providerId: ProviderId, + serializedProviderData?: string, + userFields?: PossibleAdditionalSignupFields, +): Promise { + return prisma.user.create({ + data: { + // Using any here to prevent type errors when userFields are not + // defined. We want Prisma to throw an error in that case. + ...(userFields ?? {} as any), + auth: { + create: { + identities: { + create: { + providerName: providerId.providerName, + providerUserId: providerId.providerUserId, + providerData: serializedProviderData, + }, + }, + } + }, + } + }) +} + +export async function deleteUserByAuthId(authId: string): Promise<{ count: number }> { + return prisma.user.deleteMany({ where: { auth: { + id: authId, + } } }) +} + +export async function createAuthToken( + userId: User['id'] +): Promise { + return sign(userId); +} + +export async function verifyToken(token: string): Promise { + return verify(token); +} + +// If an user exists, we don't want to leak information +// about it. Pretending that we're doing some work +// will make it harder for an attacker to determine +// if a user exists or not. +// NOTE: Attacker measuring time to response can still determine +// if a user exists or not. We'll be able to avoid it when +// we implement e-mail sending via jobs. +export async function doFakeWork(): Promise { + const timeToWork = Math.floor(Math.random() * 1000) + 1000; + return sleep(timeToWork); +} + +export function rethrowPossibleAuthError(e: unknown): void { + if (e instanceof AuthError) { + throwValidationError(e.message); + } + + // Prisma code P2002 is for unique constraint violations. + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') { + throw new HttpError(422, 'Save failed', { + message: `user with the same identity already exists`, + }) + } + + if (e instanceof Prisma.PrismaClientValidationError) { + // NOTE: Logging the error since this usually means that there are + // required fields missing in the request, we want the developer + // to know about it. + console.error(e) + throw new HttpError(422, 'Save failed', { + message: 'there was a database error' + }) + } + + // Prisma code P2021 is for missing table errors. + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2021') { + // NOTE: Logging the error since this usually means that the database + // migrations weren't run, we want the developer to know about it. + console.error(e) + console.info('🐝 This error can happen if you did\'t run the database migrations.') + throw new HttpError(500, 'Save failed', { + message: `there was a database error`, + }) + } + + // Prisma code P2003 is for foreign key constraint failure + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2003') { + console.error(e) + console.info(`🐝 This error can happen if you have some relation on your User entity + but you didn't specify the "onDelete" behaviour to either "Cascade" or "SetNull". + Read more at: https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/referential-actions`) + throw new HttpError(500, 'Save failed', { + message: `there was a database error`, + }) + } + + throw e +} + +export async function validateAndGetAdditionalFields(data: { + [key: string]: unknown +}): Promise> { + const { + password: _password, + ...sanitizedData + } = data; + const result: Record = {}; + for (const [field, getFieldValue] of Object.entries(_waspAdditionalSignupFieldsConfig)) { + try { + const value = await getFieldValue(sanitizedData) + result[field] = value + } catch (e) { + throwValidationError(e.message) + } + } + return result; +} + +export function deserializeAndSanitizeProviderData( + providerData: string, + { shouldRemovePasswordField = false }: { shouldRemovePasswordField?: boolean } = {}, +): PossibleProviderData[PN] { + // NOTE: We are letting JSON.parse throw an error if the providerData is not valid JSON. + let data = JSON.parse(providerData) as PossibleProviderData[PN]; + + if (providerDataHasPasswordField(data) && shouldRemovePasswordField) { + delete data.hashedPassword; + } + + return data; +} + +export async function sanitizeAndSerializeProviderData( + providerData: PossibleProviderData[PN], +): Promise { + return serializeProviderData( + await sanitizeProviderData(providerData) + ); +} + +function serializeProviderData(providerData: PossibleProviderData[PN]): string { + return JSON.stringify(providerData); +} + +async function sanitizeProviderData( + providerData: PossibleProviderData[PN], +): Promise { + const data = { + ...providerData, + }; + if (providerDataHasPasswordField(data)) { + data.hashedPassword = await hashPassword(data.hashedPassword); + } + + return data; +} + + +function providerDataHasPasswordField( + providerData: PossibleProviderData[keyof PossibleProviderData], +): providerData is { hashedPassword: string } { + return 'hashedPassword' in providerData; +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/validation.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/validation.ts new file mode 100644 index 0000000000..f384a28c87 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/validation.ts @@ -0,0 +1,77 @@ +import HttpError from '../core/HttpError.js'; + +export const PASSWORD_FIELD = 'password'; +const USERNAME_FIELD = 'username'; +const EMAIL_FIELD = 'email'; +const TOKEN_FIELD = 'token'; + +export function ensureValidEmail(args: unknown): void { + validate(args, [ + { validates: EMAIL_FIELD, message: 'email must be present', validator: email => !!email }, + { validates: EMAIL_FIELD, message: 'email must be a valid email', validator: email => isValidEmail(email) }, + ]); +} + +export function ensureValidUsername(args: unknown): void { + validate(args, [ + { validates: USERNAME_FIELD, message: 'username must be present', validator: username => !!username } + ]); +} + +export function ensurePasswordIsPresent(args: unknown): void { + validate(args, [ + { validates: PASSWORD_FIELD, message: 'password must be present', validator: password => !!password }, + ]); +} + +export function ensureValidPassword(args: unknown): void { + validate(args, [ + { validates: PASSWORD_FIELD, message: 'password must be at least 8 characters', validator: password => isMinLength(password, 8) }, + { validates: PASSWORD_FIELD, message: 'password must contain a number', validator: password => containsNumber(password) }, + ]); +} + +export function ensureTokenIsPresent(args: unknown): void { + validate(args, [ + { validates: TOKEN_FIELD, message: 'token must be present', validator: token => !!token }, + ]); +} + +export function throwValidationError(message: string): void { + throw new HttpError(422, 'Validation failed', { message }) +} + +function validate(args: unknown, validators: { validates: string, message: string, validator: (value: unknown) => boolean }[]): void { + for (const { validates, message, validator } of validators) { + if (!validator(args[validates])) { + throwValidationError(message); + } + } +} + +// NOTE(miho): it would be good to replace our custom validations with e.g. Zod + +const validEmailRegex = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/ +function isValidEmail(input: unknown): boolean { + if (typeof input !== 'string') { + return false + } + + return input.match(validEmailRegex) !== null +} + +function isMinLength(input: unknown, minLength: number): boolean { + if (typeof input !== 'string') { + return false + } + + return input.length >= minLength +} + +function containsNumber(input: unknown): boolean { + if (typeof input !== 'string') { + return false + } + + return /\d/.test(input) +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/AuthError.js b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/AuthError.js new file mode 100644 index 0000000000..2d965c168e --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/AuthError.js @@ -0,0 +1,17 @@ +class AuthError extends Error { + constructor (message, data, ...params) { + super(message, ...params) + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, AuthError) + } + + this.name = this.constructor.name + + if (data) { + this.data = data + } + } +} + +export default AuthError diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/HttpError.js b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/HttpError.js new file mode 100644 index 0000000000..8a2cb04db5 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/HttpError.js @@ -0,0 +1,22 @@ +class HttpError extends Error { + constructor (statusCode, message, data, ...params) { + super(message, ...params) + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, HttpError) + } + + this.name = this.constructor.name + + if (!(Number.isInteger(statusCode) && statusCode >= 400 && statusCode < 600)) { + throw new Error('statusCode has to be integer in range [400, 600).') + } + this.statusCode = statusCode + + if (data) { + this.data = data + } + } +} + +export default HttpError diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/auth.js b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/auth.js new file mode 100644 index 0000000000..75e77a7fb9 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/auth.js @@ -0,0 +1,145 @@ +import jwt from 'jsonwebtoken' +import SecurePassword from 'secure-password' +import util from 'util' +import { randomInt } from 'node:crypto' + +import prisma from '@server/dbClient.js' +import { handleRejection } from '../server/utils' +import HttpError from './HttpError.js' +import config from '../config.js' +import { deserializeAndSanitizeProviderData } from 'wasp/auth/utils' + +const jwtSign = util.promisify(jwt.sign) +const jwtVerify = util.promisify(jwt.verify) + +const JWT_SECRET = config.auth.jwtSecret + +export const signData = (data, options) => jwtSign(data, JWT_SECRET, options) +export const sign = (id, options) => signData({ id }, options) +export const verify = (token) => jwtVerify(token, JWT_SECRET) + +const auth = handleRejection(async (req, res, next) => { + const authHeader = req.get('Authorization') + if (!authHeader) { + // NOTE(matija): for now we let tokenless requests through and make it operation's + // responsibility to verify whether the request is authenticated or not. In the future + // we will develop our own system at Wasp-level for that. + return next() + } + + if (authHeader.startsWith('Bearer ')) { + const token = authHeader.substring(7, authHeader.length) + req.user = await getUserFromToken(token) + } else { + throwInvalidCredentialsError() + } + + next() +}) + +export async function getUserFromToken(token) { + let userIdFromToken + try { + userIdFromToken = (await verify(token)).id + } catch (error) { + if (['TokenExpiredError', 'JsonWebTokenError', 'NotBeforeError'].includes(error.name)) { + throwInvalidCredentialsError() + } else { + throw error + } + } + + const user = await prisma.user + .findUnique({ + where: { id: userIdFromToken }, + include: { + auth: { + include: { + identities: true + } + } + } + }) + if (!user) { + throwInvalidCredentialsError() + } + + // TODO: This logic must match the type in types/index.ts (if we remove the + // password field from the object here, we must to do the same there). + // Ideally, these two things would live in the same place: + // https://github.com/wasp-lang/wasp/issues/965 + let sanitizedUser = { ...user } + sanitizedUser.auth.identities = sanitizedUser.auth.identities.map(identity => { + identity.providerData = deserializeAndSanitizeProviderData(identity.providerData, { shouldRemovePasswordField: true }) + return identity + }); + return sanitizedUser +} + +const SP = new SecurePassword() + +export const hashPassword = async (password) => { + const hashedPwdBuffer = await SP.hash(Buffer.from(password)) + return hashedPwdBuffer.toString("base64") +} + +export const verifyPassword = async (hashedPassword, password) => { + const result = await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64")) + if (result !== SecurePassword.VALID) { + throw new Error('Invalid password.') + } +} + +// Generates an unused username that looks similar to "quick-purple-sheep-91231". +// It generates several options and ensures it picks one that is not currently in use. +export function generateAvailableDictionaryUsername() { + const adjectives = ['fuzzy', 'tall', 'short', 'nice', 'happy', 'quick', 'slow', 'good', 'new', 'old', 'first', 'last', 'old', 'young'] + const colors = ['red', 'green', 'blue', 'white', 'black', 'brown', 'purple', 'orange', 'yellow'] + const nouns = ['wasp', 'cat', 'dog', 'lion', 'rabbit', 'duck', 'pig', 'bee', 'goat', 'crab', 'fish', 'chicken', 'horse', 'llama', 'camel', 'sheep'] + + const potentialUsernames = [] + for (let i = 0; i < 10; i++) { + const potentialUsername = `${adjectives[randomInt(adjectives.length)]}-${colors[randomInt(colors.length)]}-${nouns[randomInt(nouns.length)]}-${randomInt(100_000)}` + potentialUsernames.push(potentialUsername) + } + + return findAvailableUsername(potentialUsernames) +} + +// Generates an unused username based on an array of username segments and a separator. +// It generates several options and ensures it picks one that is not currently in use. +export function generateAvailableUsername(usernameSegments, config) { + const separator = config?.separator || '-' + const baseUsername = usernameSegments.join(separator) + + const potentialUsernames = [] + for (let i = 0; i < 10; i++) { + const potentialUsername = `${baseUsername}${separator}${randomInt(100_000)}` + potentialUsernames.push(potentialUsername) + } + + return findAvailableUsername(potentialUsernames) +} + +// Checks the database for an unused username from an array provided and returns first. +async function findAvailableUsername(potentialUsernames) { + const users = await prisma.user.findMany({ + where: { + username: { in: potentialUsernames }, + } + }) + const takenUsernames = users.map(user => user.username) + const availableUsernames = potentialUsernames.filter(username => !takenUsernames.includes(username)) + + if (availableUsernames.length === 0) { + throw new Error('Unable to generate a unique username. Please contact Wasp.') + } + + return availableUsernames[0] +} + +export function throwInvalidCredentialsError(message) { + throw new HttpError(401, 'Invalid credentials', { message }) +} + +export default auth diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/config.js b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/config.js new file mode 100644 index 0000000000..e9234e6f2a --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/config.js @@ -0,0 +1,9 @@ +import { stripTrailingSlash } from 'wasp/universal/url' + +const apiUrl = stripTrailingSlash(import.meta.env.REACT_APP_API_URL) || 'http://localhost:3001'; + +const config = { + apiUrl, +} + +export default config diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/stitches.config.js b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/stitches.config.js new file mode 100644 index 0000000000..c1d600a3f6 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/stitches.config.js @@ -0,0 +1,33 @@ +import { createStitches } from '@stitches/react' + +export const { + styled, + css +} = createStitches({ + theme: { + colors: { + waspYellow: '#ffcc00', + gray700: '#a1a5ab', + gray600: '#d1d5db', + gray500: 'gainsboro', + gray400: '#f0f0f0', + red: '#FED7D7', + darkRed: '#fa3838', + green: '#C6F6D5', + + brand: '$waspYellow', + brandAccent: '#ffdb46', + errorBackground: '$red', + errorText: '#2D3748', + successBackground: '$green', + successText: '#2D3748', + + submitButtonText: 'black', + + formErrorText: '$darkRed', + }, + fontSizes: { + sm: '0.875rem' + } + } +}) diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/storage.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/storage.ts new file mode 100644 index 0000000000..0321acea8b --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/storage.ts @@ -0,0 +1,50 @@ +export type DataStore = { + getPrefixedKey(key: string): string + set(key: string, value: unknown): void + get(key: string): unknown + remove(key: string): void + clear(): void +} + +function createLocalStorageDataStore(prefix: string): DataStore { + function getPrefixedKey(key: string): string { + return `${prefix}:${key}` + } + + return { + getPrefixedKey, + set(key, value) { + ensureLocalStorageIsAvailable() + localStorage.setItem(getPrefixedKey(key), JSON.stringify(value)) + }, + get(key) { + ensureLocalStorageIsAvailable() + const value = localStorage.getItem(getPrefixedKey(key)) + try { + return value ? JSON.parse(value) : undefined + } catch (e: any) { + return undefined + } + }, + remove(key) { + ensureLocalStorageIsAvailable() + localStorage.removeItem(getPrefixedKey(key)) + }, + clear() { + ensureLocalStorageIsAvailable() + Object.keys(localStorage).forEach((key) => { + if (key.startsWith(prefix)) { + localStorage.removeItem(key) + } + }) + }, + } +} + +export const storage = createLocalStorageDataStore('wasp') + +function ensureLocalStorageIsAvailable(): void { + if (!window.localStorage) { + throw new Error('Local storage is not available.') + } +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/entities/index.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/entities/index.ts new file mode 100644 index 0000000000..5febac3804 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/entities/index.ts @@ -0,0 +1,21 @@ +import { + type User, + type Task, +} from "@prisma/client" + +export { + type User, + type Task, + type Auth, + type AuthIdentity, +} from "@prisma/client" + +export type Entity = + | User + | Task + | never + +export type EntityName = + | "User" + | "Task" + | never diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/ext-src/actions.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/ext-src/actions.ts new file mode 100644 index 0000000000..c03bfac62b --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/ext-src/actions.ts @@ -0,0 +1,56 @@ +import HttpError from 'wasp/core/HttpError' +import type { + CreateTask, + UpdateTask, + DeleteTasks, +} from 'wasp/server/actions/types' +import type { Task } from 'wasp/entities' + +type CreateArgs = Pick + +export const createTask: CreateTask = async ( + { description }, + context +) => { + if (!context.user) { + throw new HttpError(401) + } + + return context.entities.Task.create({ + data: { + description, + user: { connect: { id: context.user.id } }, + }, + }) +} + +type UpdateArgs = Pick + +export const updateTask: UpdateTask = async ( + { id, isDone }, + context +) => { + if (!context.user) { + throw new HttpError(401) + } + + return context.entities.Task.update({ + where: { + id, + }, + data: { isDone }, + }) +} + +export const deleteTasks: DeleteTasks = async ( + idsToDelete, + context +) => { + return context.entities.Task.deleteMany({ + where: { + id: { + in: idsToDelete, + }, + }, + }) +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/ext-src/queries.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/ext-src/queries.ts new file mode 100644 index 0000000000..ac49e0a7a7 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/ext-src/queries.ts @@ -0,0 +1,15 @@ +import HttpError from 'wasp/core/HttpError' +import type { GetTasks } from 'wasp/server/queries/types' +import type { Task } from 'wasp/entities' + +//Using TypeScript's new 'satisfies' keyword, it will infer the types of the arguments and return value +export const getTasks = ((_args, context) => { + if (!context.user) { + throw new HttpError(401) + } + + return context.entities.Task.findMany({ + where: { user: { id: context.user.id } }, + orderBy: { id: 'asc' }, + }) +}) satisfies GetTasks diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/operations/index.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/operations/index.ts new file mode 100644 index 0000000000..31e70ae98b --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/operations/index.ts @@ -0,0 +1,22 @@ +import api, { handleApiError } from 'wasp/api' +import { HttpMethod } from 'wasp/types' +import { + serialize as superjsonSerialize, + deserialize as superjsonDeserialize, +} from 'superjson' + +export type OperationRoute = { method: HttpMethod, path: string } + +export async function callOperation(operationRoute: OperationRoute & { method: HttpMethod.Post }, args: any) { + try { + const superjsonArgs = superjsonSerialize(args) + const response = await api.post(operationRoute.path, superjsonArgs) + return superjsonDeserialize(response.data) + } catch (error) { + handleApiError(error) + } +} + +export function makeOperationRoute(relativeOperationRoute: string): OperationRoute { + return { method: HttpMethod.Post, path: `/${relativeOperationRoute}` } +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/operations/resources.js b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/operations/resources.js new file mode 100644 index 0000000000..5261654600 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/operations/resources.js @@ -0,0 +1,81 @@ +import { queryClientInitialized } from 'wasp/rpc/queryClient' +import { makeUpdateHandlersMap } from './updateHandlersMap' +import { hashQueryKey } from '@tanstack/react-query' + +// Map where key is resource name and value is Set +// containing query ids of all the queries that use +// that resource. +const resourceToQueryCacheKeys = new Map() + +const updateHandlers = makeUpdateHandlersMap(hashQueryKey) +/** + * Remembers that specified query is using specified resources. + * If called multiple times for same query, resources are added, not reset. + * @param {string[]} queryCacheKey - Unique key under used to identify query in the cache. + * @param {string[]} resources - Names of resources that query is using. + */ +export function addResourcesUsedByQuery(queryCacheKey, resources) { + for (const resource of resources) { + let cacheKeys = resourceToQueryCacheKeys.get(resource) + if (!cacheKeys) { + cacheKeys = new Set() + resourceToQueryCacheKeys.set(resource, cacheKeys) + } + cacheKeys.add(queryCacheKey) + } +} + +export function registerActionInProgress(optimisticUpdateTuples) { + optimisticUpdateTuples.forEach( + ({ queryKey, updateQuery }) => updateHandlers.add(queryKey, updateQuery) + ) +} + +export async function registerActionDone(resources, optimisticUpdateTuples) { + optimisticUpdateTuples.forEach(({ queryKey }) => updateHandlers.remove(queryKey)) + await invalidateQueriesUsing(resources) +} + +export function getActiveOptimisticUpdates(queryKey) { + return updateHandlers.getUpdateHandlers(queryKey) +} + +export async function invalidateAndRemoveQueries() { + const queryClient = await queryClientInitialized + // If we don't reset the queries before removing them, Wasp will stay on + // the same page. The user would have to manually refresh the page to "finish" + // logging out. + // When a query is removed, the `Observer` is removed as well, and the components + // that are using the query are not re-rendered. This is why we need to reset + // the queries, so that the `Observer` is re-created and the components are re-rendered. + // For more details: https://github.com/wasp-lang/wasp/pull/1014/files#r1111862125 + queryClient.resetQueries() + // If we don't remove the queries after invalidating them, the old query data + // remains in the cache, casuing a potential privacy issue. + queryClient.removeQueries() +} + +/** + * Invalidates all queries that are using specified resources. + * @param {string[]} resources - Names of resources. + */ +async function invalidateQueriesUsing(resources) { + const queryClient = await queryClientInitialized + + const queryCacheKeysToInvalidate = getQueriesUsingResources(resources) + queryCacheKeysToInvalidate.forEach( + queryCacheKey => queryClient.invalidateQueries(queryCacheKey) + ) +} + +/** + * @param {string} resource - Resource name. + * @returns {string[]} Array of "query cache keys" of queries that use specified resource. + */ +function getQueriesUsingResource(resource) { + return Array.from(resourceToQueryCacheKeys.get(resource) || []) +} + +function getQueriesUsingResources(resources) { + return Array.from(new Set(resources.flatMap(getQueriesUsingResource))) +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/operations/updateHandlersMap.js b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/operations/updateHandlersMap.js new file mode 100644 index 0000000000..8c43c0b1ba --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/operations/updateHandlersMap.js @@ -0,0 +1,37 @@ +export function makeUpdateHandlersMap(calculateHash) { + const updateHandlers = new Map() + + function getHandlerTuples(queryKeyHash) { + return updateHandlers.get(queryKeyHash) || []; + } + + function add(queryKey, updateQuery) { + const queryKeyHash = calculateHash(queryKey) + const handlers = getHandlerTuples(queryKeyHash); + updateHandlers.set(queryKeyHash, [...handlers, { queryKey, updateQuery }]) + } + + function getUpdateHandlers(queryKey) { + const queryKeyHash = calculateHash(queryKey) + return getHandlerTuples(queryKeyHash).map(({ updateQuery }) => updateQuery) + } + + function remove(queryKeyToRemove) { + const queryKeyHash = calculateHash(queryKeyToRemove) + const filteredHandlers = getHandlerTuples(queryKeyHash).filter( + ({ queryKey }) => queryKey !== queryKeyToRemove + ) + + if (filteredHandlers.length > 0) { + updateHandlers.set(queryKeyHash, filteredHandlers) + } else { + updateHandlers.delete(queryKeyHash) + } + } + + return { + add, + remove, + getUpdateHandlers, + } +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json new file mode 100644 index 0000000000..ead89c3038 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json @@ -0,0 +1,49 @@ +{ + "name": "wasp", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "types": "tsc --declaration --emitDeclarationOnly --stripInternal --declarationDir dist" + }, + "exports": { + "./core/HttpError": "./core/HttpError.js", + "./core/config": "./core/config.js", + "./core/stitches.config": "./core/stitches.config.js", + "./core/storage": "./core/storage.ts", + "./rpc": "./rpc/index.ts", + "./rpc/queries": "./rpc/queries/index.ts", + "./rpc/actions": "./rpc/actions/index.ts", + "./rpc/queryClient": "./rpc/queryClient.ts", + "./types": "./types/index.ts", + "./auth/*": "./auth/*", + "./api": "./api/index.ts", + "./api/*": "./api/*", + "./operations": "./operations/index.ts", + "./operations/*": "./operations/*", + "./universal/url": "./universal/url.ts", + "./universal/types": "./universal/url.ts" + }, + "license": "ISC", + "include": [ + "src/**/*" + ], + "dependencies": {"@prisma/client": "4.16.2", + "prisma": "4.16.2", + "@tanstack/react-query": "^4.29.0", + "axios": "^1.4.0", + "express": "~4.18.1", + "jsonwebtoken": "^8.5.1", + "mitt": "3.0.0", + "react": "^18.2.0", + "react-router-dom": "^5.3.3", + "react-hook-form": "^7.45.4", + "secure-password": "^4.0.0", + "superjson": "^1.12.2", + "@types/express-serve-static-core": "^4.17.13", + "@stitches/react": "^1.2.8" +}, + "devDependencies": { +} +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/actions/core.d.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/actions/core.d.ts new file mode 100644 index 0000000000..ea41a0eed3 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/actions/core.d.ts @@ -0,0 +1,13 @@ +import { type Action } from '.' +import type { Expand, _Awaited, _ReturnType } from 'wasp/universal/types' + +export function createAction( + actionRoute: string, + entitiesUsed: unknown[] +): ActionFor + +type ActionFor = Expand< + Action[0], _Awaited<_ReturnType>> +> + +type GenericBackendAction = (args: never, context: any) => unknown diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/actions/core.js b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/actions/core.js new file mode 100644 index 0000000000..cd1c60ecef --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/actions/core.js @@ -0,0 +1,37 @@ +import { callOperation, makeOperationRoute } from 'wasp/operations' +import { + registerActionInProgress, + registerActionDone, +} from 'wasp/operations/resources' + +// todo(filip) - turn helpers and core into the same thing + +export function createAction(relativeActionRoute, entitiesUsed) { + const actionRoute = makeOperationRoute(relativeActionRoute) + + async function internalAction(args, specificOptimisticUpdateDefinitions) { + registerActionInProgress(specificOptimisticUpdateDefinitions) + try { + // The `return await` is not redundant here. If we removed the await, the + // `finally` block would execute before the action finishes, prematurely + // registering the action as done. + return await callOperation(actionRoute, args) + } finally { + await registerActionDone(entitiesUsed, specificOptimisticUpdateDefinitions) + } + } + + // We expose (and document) a restricted version of the API for our users, + // while also attaching the full "internal" API to the exposed action. By + // doing this, we can easily use the internal API of an action a users passes + // into our system (e.g., through the `useAction` hook) without needing a + // lookup table. + // + // While it does technically allow our users to access the interal API, it + // shouldn't be a problem in practice. Still, if it turns out to be a problem, + // we can always hide it using a Symbol. + const action = (args) => internalAction(args, []) + action.internal = internalAction + + return action +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/actions/index.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/actions/index.ts new file mode 100644 index 0000000000..2be33b3d65 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/actions/index.ts @@ -0,0 +1,14 @@ +import { createAction } from './core' +import { CreateTask, UpdateTask } from 'wasp/server/actions' + +export const updateTask = createAction('operations/update-task', [ + 'Task', +]) + +export const createTask = createAction('operations/create-task', [ + 'Task', +]) + +export const deleteTasks = createAction('operations/delete-tasks', [ + 'Task', +]) diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/index.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/index.ts new file mode 100644 index 0000000000..8a743e3456 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/index.ts @@ -0,0 +1,338 @@ +import { + QueryClient, + QueryKey, + useMutation, + UseMutationOptions, + useQueryClient, + useQuery as rqUseQuery, + UseQueryResult, +} from "@tanstack/react-query"; +export { configureQueryClient } from "./queryClient"; + +export type Query = { + (queryCacheKey: string[], args: Input): Promise; +}; + +export function useQuery( + queryFn: Query, + queryFnArgs?: Input, + options?: any +): UseQueryResult; + +export function useQuery(queryFn, queryFnArgs, options) { + 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 queryKey = + queryFnArgs !== undefined + ? [...queryFn.queryCacheKey, queryFnArgs] + : queryFn.queryCacheKey; + return rqUseQuery({ + queryKey, + queryFn: () => queryFn(queryKey, queryFnArgs), + ...options, + }); +} + +// todo - turn helpers and core into the same thing + +export type Action = [Input] extends [never] + ? (args?: unknown) => Promise + : (args: Input) => Promise; + +/** + * An options object passed into the `useAction` hook and used to enhance the + * action with extra options. + * + */ +export type ActionOptions = { + optimisticUpdates: OptimisticUpdateDefinition[]; +}; + +/** + * A documented (public) way to define optimistic updates. + */ +export type OptimisticUpdateDefinition = { + getQuerySpecifier: GetQuerySpecifier; + updateQuery: UpdateQuery; +}; + +/** + * A function that takes an item and returns a Wasp Query specifier. + */ +export type GetQuerySpecifier = ( + item: ActionInput +) => QuerySpecifier; + +/** + * A function that takes an item and the previous state of the cache, and returns + * the desired (new) state of the cache. + */ +export type UpdateQuery = ( + item: ActionInput, + oldData: CachedData | undefined +) => CachedData; + +/** + * A public query specifier used for addressing Wasp queries. See our docs for details: + * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + */ +export type QuerySpecifier = [Query, ...any[]]; + +/** + * A hook for adding extra behavior to a Wasp Action (e.g., optimistic updates). + * + * @param actionFn The Wasp Action you wish to enhance/decorate. + * @param actionOptions An options object for enhancing/decorating the given Action. + * @returns A decorated Action with added behavior but an unchanged API. + */ +export function useAction( + actionFn: Action, + actionOptions?: ActionOptions +): typeof actionFn { + const queryClient = useQueryClient(); + + let mutationFn = actionFn; + let options = {}; + if (actionOptions?.optimisticUpdates) { + const optimisticUpdatesDefinitions = actionOptions.optimisticUpdates.map( + translateToInternalDefinition + ); + mutationFn = makeOptimisticUpdateMutationFn( + actionFn, + optimisticUpdatesDefinitions + ); + options = makeRqOptimisticUpdateOptions( + queryClient, + optimisticUpdatesDefinitions + ); + } + + // NOTE: We decided to hide React Query's extra mutation features (e.g., + // isLoading, onSuccess and onError callbacks, synchronous mutate) and only + // expose a simple async function whose API matches the original Action. + // We did this to avoid cluttering the API with stuff we're not sure we need + // yet (e.g., isLoading), to postpone the action vs mutation dilemma, and to + // clearly separate our opinionated API from React Query's lower-level + // advanced API (which users can also use) + const mutation = useMutation(mutationFn, options); + return (args) => mutation.mutateAsync(args); +} + +/** + * An internal (undocumented, private, desugared) way of defining optimistic updates. + */ +type InternalOptimisticUpdateDefinition = { + getQueryKey: (item: ActionInput) => QueryKey; + updateQuery: UpdateQuery; +}; + +/** + * An UpdateQuery function "instantiated" with a specific item. It only takes + * the current state of the cache and returns the desired (new) state of the + * cache. + */ +type SpecificUpdateQuery = (oldData: CachedData) => CachedData; + +/** + * A specific, "instantiated" optimistic update definition which contains a + * fully-constructed query key and a specific update function. + */ +type SpecificOptimisticUpdateDefinition = { + queryKey: QueryKey; + updateQuery: SpecificUpdateQuery; +}; + +type InternalAction = Action & { + internal( + item: Input, + optimisticUpdateDefinitions: SpecificOptimisticUpdateDefinition[] + ): Promise; +}; + +/** + * Translates/Desugars a public optimistic update definition object into a + * definition object our system uses internally. + * + * @param publicOptimisticUpdateDefinition An optimistic update definition + * object that's a part of the public API: + * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * @returns An internally-used optimistic update definition object. + */ +function translateToInternalDefinition( + publicOptimisticUpdateDefinition: OptimisticUpdateDefinition +): InternalOptimisticUpdateDefinition { + const { getQuerySpecifier, updateQuery } = publicOptimisticUpdateDefinition; + + const definitionErrors = []; + if (typeof getQuerySpecifier !== "function") { + definitionErrors.push("`getQuerySpecifier` is not a function."); + } + if (typeof updateQuery !== "function") { + definitionErrors.push("`updateQuery` is not a function."); + } + if (definitionErrors.length) { + throw new TypeError( + `Invalid optimistic update definition: ${definitionErrors.join(", ")}.` + ); + } + + return { + getQueryKey: (item) => getRqQueryKeyFromSpecifier(getQuerySpecifier(item)), + updateQuery, + }; +} + +/** + * Creates a function that performs an action while telling it about the + * optimistic updates it caused. + * + * @param actionFn The Wasp Action. + * @param optimisticUpdateDefinitions The optimisitc updates the action causes. + * @returns An decorated action which performs optimistic updates. + */ +function makeOptimisticUpdateMutationFn( + actionFn: Action, + optimisticUpdateDefinitions: InternalOptimisticUpdateDefinition< + Input, + CachedData + >[] +): typeof actionFn { + return function performActionWithOptimisticUpdates(item) { + const specificOptimisticUpdateDefinitions = optimisticUpdateDefinitions.map( + (generalDefinition) => + getOptimisticUpdateDefinitionForSpecificItem(generalDefinition, item) + ); + return (actionFn as InternalAction).internal( + item, + specificOptimisticUpdateDefinitions + ); + }; +} + +/** + * Given a ReactQuery query client and our internal definition of optimistic + * updates, this function constructs an object describing those same optimistic + * updates in a format we can pass into React Query's useMutation hook. In other + * words, it translates our optimistic updates definition into React Query's + * optimistic updates definition. Check their docs for details: + * https://tanstack.com/query/v4/docs/guides/optimistic-updates?from=reactQueryV3&original=https://react-query-v3.tanstack.com/guides/optimistic-updates + * + * @param queryClient The QueryClient instance used by React Query. + * @param optimisticUpdateDefinitions A list containing internal optimistic + * updates definition objects (i.e., a list where each object carries the + * instructions for performing particular optimistic update). + * @returns An object containing 'onMutate' and 'onError' functions + * corresponding to the given optimistic update definitions (check the docs + * linked above for details). + */ +function makeRqOptimisticUpdateOptions( + queryClient: QueryClient, + optimisticUpdateDefinitions: InternalOptimisticUpdateDefinition< + ActionInput, + CachedData + >[] +): Pick { + async function onMutate(item) { + const specificOptimisticUpdateDefinitions = optimisticUpdateDefinitions.map( + (generalDefinition) => + getOptimisticUpdateDefinitionForSpecificItem(generalDefinition, item) + ); + + // Cancel any outgoing refetches (so they don't overwrite our optimistic update). + // Theoretically, we can be a bit faster. Instead of awaiting the + // cancellation of all queries, we could cancel and update them in parallel. + // However, awaiting cancellation hasn't yet proven to be a performance bottleneck. + await Promise.all( + specificOptimisticUpdateDefinitions.map(({ queryKey }) => + queryClient.cancelQueries(queryKey) + ) + ); + + // We're using a Map to correctly serialize query keys that contain objects. + const previousData = new Map(); + specificOptimisticUpdateDefinitions.forEach(({ queryKey, updateQuery }) => { + // Snapshot the currently cached value. + const previousDataForQuery: CachedData = + queryClient.getQueryData(queryKey); + + // Attempt to optimistically update the cache using the new value. + try { + queryClient.setQueryData(queryKey, updateQuery); + } catch (e) { + console.error( + "The `updateQuery` function threw an exception, skipping optimistic update:" + ); + console.error(e); + } + + // Remember the snapshotted value to restore in case of an error. + previousData.set(queryKey, previousDataForQuery); + }); + + return { previousData }; + } + + function onError(_err, _item, context) { + // All we do in case of an error is roll back all optimistic updates. We ensure + // not to do anything else because React Query rethrows the error. This allows + // the programmer to handle the error as they usually would (i.e., we want the + // error handling to work as it would if the programmer wasn't using optimistic + // updates). + context.previousData.forEach(async (data, queryKey) => { + await queryClient.cancelQueries(queryKey); + queryClient.setQueryData(queryKey, data); + }); + } + + return { + onMutate, + onError, + }; +} + +/** + * Constructs the definition for optimistically updating a specific item. It + * uses a closure over the updated item to construct an item-specific query key + * (e.g., useful when the query key depends on an ID). + * + * @param optimisticUpdateDefinition The general, "uninstantiated" optimistic + * update definition with a function for constructing the query key. + * @param item The item triggering the Action/optimistic update (i.e., the + * argument passed to the Action). + * @returns A specific optimistic update definition which corresponds to the + * provided definition and closes over the provided item. + */ +function getOptimisticUpdateDefinitionForSpecificItem( + optimisticUpdateDefinition: InternalOptimisticUpdateDefinition< + ActionInput, + CachedData + >, + item: ActionInput +): SpecificOptimisticUpdateDefinition { + const { getQueryKey, updateQuery } = optimisticUpdateDefinition; + return { + queryKey: getQueryKey(item), + updateQuery: (old) => updateQuery(item, old), + }; +} + +/** + * Translates a Wasp query specifier to a query cache key used by React Query. + * + * @param querySpecifier A query specifier that's a part of the public API: + * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * @returns A cache key React Query internally uses for addressing queries. + */ +function getRqQueryKeyFromSpecifier( + querySpecifier: QuerySpecifier +): QueryKey { + const [queryFn, ...otherKeys] = querySpecifier; + return [...(queryFn as any).queryCacheKey, ...otherKeys]; +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/queries/core.d.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/queries/core.d.ts new file mode 100644 index 0000000000..ddbb4f2b8e --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/queries/core.d.ts @@ -0,0 +1,23 @@ +import { type Query } from '..' +import { Route } from 'wasp/types' +import type { Expand, _Awaited, _ReturnType } from 'wasp/universal/types' + +export function createQuery( + queryRoute: string, + entitiesUsed: any[] +): QueryFor + +export function addMetadataToQuery( + query: (...args: any[]) => Promise, + metadata: { + relativeQueryPath: string + queryRoute: Route + entitiesUsed: string[] + } +): void + +type QueryFor = Expand< + Query[0], _Awaited<_ReturnType>> +> + +type GenericBackendQuery = (args: never, context: any) => unknown diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/queries/core.js b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/queries/core.js new file mode 100644 index 0000000000..616fb82958 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/queries/core.js @@ -0,0 +1,30 @@ +import { callOperation, makeOperationRoute } from 'wasp/operations' +import { + addResourcesUsedByQuery, + getActiveOptimisticUpdates, +} from 'wasp/operations/resources' + +export function createQuery(relativeQueryPath, entitiesUsed) { + const queryRoute = makeOperationRoute(relativeQueryPath) + + async function query(queryKey, queryArgs) { + const serverResult = await callOperation(queryRoute, queryArgs) + return getActiveOptimisticUpdates(queryKey).reduce( + (result, update) => update(result), + serverResult, + ) + } + + addMetadataToQuery(query, { relativeQueryPath, queryRoute, entitiesUsed }) + + return query +} + +export function addMetadataToQuery( + query, + { relativeQueryPath, queryRoute, entitiesUsed } +) { + query.queryCacheKey = [relativeQueryPath] + query.route = queryRoute + addResourcesUsedByQuery(query.queryCacheKey, entitiesUsed) +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/queries/index.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/queries/index.ts new file mode 100644 index 0000000000..a03221553d --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/queries/index.ts @@ -0,0 +1,6 @@ +import { createQuery } from './core' +import { GetTasks } from 'wasp/server/queries' + +export const getTasks = createQuery('operations/get-tasks', ['Task']) + +export { addMetadataToQuery } from './core' diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/queryClient.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/queryClient.ts new file mode 100644 index 0000000000..448be4c5ce --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/rpc/queryClient.ts @@ -0,0 +1,33 @@ +import { QueryClient } from "@tanstack/react-query"; + +type QueryClientConfig = object; + +const defaultQueryClientConfig = {}; + +let queryClientConfig: QueryClientConfig, + resolveQueryClientInitialized: (...args: any[]) => any, + isQueryClientInitialized: boolean; + +export const queryClientInitialized: Promise = new Promise( + (resolve) => { + resolveQueryClientInitialized = resolve; + } +); + +export function configureQueryClient(config: QueryClientConfig): void { + if (isQueryClientInitialized) { + throw new Error( + "Attempted to configure the QueryClient after initialization" + ); + } + + queryClientConfig = config; +} + +export function initializeQueryClient(): void { + const queryClient = new QueryClient( + queryClientConfig ?? defaultQueryClientConfig + ); + isQueryClientInitialized = true; + resolveQueryClientInitialized(queryClient); +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/index.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/index.ts new file mode 100644 index 0000000000..ad1de55e09 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/index.ts @@ -0,0 +1,101 @@ +import { type Expand } from 'wasp/universal/types'; +import { type Request, type Response } from 'express' +import { type ParamsDictionary as ExpressParams, type Query as ExpressQuery } from 'express-serve-static-core' +import prisma from "wasp/server/dbClient" +import { + type User, + type Auth, + type AuthIdentity, +} from "wasp/entities" +import { + type EmailProviderData, + type UsernameProviderData, + type OAuthProviderData, + // todo(filip): marker +} from 'wasp/auth/utils' +import { type _Entity } from "./taggedEntities" +import { type Payload } from "./serialization"; + +export * from "./taggedEntities" +export * from "./serialization" + +export type Query = + Operation + +export type Action = + Operation + +export type AuthenticatedQuery = + AuthenticatedOperation + +export type AuthenticatedAction = + AuthenticatedOperation + +type AuthenticatedOperation = ( + args: Input, + context: ContextWithUser, +) => Output | Promise + +export type AuthenticatedApi< + Entities extends _Entity[], + Params extends ExpressParams, + ResBody, + ReqBody, + ReqQuery extends ExpressQuery, + Locals extends Record +> = ( + req: Request, + res: Response, + context: ContextWithUser, +) => void + +type Operation = ( + args: Input, + context: Context, +) => Output | Promise + +export type Api< + Entities extends _Entity[], + Params extends ExpressParams, + ResBody, + ReqBody, + ReqQuery extends ExpressQuery, + Locals extends Record +> = ( + req: Request, + res: Response, + context: Context, +) => void + +type EntityMap = { + [EntityName in Entities[number]["_entityName"]]: PrismaDelegate[EntityName] +} + +export type PrismaDelegate = { + "User": typeof prisma.user, + "Task": typeof prisma.task, +} + +type Context = Expand<{ + entities: Expand> +}> + +type ContextWithUser = Expand & { user?: SanitizedUser }> + +// TODO: This type must match the logic in core/auth.js (if we remove the +// password field from the object there, we must do the same here). Ideally, +// these two things would live in the same place: +// https://github.com/wasp-lang/wasp/issues/965 + +export type DeserializedAuthEntity = Expand & { + providerData: Omit | Omit | OAuthProviderData +}> + +export type SanitizedUser = User & { + auth: Auth & { + identities: DeserializedAuthEntity[] + } | null +} + +// todo(filip): marker +export type { ProviderName } from 'wasp/auth/utils' diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/serialization.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/serialization.ts new file mode 100644 index 0000000000..595b5ba69f --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/serialization.ts @@ -0,0 +1,43 @@ +export type Payload = void | SuperJSONValue + +// The part below was copied from SuperJSON and slightly modified: +// https://github.com/blitz-js/superjson/blob/ae7dbcefe5d3ece5b04be0c6afe6b40f3a44a22a/src/types.ts +// +// We couldn't use SuperJSON's types directly because: +// 1. They aren't exported publicly. +// 2. They have a werid quirk that turns `SuperJSONValue` into `any`. +// See why here: +// https://github.com/blitz-js/superjson/pull/36#issuecomment-669239876 +// +// We changed the code as little as possible to make future comparisons easier. +export type JSONValue = PrimitiveJSONValue | JSONArray | JSONObject + +export interface JSONObject { + [key: string]: JSONValue +} + +type PrimitiveJSONValue = string | number | boolean | undefined | null + +interface JSONArray extends Array {} + +type SerializableJSONValue = + | Symbol + | Set + | Map + | undefined + | bigint + | Date + | RegExp + +// Here's where we excluded `ClassInstance` (which was `any`) from the union. +type SuperJSONValue = + | JSONValue + | SerializableJSONValue + | SuperJSONArray + | SuperJSONObject + +interface SuperJSONArray extends Array {} + +interface SuperJSONObject { + [key: string]: SuperJSONValue +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/taggedEntities.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/taggedEntities.ts new file mode 100644 index 0000000000..3331b3f893 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/taggedEntities.ts @@ -0,0 +1,22 @@ +// Wasp internally uses the types defined in this file for typing entity maps in +// operation contexts. +// +// We must explicitly tag all entities with their name to avoid issues with +// structural typing. See https://github.com/wasp-lang/wasp/pull/982 for details. +import { + type Entity, + type EntityName, + type User, + type Task, +} from '../../entities' + +export type _User = WithName +export type _Task = WithName + +export type _Entity = + | _User + | _Task + | never + +type WithName = + E & { _entityName: Name } diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/actions/index.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/actions/index.ts new file mode 100644 index 0000000000..5f9800cf8d --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/actions/index.ts @@ -0,0 +1,39 @@ +import prisma from 'wasp/server/dbClient.js' +import { + updateTask as updateTaskUser, + createTask as createTaskUser, + deleteTasks as deleteTasksUser, +} from 'wasp/ext-src/actions.js' + +export type UpdateTask = typeof updateTask + +export const updateTask = async (args, context) => { + return (updateTaskUser as any)(args, { + ...context, + entities: { + Task: prisma.task, + }, + }) +} + +export type CreateTask = typeof createTask + +export const createTask = async (args, context) => { + return (createTaskUser as any)(args, { + ...context, + entities: { + Task: prisma.task, + }, + }) +} + +export type DeleteTasks = typeof deleteTasks + +export const deleteTasks = async (args, context) => { + return (deleteTasksUser as any)(args, { + ...context, + entities: { + Task: prisma.task, + }, + }) +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/actions/types.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/actions/types.ts new file mode 100644 index 0000000000..8e03d4963e --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/actions/types.ts @@ -0,0 +1,34 @@ +import { + type _Task, + type AuthenticatedAction, + type Payload, +} from '../_types' + +export type CreateTask = + AuthenticatedAction< + [ + _Task, + ], + Input, + Output + > + +export type UpdateTask = + AuthenticatedAction< + [ + _Task, + ], + Input, + Output + > + +export type DeleteTasks = + AuthenticatedAction< + [ + _Task, + ], + Input, + Output + > + + diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/dbClient.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/dbClient.ts new file mode 100644 index 0000000000..66e7801be3 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/dbClient.ts @@ -0,0 +1,12 @@ +import Prisma from '@prisma/client' + + +const createDbClient = () => { + const prismaClient = new Prisma.PrismaClient() + + return prismaClient +} + +const dbClient = createDbClient() + +export default dbClient diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/queries/index.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/queries/index.ts new file mode 100644 index 0000000000..3c49adc9dc --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/queries/index.ts @@ -0,0 +1,13 @@ +import prisma from 'wasp/server/dbClient.js' +import { getTasks as getTasksUser } from 'wasp/ext-src/queries.js' + +export type GetTasks = typeof getTasksUser + +export const getTasks = async (args, context) => { + return (getTasksUser as any)(args, { + ...context, + entities: { + Task: prisma.task, + }, + }) +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/queries/types.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/queries/types.ts new file mode 100644 index 0000000000..0617ad4559 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/queries/types.ts @@ -0,0 +1,6 @@ +import { type _Task, type AuthenticatedQuery, type Payload } from "../_types"; + +export type GetTasks< + Input extends Payload = never, + Output extends Payload = Payload +> = AuthenticatedQuery<[_Task], Input, Output>; diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/utils.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/utils.ts new file mode 100644 index 0000000000..a930149d08 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/utils.ts @@ -0,0 +1,66 @@ +import crypto from 'crypto' +import { Request, Response, NextFunction } from 'express' + +import { readdir } from 'fs' +import { dirname } from 'path' +import { fileURLToPath } from 'url' + +import { type SanitizedUser } from './_types/index.js' + +type RequestWithExtraFields = Request & { + user?: SanitizedUser +} + +/** + * Decorator for async express middleware that handles promise rejections. + * @param {Func} middleware - Express middleware function. + * @returns Express middleware that is exactly the same as the given middleware but, + * if given middleware returns promise, reject of that promise will be correctly handled, + * meaning that error will be forwarded to next(). + */ +export const handleRejection = ( + middleware: ( + req: RequestWithExtraFields, + res: Response, + next: NextFunction + ) => any +) => +async (req: RequestWithExtraFields, res: Response, next: NextFunction) => { + try { + await middleware(req, res, next) + } catch (error) { + next(error) + } +} + +export const sleep = (ms: number): Promise => new Promise((r) => setTimeout(r, ms)) + +export function getDirPathFromFileUrl(fileUrl: string): string { + return fileURLToPath(dirname(fileUrl)) +} + +export async function importJsFilesFromDir( + pathToDir: string, + whitelistedFileNames: string[] | null = null +): Promise { + return new Promise((resolve, reject) => { + readdir(pathToDir, async (err, files) => { + if (err) { + return reject(err) + } + const importPromises = files + .filter((file) => file.endsWith('.js') && isWhitelistedFileName(file)) + .map((file) => import(`${pathToDir}/${file}`)) + resolve(Promise.all(importPromises)) + }) + }) + + function isWhitelistedFileName(fileName: string) { + // No whitelist means all files are whitelisted + if (!Array.isArray(whitelistedFileNames)) { + return true + } + + return whitelistedFileNames.some((whitelistedFileName) => fileName === whitelistedFileName) + } +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/types/index.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/types/index.ts new file mode 100644 index 0000000000..982b766e37 --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/types/index.ts @@ -0,0 +1,9 @@ +// NOTE: This is enough to cover Operations and our APIs (src/Wasp/AppSpec/Api.hs). +export enum HttpMethod { + Get = 'GET', + Post = 'POST', + Put = 'PUT', + Delete = 'DELETE', +} + +export type Route = { method: HttpMethod; path: string } diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/universal/types.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/universal/types.ts new file mode 100644 index 0000000000..8cadbd740d --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/universal/types.ts @@ -0,0 +1,31 @@ +// This is a helper type used exclusively for DX purposes. It's a No-op for the +// compiler, but expands the type's representatoin in IDEs (i.e., inlines all +// type constructors) to make it more readable for the user. +// +// It expands this SO answer to functions: https://stackoverflow.com/a/57683652 +export type Expand = T extends (...args: infer A) => infer R + ? (...args: A) => R + : T extends infer O + ? { [K in keyof O]: O[K] } + : never + +// TypeScript's native Awaited type exhibits strange behavior in VS Code (see +// https://github.com/wasp-lang/wasp/pull/1090#discussion_r1159687537 for +// details). Until it's fixed, we're using our own type for this. +// +// TODO: investigate further. This most likely has something to do with an +// unsatisfied 'extends' constraints. A mismatch is probably happening with +// function parameter types and/or return types (check '_ReturnType' below for +// more). +export type _Awaited = T extends Promise + ? _Awaited + : T + +// TypeScript's native ReturnType does not work for functions of type '(...args: +// never[]) => unknown' (and that's what operations currently use). +// +// TODO: investigate how to properly specify the 'extends' constraint for function +// type (i.e., any vs never and unknown) and stick with that. Take DX into +// consideration. +export type _ReturnType unknown> = + T extends (...args: never[]) => infer R ? R : never diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/universal/url.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/universal/url.ts new file mode 100644 index 0000000000..d21c06c65c --- /dev/null +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/universal/url.ts @@ -0,0 +1,3 @@ +export function stripTrailingSlash(url?: string): string | undefined { + return url?.replace(/\/$/, ""); +} From 9bd044e6a0a22dc7843edcc22b14344a3935f61e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Wed, 10 Jan 2024 17:38:34 +0100 Subject: [PATCH 34/54] Update restructuring sdk base for cleaner diffs --- .../20230816092617_migracijone/migration.sql | 18 - .../20240110163721_init/migration.sql | 34 + .../todo-typescript/package-lock.json | 1277 +++++++++++++++++ 3 files changed, 1311 insertions(+), 18 deletions(-) delete mode 100644 waspc/examples/todo-typescript/migrations/20230816092617_migracijone/migration.sql create mode 100644 waspc/examples/todo-typescript/migrations/20240110163721_init/migration.sql create mode 100644 waspc/examples/todo-typescript/package-lock.json diff --git a/waspc/examples/todo-typescript/migrations/20230816092617_migracijone/migration.sql b/waspc/examples/todo-typescript/migrations/20230816092617_migracijone/migration.sql deleted file mode 100644 index 7b62627672..0000000000 --- a/waspc/examples/todo-typescript/migrations/20230816092617_migracijone/migration.sql +++ /dev/null @@ -1,18 +0,0 @@ --- CreateTable -CREATE TABLE "User" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "username" TEXT NOT NULL, - "password" TEXT NOT NULL -); - --- CreateTable -CREATE TABLE "Task" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "description" TEXT NOT NULL, - "isDone" BOOLEAN NOT NULL DEFAULT false, - "userId" INTEGER, - CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE -); - --- CreateIndex -CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); diff --git a/waspc/examples/todo-typescript/migrations/20240110163721_init/migration.sql b/waspc/examples/todo-typescript/migrations/20240110163721_init/migration.sql new file mode 100644 index 0000000000..0ea8e16da6 --- /dev/null +++ b/waspc/examples/todo-typescript/migrations/20240110163721_init/migration.sql @@ -0,0 +1,34 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT +); + +-- CreateTable +CREATE TABLE "Task" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "description" TEXT NOT NULL, + "isDone" BOOLEAN NOT NULL DEFAULT false, + "userId" INTEGER NOT NULL, + CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Auth" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" INTEGER, + CONSTRAINT "Auth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "AuthIdentity" ( + "providerName" TEXT NOT NULL, + "providerUserId" TEXT NOT NULL, + "providerData" TEXT NOT NULL DEFAULT '{}', + "authId" TEXT NOT NULL, + + PRIMARY KEY ("providerName", "providerUserId"), + CONSTRAINT "AuthIdentity_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "Auth_userId_key" ON "Auth"("userId"); diff --git a/waspc/examples/todo-typescript/package-lock.json b/waspc/examples/todo-typescript/package-lock.json new file mode 100644 index 0000000000..c438c1ae41 --- /dev/null +++ b/waspc/examples/todo-typescript/package-lock.json @@ -0,0 +1,1277 @@ +{ + "name": "prototype", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "prototype", + "dependencies": { + "react": "^18.2.0", + "wasp": "file:.wasp/out/sdk/wasp" + }, + "devDependencies": { + "@types/react": "^18.0.37", + "prisma": "4.16.2" + } + }, + ".wasp/out/sdk/wasp": { + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@prisma/client": "4.16.2", + "@stitches/react": "^1.2.8", + "@tanstack/react-query": "^4.29.0", + "@types/express-serve-static-core": "^4.17.13", + "axios": "^1.4.0", + "express": "~4.18.1", + "jsonwebtoken": "^8.5.1", + "mitt": "3.0.0", + "prisma": "4.16.2", + "react": "^18.2.0", + "react-hook-form": "^7.45.4", + "react-router-dom": "^5.3.3", + "secure-password": "^4.0.0", + "superjson": "^1.12.2" + }, + "devDependencies": {} + }, + "node_modules/@babel/runtime": { + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz", + "integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@prisma/client": { + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.16.2.tgz", + "integrity": "sha512-qCoEyxv1ZrQ4bKy39GnylE8Zq31IRmm8bNhNbZx7bF2cU5aiCCnSa93J2imF88MBjn7J9eUQneNxUQVJdl/rPQ==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines-version": "4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81" + }, + "engines": { + "node": ">=14.17" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/engines": { + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.16.2.tgz", + "integrity": "sha512-vx1nxVvN4QeT/cepQce68deh/Turxy5Mr+4L4zClFuK1GlxN3+ivxfuv+ej/gvidWn1cE1uAhW7ALLNlYbRUAw==", + "hasInstallScript": true + }, + "node_modules/@prisma/engines-version": { + "version": "4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81.tgz", + "integrity": "sha512-q617EUWfRIDTriWADZ4YiWRZXCa/WuhNgLTVd+HqWLffjMSPzyM5uOWoauX91wvQClSKZU4pzI4JJLQ9Kl62Qg==" + }, + "node_modules/@stitches/react": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@stitches/react/-/react-1.2.8.tgz", + "integrity": "sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA==", + "peerDependencies": { + "react": ">= 16.3.0" + } + }, + "node_modules/@tanstack/query-core": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", + "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz", + "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==", + "dependencies": { + "@tanstack/query-core": "4.36.1", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.41", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", + "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/node": { + "version": "20.10.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz", + "integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.11", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", + "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, + "node_modules/@types/react": { + "version": "18.2.47", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.47.tgz", + "integrity": "sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "dependencies": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=4", + "npm": ">=1.4.28" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mitt": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.0.tgz", + "integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/nanoassert": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz", + "integrity": "sha512-C40jQ3NzfkP53NsO8kEOFd79p4b9kDXQMwgiY1z8ZwrDZgUyom0AHwGegF4Dm99L+YoYhuaB0ceerUcXmqr1rQ==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/prisma": { + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.16.2.tgz", + "integrity": "sha512-SYCsBvDf0/7XSJyf2cHTLjLeTLVXYfqp7pG5eEVafFLeT0u/hLFz/9W196nDRGUOo1JfPatAEb+uEnTQImQC1g==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "4.16.2" + }, + "bin": { + "prisma": "build/index.js", + "prisma2": "build/index.js" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.49.3", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.3.tgz", + "integrity": "sha512-foD6r3juidAT1cOZzpmD/gOKt7fRsDhXXZ0y28+Al1CHgX+AY1qIN9VSIIItXRq1dN68QrRwl1ORFlwjBaAqeQ==", + "engines": { + "node": ">=18", + "pnpm": "8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-router": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", + "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-router-dom": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", + "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.3.4", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-router/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/secure-password": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/secure-password/-/secure-password-4.0.0.tgz", + "integrity": "sha512-B268T/tx+hq7q85KH6gonEqK/lhrLhNtzYzqojuMtBPVFBtwiIwxqF+4yr9POsJu5cIxbJyM66eYfXZiPZUXRA==", + "dependencies": { + "nanoassert": "^1.0.0", + "sodium-native": "^3.1.1" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sodium-native": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-3.4.1.tgz", + "integrity": "sha512-PaNN/roiFWzVVTL6OqjzYct38NSXewdl2wz8SRB51Br/MLIJPrbM3XexhVWkq7D3UWMysfrhKVf1v1phZq6MeQ==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/superjson": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.13.3.tgz", + "integrity": "sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", + "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wasp": { + "resolved": ".wasp/out/sdk/wasp", + "link": true + } + } +} From 783870728600593eac303af5354ff0d53f9a008d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Wed, 10 Jan 2024 17:42:06 +0100 Subject: [PATCH 35/54] Update cleanstart script for cleaner diffs --- waspc/examples/todo-typescript/cleanstart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/waspc/examples/todo-typescript/cleanstart b/waspc/examples/todo-typescript/cleanstart index 3c7987deb5..8cf1609407 100755 --- a/waspc/examples/todo-typescript/cleanstart +++ b/waspc/examples/todo-typescript/cleanstart @@ -1 +1,2 @@ -rm -r .wasp node_modules package-lock.json migrations; cabal run wasp-cli db migrate-dev -- --name init && ./fix; cabal run wasp-cli start +cabal run wasp-cli reset; cabal run wasp-cli db migrate-dev -- --name init && ./fix; cabal run wasp-cli start + From 37d0662f56934e572232794dfc3aaca3bd670364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Wed, 10 Jan 2024 17:47:07 +0100 Subject: [PATCH 36/54] Update cleanstart script for cleaner diffs --- waspc/examples/todo-typescript/cleanstart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/waspc/examples/todo-typescript/cleanstart b/waspc/examples/todo-typescript/cleanstart index 8cf1609407..33e58b6caa 100755 --- a/waspc/examples/todo-typescript/cleanstart +++ b/waspc/examples/todo-typescript/cleanstart @@ -1,2 +1,3 @@ -cabal run wasp-cli reset; cabal run wasp-cli db migrate-dev -- --name init && ./fix; cabal run wasp-cli start +#!/bin/bash +cabal run wasp-cli reset; cabal run wasp-cli db migrate-dev && ./fix; cabal run wasp-cli start From cf00106b6c19f14279ae7a41493fc2394dc6d783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Wed, 10 Jan 2024 17:20:57 +0100 Subject: [PATCH 37/54] Set up proper SDK generation and generate api module --- .../Generator/templates/react-app/src/api.ts | 103 ------------------ .../{react-app/src => sdk}/api/events.ts | 0 .../templates/sdk/{wasp => }/api/index.ts | 6 +- .../sdk/{wasp => }/auth/forms/Auth.tsx | 0 .../sdk/{wasp => }/auth/forms/Login.tsx | 0 .../sdk/{wasp => }/auth/forms/Signup.tsx | 0 .../{wasp => }/auth/forms/internal/Form.tsx | 0 .../auth/forms/internal/Message.tsx | 0 .../forms/internal/common/LoginSignupForm.tsx | 0 .../useUsernameAndPassword.ts | 0 .../sdk/{wasp => }/auth/forms/types.ts | 0 .../sdk/{wasp => }/auth/helpers/user.ts | 0 .../templates/sdk/{wasp => }/auth/login.ts | 0 .../templates/sdk/{wasp => }/auth/logout.ts | 0 .../auth/pages/createAuthRequiredPage.jsx | 0 .../sdk/{wasp => }/auth/providers/types.ts | 0 .../templates/sdk/{wasp => }/auth/signup.ts | 0 .../templates/sdk/{wasp => }/auth/types.ts | 0 .../templates/sdk/{wasp => }/auth/useAuth.ts | 0 .../templates/sdk/{wasp => }/auth/user.ts | 0 .../templates/sdk/{wasp => }/auth/utils.ts | 0 .../sdk/{wasp => }/auth/validation.ts | 0 .../sdk/{wasp => }/core/AuthError.js | 0 .../sdk/{wasp => }/core/HttpError.js | 0 .../templates/sdk/{wasp => }/core/auth.js | 0 .../templates/sdk/{wasp => }/core/config.js | 0 .../sdk/{wasp => }/core/stitches.config.js | 0 .../templates/sdk/{wasp => }/core/storage.ts | 0 .../sdk/{wasp => }/entities/index.ts | 0 .../sdk/{wasp => }/ext-src/actions.ts | 0 .../sdk/{wasp => }/ext-src/queries.ts | 0 .../sdk/{wasp => }/operations/index.ts | 0 .../sdk/{wasp => }/operations/resources.js | 0 .../operations/updateHandlersMap.js | 0 .../sdk/{wasp => }/rpc/actions/core.d.ts | 0 .../sdk/{wasp => }/rpc/actions/core.js | 0 .../sdk/{wasp => }/rpc/actions/index.ts | 0 .../templates/sdk/{wasp => }/rpc/index.ts | 0 .../sdk/{wasp => }/rpc/queries/core.d.ts | 0 .../sdk/{wasp => }/rpc/queries/core.js | 0 .../sdk/{wasp => }/rpc/queries/index.ts | 0 .../sdk/{wasp => }/rpc/queryClient.ts | 0 .../sdk/{wasp => }/server/_types/index.ts | 0 .../{wasp => }/server/_types/serialization.ts | 0 .../server/_types/taggedEntities.ts | 0 .../sdk/{wasp => }/server/actions/index.ts | 0 .../sdk/{wasp => }/server/actions/types.ts | 0 .../sdk/{wasp => }/server/dbClient.ts | 0 .../sdk/{wasp => }/server/queries/index.ts | 0 .../sdk/{wasp => }/server/queries/types.ts | 0 .../templates/sdk/{wasp => }/server/utils.ts | 0 .../templates/sdk/{wasp => }/types/index.ts | 0 .../sdk/{wasp => }/universal/types.ts | 0 .../templates/sdk/{wasp => }/universal/url.ts | 0 .../templates/sdk/wasp/api/events.ts | 11 -- .../.wasp/out/sdk/wasp/api/index.ts | 6 +- waspc/src/Wasp/Generator/SdkGenerator.hs | 53 +++++++-- waspc/src/Wasp/Generator/WebAppGenerator.hs | 3 - 58 files changed, 51 insertions(+), 131 deletions(-) delete mode 100644 waspc/data/Generator/templates/react-app/src/api.ts rename waspc/data/Generator/templates/{react-app/src => sdk}/api/events.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/api/index.ts (95%) rename waspc/data/Generator/templates/sdk/{wasp => }/auth/forms/Auth.tsx (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/auth/forms/Login.tsx (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/auth/forms/Signup.tsx (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/auth/forms/internal/Form.tsx (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/auth/forms/internal/Message.tsx (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/auth/forms/internal/common/LoginSignupForm.tsx (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/auth/forms/types.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/auth/helpers/user.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/auth/login.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/auth/logout.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/auth/pages/createAuthRequiredPage.jsx (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/auth/providers/types.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/auth/signup.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/auth/types.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/auth/useAuth.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/auth/user.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/auth/utils.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/auth/validation.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/core/AuthError.js (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/core/HttpError.js (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/core/auth.js (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/core/config.js (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/core/stitches.config.js (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/core/storage.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/entities/index.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/ext-src/actions.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/ext-src/queries.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/operations/index.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/operations/resources.js (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/operations/updateHandlersMap.js (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/rpc/actions/core.d.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/rpc/actions/core.js (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/rpc/actions/index.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/rpc/index.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/rpc/queries/core.d.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/rpc/queries/core.js (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/rpc/queries/index.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/rpc/queryClient.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/server/_types/index.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/server/_types/serialization.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/server/_types/taggedEntities.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/server/actions/index.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/server/actions/types.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/server/dbClient.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/server/queries/index.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/server/queries/types.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/server/utils.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/types/index.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/universal/types.ts (100%) rename waspc/data/Generator/templates/sdk/{wasp => }/universal/url.ts (100%) delete mode 100644 waspc/data/Generator/templates/sdk/wasp/api/events.ts diff --git a/waspc/data/Generator/templates/react-app/src/api.ts b/waspc/data/Generator/templates/react-app/src/api.ts deleted file mode 100644 index d7532f65c6..0000000000 --- a/waspc/data/Generator/templates/react-app/src/api.ts +++ /dev/null @@ -1,103 +0,0 @@ -import axios, { type AxiosError } from 'axios' - -import config from './config' -import { storage } from './storage' -import { apiEventsEmitter } from './api/events' - -const api = axios.create({ - baseURL: config.apiUrl, -}) - -const WASP_APP_AUTH_TOKEN_NAME = 'authToken' - -let authToken = storage.get(WASP_APP_AUTH_TOKEN_NAME) as string | undefined - -export function setAuthToken(token: string): void { - authToken = token - storage.set(WASP_APP_AUTH_TOKEN_NAME, token) - apiEventsEmitter.emit('authToken.set') -} - -export function getAuthToken(): string | undefined { - return authToken -} - -export function clearAuthToken(): void { - authToken = undefined - storage.remove(WASP_APP_AUTH_TOKEN_NAME) - apiEventsEmitter.emit('authToken.clear') -} - -export function removeLocalUserData(): void { - authToken = undefined - storage.clear() - apiEventsEmitter.emit('authToken.clear') -} - -api.interceptors.request.use((request) => { - if (authToken) { - request.headers['Authorization'] = `Bearer ${authToken}` - } - return request -}) - -api.interceptors.response.use(undefined, (error) => { - if (error.response?.status === 401) { - clearAuthToken() - } - return Promise.reject(error) -}) - -// This handler will run on other tabs (not the active one calling API functions), -// and will ensure they know about auth token changes. -// Ref: https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event -// "Note: This won't work on the same page that is making the changes — it is really a way -// for other pages on the domain using the storage to sync any changes that are made." -window.addEventListener('storage', (event) => { - if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_TOKEN_NAME)) { - if (!!event.newValue) { - authToken = event.newValue - apiEventsEmitter.emit('authToken.set') - } else { - authToken = undefined - apiEventsEmitter.emit('authToken.clear') - } - } -}) - -/** - * Takes an error returned by the app's API (as returned by axios), and transforms into a more - * standard format to be further used by the client. It is also assumed that given API - * error has been formatted as implemented by HttpError on the server. - */ -export function handleApiError(error: AxiosError<{ message?: string, data?: unknown }>): void { - if (error?.response) { - // If error came from HTTP response, we capture most informative message - // and also add .statusCode information to it. - // If error had JSON response, we assume it is of format { message, data } and - // add that info to the error. - // TODO: We might want to use HttpError here instead of just Error, since - // HttpError is also used on server to throw errors like these. - // That would require copying HttpError code to web-app also and using it here. - const responseJson = error.response?.data - const responseStatusCode = error.response.status - throw new WaspHttpError(responseStatusCode, responseJson?.message ?? error.message, responseJson) - } else { - // If any other error, we just propagate it. - throw error - } -} - -class WaspHttpError extends Error { - statusCode: number - - data: unknown - - constructor (statusCode: number, message: string, data: unknown) { - super(message) - this.statusCode = statusCode - this.data = data - } -} - -export default api diff --git a/waspc/data/Generator/templates/react-app/src/api/events.ts b/waspc/data/Generator/templates/sdk/api/events.ts similarity index 100% rename from waspc/data/Generator/templates/react-app/src/api/events.ts rename to waspc/data/Generator/templates/sdk/api/events.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/api/index.ts b/waspc/data/Generator/templates/sdk/api/index.ts similarity index 95% rename from waspc/data/Generator/templates/sdk/wasp/api/index.ts rename to waspc/data/Generator/templates/sdk/api/index.ts index 9aad1ead59..7170ebab19 100644 --- a/waspc/data/Generator/templates/sdk/wasp/api/index.ts +++ b/waspc/data/Generator/templates/sdk/api/index.ts @@ -1,8 +1,8 @@ import axios, { type AxiosError } from 'axios' -import config from 'wasp/core/config' -import { storage } from 'wasp/core/storage' -import { apiEventsEmitter } from 'wasp/api/events' +import config from '../core/config' +import { storage } from '../core/storage' +import { apiEventsEmitter } from '../api/events' const api = axios.create({ baseURL: config.apiUrl, diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/forms/Auth.tsx b/waspc/data/Generator/templates/sdk/auth/forms/Auth.tsx similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/auth/forms/Auth.tsx rename to waspc/data/Generator/templates/sdk/auth/forms/Auth.tsx diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/forms/Login.tsx b/waspc/data/Generator/templates/sdk/auth/forms/Login.tsx similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/auth/forms/Login.tsx rename to waspc/data/Generator/templates/sdk/auth/forms/Login.tsx diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/forms/Signup.tsx b/waspc/data/Generator/templates/sdk/auth/forms/Signup.tsx similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/auth/forms/Signup.tsx rename to waspc/data/Generator/templates/sdk/auth/forms/Signup.tsx diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/Form.tsx b/waspc/data/Generator/templates/sdk/auth/forms/internal/Form.tsx similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/Form.tsx rename to waspc/data/Generator/templates/sdk/auth/forms/internal/Form.tsx diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/Message.tsx b/waspc/data/Generator/templates/sdk/auth/forms/internal/Message.tsx similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/Message.tsx rename to waspc/data/Generator/templates/sdk/auth/forms/internal/Message.tsx diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/common/LoginSignupForm.tsx b/waspc/data/Generator/templates/sdk/auth/forms/internal/common/LoginSignupForm.tsx similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/common/LoginSignupForm.tsx rename to waspc/data/Generator/templates/sdk/auth/forms/internal/common/LoginSignupForm.tsx diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts b/waspc/data/Generator/templates/sdk/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts rename to waspc/data/Generator/templates/sdk/auth/forms/internal/usernameAndPassword/useUsernameAndPassword.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/forms/types.ts b/waspc/data/Generator/templates/sdk/auth/forms/types.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/auth/forms/types.ts rename to waspc/data/Generator/templates/sdk/auth/forms/types.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/helpers/user.ts b/waspc/data/Generator/templates/sdk/auth/helpers/user.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/auth/helpers/user.ts rename to waspc/data/Generator/templates/sdk/auth/helpers/user.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/login.ts b/waspc/data/Generator/templates/sdk/auth/login.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/auth/login.ts rename to waspc/data/Generator/templates/sdk/auth/login.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/logout.ts b/waspc/data/Generator/templates/sdk/auth/logout.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/auth/logout.ts rename to waspc/data/Generator/templates/sdk/auth/logout.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/pages/createAuthRequiredPage.jsx b/waspc/data/Generator/templates/sdk/auth/pages/createAuthRequiredPage.jsx similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/auth/pages/createAuthRequiredPage.jsx rename to waspc/data/Generator/templates/sdk/auth/pages/createAuthRequiredPage.jsx diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/providers/types.ts b/waspc/data/Generator/templates/sdk/auth/providers/types.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/auth/providers/types.ts rename to waspc/data/Generator/templates/sdk/auth/providers/types.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/signup.ts b/waspc/data/Generator/templates/sdk/auth/signup.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/auth/signup.ts rename to waspc/data/Generator/templates/sdk/auth/signup.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/types.ts b/waspc/data/Generator/templates/sdk/auth/types.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/auth/types.ts rename to waspc/data/Generator/templates/sdk/auth/types.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/useAuth.ts b/waspc/data/Generator/templates/sdk/auth/useAuth.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/auth/useAuth.ts rename to waspc/data/Generator/templates/sdk/auth/useAuth.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/user.ts b/waspc/data/Generator/templates/sdk/auth/user.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/auth/user.ts rename to waspc/data/Generator/templates/sdk/auth/user.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/utils.ts b/waspc/data/Generator/templates/sdk/auth/utils.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/auth/utils.ts rename to waspc/data/Generator/templates/sdk/auth/utils.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/validation.ts b/waspc/data/Generator/templates/sdk/auth/validation.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/auth/validation.ts rename to waspc/data/Generator/templates/sdk/auth/validation.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/core/AuthError.js b/waspc/data/Generator/templates/sdk/core/AuthError.js similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/core/AuthError.js rename to waspc/data/Generator/templates/sdk/core/AuthError.js diff --git a/waspc/data/Generator/templates/sdk/wasp/core/HttpError.js b/waspc/data/Generator/templates/sdk/core/HttpError.js similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/core/HttpError.js rename to waspc/data/Generator/templates/sdk/core/HttpError.js diff --git a/waspc/data/Generator/templates/sdk/wasp/core/auth.js b/waspc/data/Generator/templates/sdk/core/auth.js similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/core/auth.js rename to waspc/data/Generator/templates/sdk/core/auth.js diff --git a/waspc/data/Generator/templates/sdk/wasp/core/config.js b/waspc/data/Generator/templates/sdk/core/config.js similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/core/config.js rename to waspc/data/Generator/templates/sdk/core/config.js diff --git a/waspc/data/Generator/templates/sdk/wasp/core/stitches.config.js b/waspc/data/Generator/templates/sdk/core/stitches.config.js similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/core/stitches.config.js rename to waspc/data/Generator/templates/sdk/core/stitches.config.js diff --git a/waspc/data/Generator/templates/sdk/wasp/core/storage.ts b/waspc/data/Generator/templates/sdk/core/storage.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/core/storage.ts rename to waspc/data/Generator/templates/sdk/core/storage.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/entities/index.ts b/waspc/data/Generator/templates/sdk/entities/index.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/entities/index.ts rename to waspc/data/Generator/templates/sdk/entities/index.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/ext-src/actions.ts b/waspc/data/Generator/templates/sdk/ext-src/actions.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/ext-src/actions.ts rename to waspc/data/Generator/templates/sdk/ext-src/actions.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/ext-src/queries.ts b/waspc/data/Generator/templates/sdk/ext-src/queries.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/ext-src/queries.ts rename to waspc/data/Generator/templates/sdk/ext-src/queries.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/operations/index.ts b/waspc/data/Generator/templates/sdk/operations/index.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/operations/index.ts rename to waspc/data/Generator/templates/sdk/operations/index.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/operations/resources.js b/waspc/data/Generator/templates/sdk/operations/resources.js similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/operations/resources.js rename to waspc/data/Generator/templates/sdk/operations/resources.js diff --git a/waspc/data/Generator/templates/sdk/wasp/operations/updateHandlersMap.js b/waspc/data/Generator/templates/sdk/operations/updateHandlersMap.js similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/operations/updateHandlersMap.js rename to waspc/data/Generator/templates/sdk/operations/updateHandlersMap.js diff --git a/waspc/data/Generator/templates/sdk/wasp/rpc/actions/core.d.ts b/waspc/data/Generator/templates/sdk/rpc/actions/core.d.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/rpc/actions/core.d.ts rename to waspc/data/Generator/templates/sdk/rpc/actions/core.d.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/rpc/actions/core.js b/waspc/data/Generator/templates/sdk/rpc/actions/core.js similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/rpc/actions/core.js rename to waspc/data/Generator/templates/sdk/rpc/actions/core.js diff --git a/waspc/data/Generator/templates/sdk/wasp/rpc/actions/index.ts b/waspc/data/Generator/templates/sdk/rpc/actions/index.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/rpc/actions/index.ts rename to waspc/data/Generator/templates/sdk/rpc/actions/index.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/rpc/index.ts b/waspc/data/Generator/templates/sdk/rpc/index.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/rpc/index.ts rename to waspc/data/Generator/templates/sdk/rpc/index.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/rpc/queries/core.d.ts b/waspc/data/Generator/templates/sdk/rpc/queries/core.d.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/rpc/queries/core.d.ts rename to waspc/data/Generator/templates/sdk/rpc/queries/core.d.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/rpc/queries/core.js b/waspc/data/Generator/templates/sdk/rpc/queries/core.js similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/rpc/queries/core.js rename to waspc/data/Generator/templates/sdk/rpc/queries/core.js diff --git a/waspc/data/Generator/templates/sdk/wasp/rpc/queries/index.ts b/waspc/data/Generator/templates/sdk/rpc/queries/index.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/rpc/queries/index.ts rename to waspc/data/Generator/templates/sdk/rpc/queries/index.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/rpc/queryClient.ts b/waspc/data/Generator/templates/sdk/rpc/queryClient.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/rpc/queryClient.ts rename to waspc/data/Generator/templates/sdk/rpc/queryClient.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/server/_types/index.ts b/waspc/data/Generator/templates/sdk/server/_types/index.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/server/_types/index.ts rename to waspc/data/Generator/templates/sdk/server/_types/index.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/server/_types/serialization.ts b/waspc/data/Generator/templates/sdk/server/_types/serialization.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/server/_types/serialization.ts rename to waspc/data/Generator/templates/sdk/server/_types/serialization.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/server/_types/taggedEntities.ts b/waspc/data/Generator/templates/sdk/server/_types/taggedEntities.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/server/_types/taggedEntities.ts rename to waspc/data/Generator/templates/sdk/server/_types/taggedEntities.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/server/actions/index.ts b/waspc/data/Generator/templates/sdk/server/actions/index.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/server/actions/index.ts rename to waspc/data/Generator/templates/sdk/server/actions/index.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/server/actions/types.ts b/waspc/data/Generator/templates/sdk/server/actions/types.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/server/actions/types.ts rename to waspc/data/Generator/templates/sdk/server/actions/types.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/server/dbClient.ts b/waspc/data/Generator/templates/sdk/server/dbClient.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/server/dbClient.ts rename to waspc/data/Generator/templates/sdk/server/dbClient.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/server/queries/index.ts b/waspc/data/Generator/templates/sdk/server/queries/index.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/server/queries/index.ts rename to waspc/data/Generator/templates/sdk/server/queries/index.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/server/queries/types.ts b/waspc/data/Generator/templates/sdk/server/queries/types.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/server/queries/types.ts rename to waspc/data/Generator/templates/sdk/server/queries/types.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/server/utils.ts b/waspc/data/Generator/templates/sdk/server/utils.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/server/utils.ts rename to waspc/data/Generator/templates/sdk/server/utils.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/types/index.ts b/waspc/data/Generator/templates/sdk/types/index.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/types/index.ts rename to waspc/data/Generator/templates/sdk/types/index.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/universal/types.ts b/waspc/data/Generator/templates/sdk/universal/types.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/universal/types.ts rename to waspc/data/Generator/templates/sdk/universal/types.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/universal/url.ts b/waspc/data/Generator/templates/sdk/universal/url.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/wasp/universal/url.ts rename to waspc/data/Generator/templates/sdk/universal/url.ts diff --git a/waspc/data/Generator/templates/sdk/wasp/api/events.ts b/waspc/data/Generator/templates/sdk/wasp/api/events.ts deleted file mode 100644 index 9a59b366d3..0000000000 --- a/waspc/data/Generator/templates/sdk/wasp/api/events.ts +++ /dev/null @@ -1,11 +0,0 @@ -import mitt, { Emitter } from 'mitt'; - -type ApiEvents = { - // key: Event name - // type: Event payload type - 'authToken.set': void; - 'authToken.clear': void; -}; - -// Used to allow API clients to register for auth token change events. -export const apiEventsEmitter: Emitter = mitt(); diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/index.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/index.ts index 9aad1ead59..7170ebab19 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/index.ts +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/index.ts @@ -1,8 +1,8 @@ import axios, { type AxiosError } from 'axios' -import config from 'wasp/core/config' -import { storage } from 'wasp/core/storage' -import { apiEventsEmitter } from 'wasp/api/events' +import config from '../core/config' +import { storage } from '../core/storage' +import { apiEventsEmitter } from '../api/events' const api = axios.create({ baseURL: config.apiUrl, diff --git a/waspc/src/Wasp/Generator/SdkGenerator.hs b/waspc/src/Wasp/Generator/SdkGenerator.hs index 846eac5dff..9b3091ad60 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator.hs @@ -1,10 +1,15 @@ -module Wasp.Generator.SdkGenerator where +module Wasp.Generator.SdkGenerator + ( genSdk, + installNpmDependencies, + ) +where import Data.Aeson (object) import qualified Data.Aeson as Aeson import Data.Aeson.Types ((.=)) import GHC.IO (unsafePerformIO) import StrongPath +import qualified StrongPath as SP import Wasp.AppSpec import qualified Wasp.AppSpec.App.Dependency as AS.Dependency import Wasp.AppSpec.Valid (isAuthEnabled) @@ -18,21 +23,47 @@ import qualified Wasp.Generator.NpmDependencies as N import Wasp.Generator.Templates (TemplatesDir, getTemplatesDirAbsPath) import Wasp.Project.Common (WaspProjectDir) import qualified Wasp.SemanticVersion as SV +import Wasp.Util ((<++>)) genSdk :: AppSpec -> Generator [FileDraft] -genSdk spec = sequence [genSdkModules, genPackageJson spec] +genSdk spec = (:) <$> genPackageJson spec <*> genHardcodedSdkModules <++> genSdkModules data SdkRootDir data SdkTemplatesDir -genSdkModules :: Generator FileDraft +genSdkModules :: Generator [FileDraft] genSdkModules = - return $ - createCopyDirFileDraft - RemoveExistingDstDir - sdkRootDirInProjectRootDir - (unsafePerformIO getTemplatesDirAbsPath sdkTemplatesDirInTemplatesDir [reldir|wasp|]) + sequence + [ genFileCopy [relfile|api/index.ts|], + genFileCopy [relfile|api/events.ts|] + ] + where + genFileCopy = return . mkTmplFd + +genHardcodedSdkModules :: Generator [FileDraft] +genHardcodedSdkModules = + return + [ copyModule [reldir|auth|], + copyModule [reldir|core|], + copyModule [reldir|entities|], + copyModule [reldir|ext-src|], + copyModule [reldir|operations|], + copyModule [reldir|rpc|], + copyModule [reldir|server|], + copyModule [reldir|types|], + copyModule [reldir|universal|] + ] + where + copyModule :: Path' (Rel SdkTemplatesDir) (Dir d) -> FileDraft + copyModule modul = + createCopyDirFileDraft + RemoveExistingDstDir + (dstFolder castRel modul) + (srcFolder modul) + dstFolder = sdkRootDirInProjectRootDir + srcFolder = absSdkTemplatesDir + absSdkTemplatesDir = unsafePerformIO getTemplatesDirAbsPath sdkTemplatesDirInTemplatesDir genPackageJson :: AppSpec -> Generator FileDraft genPackageJson spec = @@ -86,6 +117,12 @@ mkTmplFdWithDstAndData relSrcPath relDstPath tmplData = (sdkTemplatesDirInTemplatesDir relSrcPath) tmplData +mkTmplFdWithDst :: Path' (Rel SdkTemplatesDir) File' -> Path' (Rel SdkRootDir) File' -> FileDraft +mkTmplFdWithDst src dst = mkTmplFdWithDstAndData src dst Nothing + +mkTmplFd :: Path' (Rel SdkTemplatesDir) File' -> FileDraft +mkTmplFd path = mkTmplFdWithDst path (SP.castRel path) + sdkRootDirInProjectRootDir :: Path' (Rel ProjectRootDir) (Dir SdkRootDir) sdkRootDirInProjectRootDir = [reldir|sdk/wasp|] diff --git a/waspc/src/Wasp/Generator/WebAppGenerator.hs b/waspc/src/Wasp/Generator/WebAppGenerator.hs index c1c22c2b28..d873dd69db 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator.hs @@ -248,9 +248,6 @@ genSrcDir spec = genFileCopy [relfile|utils.js|], genFileCopy [relfile|types.ts|], genFileCopy [relfile|vite-env.d.ts|], - -- Generates api.js file which contains token management and configured api (e.g. axios) instance. - genFileCopy [relfile|api.ts|], - genFileCopy [relfile|api/events.ts|], genFileCopy [relfile|storage.ts|], getIndexTs spec ] From f76ed379bbf52a8138132537d5e3209496d314df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Wed, 10 Jan 2024 17:38:34 +0100 Subject: [PATCH 38/54] Update restructuring sdk base for cleaner diffs --- .../20230816092617_migracijone/migration.sql | 18 - .../20240110163721_init/migration.sql | 34 + .../todo-typescript/package-lock.json | 1277 +++++++++++++++++ 3 files changed, 1311 insertions(+), 18 deletions(-) delete mode 100644 waspc/examples/todo-typescript/migrations/20230816092617_migracijone/migration.sql create mode 100644 waspc/examples/todo-typescript/migrations/20240110163721_init/migration.sql create mode 100644 waspc/examples/todo-typescript/package-lock.json diff --git a/waspc/examples/todo-typescript/migrations/20230816092617_migracijone/migration.sql b/waspc/examples/todo-typescript/migrations/20230816092617_migracijone/migration.sql deleted file mode 100644 index 7b62627672..0000000000 --- a/waspc/examples/todo-typescript/migrations/20230816092617_migracijone/migration.sql +++ /dev/null @@ -1,18 +0,0 @@ --- CreateTable -CREATE TABLE "User" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "username" TEXT NOT NULL, - "password" TEXT NOT NULL -); - --- CreateTable -CREATE TABLE "Task" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "description" TEXT NOT NULL, - "isDone" BOOLEAN NOT NULL DEFAULT false, - "userId" INTEGER, - CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE -); - --- CreateIndex -CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); diff --git a/waspc/examples/todo-typescript/migrations/20240110163721_init/migration.sql b/waspc/examples/todo-typescript/migrations/20240110163721_init/migration.sql new file mode 100644 index 0000000000..0ea8e16da6 --- /dev/null +++ b/waspc/examples/todo-typescript/migrations/20240110163721_init/migration.sql @@ -0,0 +1,34 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT +); + +-- CreateTable +CREATE TABLE "Task" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "description" TEXT NOT NULL, + "isDone" BOOLEAN NOT NULL DEFAULT false, + "userId" INTEGER NOT NULL, + CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Auth" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" INTEGER, + CONSTRAINT "Auth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "AuthIdentity" ( + "providerName" TEXT NOT NULL, + "providerUserId" TEXT NOT NULL, + "providerData" TEXT NOT NULL DEFAULT '{}', + "authId" TEXT NOT NULL, + + PRIMARY KEY ("providerName", "providerUserId"), + CONSTRAINT "AuthIdentity_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "Auth_userId_key" ON "Auth"("userId"); diff --git a/waspc/examples/todo-typescript/package-lock.json b/waspc/examples/todo-typescript/package-lock.json new file mode 100644 index 0000000000..c438c1ae41 --- /dev/null +++ b/waspc/examples/todo-typescript/package-lock.json @@ -0,0 +1,1277 @@ +{ + "name": "prototype", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "prototype", + "dependencies": { + "react": "^18.2.0", + "wasp": "file:.wasp/out/sdk/wasp" + }, + "devDependencies": { + "@types/react": "^18.0.37", + "prisma": "4.16.2" + } + }, + ".wasp/out/sdk/wasp": { + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@prisma/client": "4.16.2", + "@stitches/react": "^1.2.8", + "@tanstack/react-query": "^4.29.0", + "@types/express-serve-static-core": "^4.17.13", + "axios": "^1.4.0", + "express": "~4.18.1", + "jsonwebtoken": "^8.5.1", + "mitt": "3.0.0", + "prisma": "4.16.2", + "react": "^18.2.0", + "react-hook-form": "^7.45.4", + "react-router-dom": "^5.3.3", + "secure-password": "^4.0.0", + "superjson": "^1.12.2" + }, + "devDependencies": {} + }, + "node_modules/@babel/runtime": { + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz", + "integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@prisma/client": { + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.16.2.tgz", + "integrity": "sha512-qCoEyxv1ZrQ4bKy39GnylE8Zq31IRmm8bNhNbZx7bF2cU5aiCCnSa93J2imF88MBjn7J9eUQneNxUQVJdl/rPQ==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines-version": "4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81" + }, + "engines": { + "node": ">=14.17" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/engines": { + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.16.2.tgz", + "integrity": "sha512-vx1nxVvN4QeT/cepQce68deh/Turxy5Mr+4L4zClFuK1GlxN3+ivxfuv+ej/gvidWn1cE1uAhW7ALLNlYbRUAw==", + "hasInstallScript": true + }, + "node_modules/@prisma/engines-version": { + "version": "4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81.tgz", + "integrity": "sha512-q617EUWfRIDTriWADZ4YiWRZXCa/WuhNgLTVd+HqWLffjMSPzyM5uOWoauX91wvQClSKZU4pzI4JJLQ9Kl62Qg==" + }, + "node_modules/@stitches/react": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@stitches/react/-/react-1.2.8.tgz", + "integrity": "sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA==", + "peerDependencies": { + "react": ">= 16.3.0" + } + }, + "node_modules/@tanstack/query-core": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", + "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz", + "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==", + "dependencies": { + "@tanstack/query-core": "4.36.1", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.41", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", + "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/node": { + "version": "20.10.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz", + "integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.11", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", + "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, + "node_modules/@types/react": { + "version": "18.2.47", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.47.tgz", + "integrity": "sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "dependencies": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=4", + "npm": ">=1.4.28" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mitt": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.0.tgz", + "integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/nanoassert": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz", + "integrity": "sha512-C40jQ3NzfkP53NsO8kEOFd79p4b9kDXQMwgiY1z8ZwrDZgUyom0AHwGegF4Dm99L+YoYhuaB0ceerUcXmqr1rQ==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/prisma": { + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.16.2.tgz", + "integrity": "sha512-SYCsBvDf0/7XSJyf2cHTLjLeTLVXYfqp7pG5eEVafFLeT0u/hLFz/9W196nDRGUOo1JfPatAEb+uEnTQImQC1g==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "4.16.2" + }, + "bin": { + "prisma": "build/index.js", + "prisma2": "build/index.js" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.49.3", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.3.tgz", + "integrity": "sha512-foD6r3juidAT1cOZzpmD/gOKt7fRsDhXXZ0y28+Al1CHgX+AY1qIN9VSIIItXRq1dN68QrRwl1ORFlwjBaAqeQ==", + "engines": { + "node": ">=18", + "pnpm": "8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-router": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", + "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-router-dom": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", + "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.3.4", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-router/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/secure-password": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/secure-password/-/secure-password-4.0.0.tgz", + "integrity": "sha512-B268T/tx+hq7q85KH6gonEqK/lhrLhNtzYzqojuMtBPVFBtwiIwxqF+4yr9POsJu5cIxbJyM66eYfXZiPZUXRA==", + "dependencies": { + "nanoassert": "^1.0.0", + "sodium-native": "^3.1.1" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sodium-native": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-3.4.1.tgz", + "integrity": "sha512-PaNN/roiFWzVVTL6OqjzYct38NSXewdl2wz8SRB51Br/MLIJPrbM3XexhVWkq7D3UWMysfrhKVf1v1phZq6MeQ==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/superjson": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.13.3.tgz", + "integrity": "sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", + "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wasp": { + "resolved": ".wasp/out/sdk/wasp", + "link": true + } + } +} From b6c353c7acdfa09eccdf9e54b2efd560e6fb98e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Wed, 10 Jan 2024 17:42:06 +0100 Subject: [PATCH 39/54] Update cleanstart script for cleaner diffs --- waspc/examples/todo-typescript/cleanstart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/waspc/examples/todo-typescript/cleanstart b/waspc/examples/todo-typescript/cleanstart index 3c7987deb5..8cf1609407 100755 --- a/waspc/examples/todo-typescript/cleanstart +++ b/waspc/examples/todo-typescript/cleanstart @@ -1 +1,2 @@ -rm -r .wasp node_modules package-lock.json migrations; cabal run wasp-cli db migrate-dev -- --name init && ./fix; cabal run wasp-cli start +cabal run wasp-cli reset; cabal run wasp-cli db migrate-dev -- --name init && ./fix; cabal run wasp-cli start + From 668f999adaf5461335bf05b7746e0c1c5c23402c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Wed, 10 Jan 2024 17:47:07 +0100 Subject: [PATCH 40/54] Update cleanstart script for cleaner diffs --- waspc/examples/todo-typescript/cleanstart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/waspc/examples/todo-typescript/cleanstart b/waspc/examples/todo-typescript/cleanstart index 8cf1609407..33e58b6caa 100755 --- a/waspc/examples/todo-typescript/cleanstart +++ b/waspc/examples/todo-typescript/cleanstart @@ -1,2 +1,3 @@ -cabal run wasp-cli reset; cabal run wasp-cli db migrate-dev -- --name init && ./fix; cabal run wasp-cli start +#!/bin/bash +cabal run wasp-cli reset; cabal run wasp-cli db migrate-dev && ./fix; cabal run wasp-cli start From 15f77f0a5f0addb9c8c00429da2a4b4cff9f6671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Wed, 10 Jan 2024 19:01:54 +0100 Subject: [PATCH 41/54] Add server/_types to proper SDK generation --- .../Generator/templates/sdk/entities/index.ts | 27 +++-- .../templates/sdk/server/_types/index.ts | 28 +++-- .../sdk/server/_types/taggedEntities.ts | 18 +-- .../templates/server/src/_types/index.ts | 107 ------------------ .../server/src/_types/serialization.ts | 43 ------- .../server/src/_types/taggedEntities.ts | 26 ----- .../templates/server/src/actions/types.ts | 2 +- .../templates/server/src/auth/user.ts | 2 +- .../templates/server/src/auth/utils.ts | 2 +- .../templates/server/src/entities/index.ts | 28 ----- .../templates/server/src/queries/types.ts | 2 +- .../Generator/templates/server/src/utils.ts | 2 +- .../.wasp/out/sdk/wasp/server/_types/index.ts | 2 - .../sdk/wasp/server/_types/taggedEntities.ts | 2 +- waspc/src/Wasp/Generator/SdkGenerator.hs | 95 +++++++++++++--- waspc/src/Wasp/Generator/ServerGenerator.hs | 55 +-------- 16 files changed, 131 insertions(+), 310 deletions(-) delete mode 100644 waspc/data/Generator/templates/server/src/_types/index.ts delete mode 100644 waspc/data/Generator/templates/server/src/_types/serialization.ts delete mode 100644 waspc/data/Generator/templates/server/src/_types/taggedEntities.ts delete mode 100644 waspc/data/Generator/templates/server/src/entities/index.ts diff --git a/waspc/data/Generator/templates/sdk/entities/index.ts b/waspc/data/Generator/templates/sdk/entities/index.ts index 5febac3804..591309c09d 100644 --- a/waspc/data/Generator/templates/sdk/entities/index.ts +++ b/waspc/data/Generator/templates/sdk/entities/index.ts @@ -1,21 +1,28 @@ +{{={= =}=}} import { - type User, - type Task, + {=# entities =} + type {= name =}, + {=/ entities =} } from "@prisma/client" export { - type User, - type Task, - type Auth, - type AuthIdentity, + {=# entities =} + type {= name =}, + {=/ entities =} + {=# isAuthEnabled =} + type {= authEntityName =}, + type {= authIdentityEntityName =}, + {=/ isAuthEnabled =} } from "@prisma/client" export type Entity = - | User - | Task + {=# entities =} + | {= name =} + {=/ entities =} | never export type EntityName = - | "User" - | "Task" + {=# entities =} + | "{= name =}" + {=/ entities =} | never diff --git a/waspc/data/Generator/templates/sdk/server/_types/index.ts b/waspc/data/Generator/templates/sdk/server/_types/index.ts index ad1de55e09..c8b1d08d83 100644 --- a/waspc/data/Generator/templates/sdk/server/_types/index.ts +++ b/waspc/data/Generator/templates/sdk/server/_types/index.ts @@ -1,18 +1,20 @@ +{{={= =}=}} import { type Expand } from 'wasp/universal/types'; import { type Request, type Response } from 'express' import { type ParamsDictionary as ExpressParams, type Query as ExpressQuery } from 'express-serve-static-core' import prisma from "wasp/server/dbClient" +{=# isAuthEnabled =} import { - type User, - type Auth, - type AuthIdentity, + type {= userEntityName =}, + type {= authEntityName =}, + type {= authIdentityEntityName =}, } from "wasp/entities" import { type EmailProviderData, type UsernameProviderData, type OAuthProviderData, - // todo(filip): marker } from 'wasp/auth/utils' +{=/ isAuthEnabled =} import { type _Entity } from "./taggedEntities" import { type Payload } from "./serialization"; @@ -25,6 +27,7 @@ export type Query = Operation +{=# isAuthEnabled =} export type AuthenticatedQuery = AuthenticatedOperation @@ -49,6 +52,7 @@ export type AuthenticatedApi< context: ContextWithUser, ) => void +{=/ isAuthEnabled =} type Operation = ( args: Input, context: Context, @@ -72,14 +76,16 @@ type EntityMap = { } export type PrismaDelegate = { - "User": typeof prisma.user, - "Task": typeof prisma.task, + {=# entities =} + "{= name =}": typeof prisma.{= prismaIdentifier =}, + {=/ entities =} } type Context = Expand<{ entities: Expand> }> +{=# isAuthEnabled =} type ContextWithUser = Expand & { user?: SanitizedUser }> // TODO: This type must match the logic in core/auth.js (if we remove the @@ -87,15 +93,15 @@ type ContextWithUser = Expand & { // these two things would live in the same place: // https://github.com/wasp-lang/wasp/issues/965 -export type DeserializedAuthEntity = Expand & { +export type DeserializedAuthEntity = Expand & { providerData: Omit | Omit | OAuthProviderData }> -export type SanitizedUser = User & { - auth: Auth & { - identities: DeserializedAuthEntity[] +export type SanitizedUser = {= userEntityName =} & { + {= authFieldOnUserEntityName =}: {= authEntityName =} & { + {= identitiesFieldOnAuthEntityName =}: DeserializedAuthEntity[] } | null } -// todo(filip): marker export type { ProviderName } from 'wasp/auth/utils' +{=/ isAuthEnabled =} diff --git a/waspc/data/Generator/templates/sdk/server/_types/taggedEntities.ts b/waspc/data/Generator/templates/sdk/server/_types/taggedEntities.ts index 3331b3f893..eda8037c0f 100644 --- a/waspc/data/Generator/templates/sdk/server/_types/taggedEntities.ts +++ b/waspc/data/Generator/templates/sdk/server/_types/taggedEntities.ts @@ -1,3 +1,4 @@ +{{={= =}=}} // Wasp internally uses the types defined in this file for typing entity maps in // operation contexts. // @@ -6,16 +7,19 @@ import { type Entity, type EntityName, - type User, - type Task, -} from '../../entities' + {=# entities =} + type {= name =}, + {=/ entities =} +} from 'wasp/entities' -export type _User = WithName -export type _Task = WithName +{=# entities =} +export type {= internalTypeName =} = WithName<{= name =}, "{= name =}"> +{=/ entities =} export type _Entity = - | _User - | _Task + {=# entities =} + | {= internalTypeName =} + {=/ entities =} | never type WithName = diff --git a/waspc/data/Generator/templates/server/src/_types/index.ts b/waspc/data/Generator/templates/server/src/_types/index.ts deleted file mode 100644 index 2b3c6bf5ae..0000000000 --- a/waspc/data/Generator/templates/server/src/_types/index.ts +++ /dev/null @@ -1,107 +0,0 @@ -{{={= =}=}} -import { type Expand } from "../universal/types.js"; -import { type Request, type Response } from 'express' -import { type ParamsDictionary as ExpressParams, type Query as ExpressQuery } from 'express-serve-static-core' -import prisma from "../dbClient.js" -{=# isAuthEnabled =} -import { - type {= userEntityName =}, - type {= authEntityName =}, - type {= authIdentityEntityName =}, -} from "../entities" -import { - type EmailProviderData, - type UsernameProviderData, - type OAuthProviderData, -} from '../auth/utils.js' -{=/ isAuthEnabled =} -import { type _Entity } from "./taggedEntities" -import { type Payload } from "./serialization"; - -export * from "./taggedEntities" -export * from "./serialization" - -export type Query = - Operation - -export type Action = - Operation - -{=# isAuthEnabled =} -export type AuthenticatedQuery = - AuthenticatedOperation - -export type AuthenticatedAction = - AuthenticatedOperation - -type AuthenticatedOperation = ( - args: Input, - context: ContextWithUser, -) => Output | Promise - -export type AuthenticatedApi< - Entities extends _Entity[], - Params extends ExpressParams, - ResBody, - ReqBody, - ReqQuery extends ExpressQuery, - Locals extends Record -> = ( - req: Request, - res: Response, - context: ContextWithUser, -) => void - -{=/ isAuthEnabled =} -type Operation = ( - args: Input, - context: Context, -) => Output | Promise - -export type Api< - Entities extends _Entity[], - Params extends ExpressParams, - ResBody, - ReqBody, - ReqQuery extends ExpressQuery, - Locals extends Record -> = ( - req: Request, - res: Response, - context: Context, -) => void - -type EntityMap = { - [EntityName in Entities[number]["_entityName"]]: PrismaDelegate[EntityName] -} - -export type PrismaDelegate = { - {=# entities =} - "{= name =}": typeof prisma.{= prismaIdentifier =}, - {=/ entities =} -} - -type Context = Expand<{ - entities: Expand> -}> - -{=# isAuthEnabled =} -type ContextWithUser = Expand & { user?: SanitizedUser }> - -// TODO: This type must match the logic in core/auth.js (if we remove the -// password field from the object there, we must do the same here). Ideally, -// these two things would live in the same place: -// https://github.com/wasp-lang/wasp/issues/965 - -export type DeserializedAuthEntity = Expand & { - providerData: Omit | Omit | OAuthProviderData -}> - -export type SanitizedUser = {= userEntityName =} & { - {= authFieldOnUserEntityName =}: {= authEntityName =} & { - {= identitiesFieldOnAuthEntityName =}: DeserializedAuthEntity[] - } | null -} - -export type { ProviderName } from '../auth/utils.js' -{=/ isAuthEnabled =} diff --git a/waspc/data/Generator/templates/server/src/_types/serialization.ts b/waspc/data/Generator/templates/server/src/_types/serialization.ts deleted file mode 100644 index 595b5ba69f..0000000000 --- a/waspc/data/Generator/templates/server/src/_types/serialization.ts +++ /dev/null @@ -1,43 +0,0 @@ -export type Payload = void | SuperJSONValue - -// The part below was copied from SuperJSON and slightly modified: -// https://github.com/blitz-js/superjson/blob/ae7dbcefe5d3ece5b04be0c6afe6b40f3a44a22a/src/types.ts -// -// We couldn't use SuperJSON's types directly because: -// 1. They aren't exported publicly. -// 2. They have a werid quirk that turns `SuperJSONValue` into `any`. -// See why here: -// https://github.com/blitz-js/superjson/pull/36#issuecomment-669239876 -// -// We changed the code as little as possible to make future comparisons easier. -export type JSONValue = PrimitiveJSONValue | JSONArray | JSONObject - -export interface JSONObject { - [key: string]: JSONValue -} - -type PrimitiveJSONValue = string | number | boolean | undefined | null - -interface JSONArray extends Array {} - -type SerializableJSONValue = - | Symbol - | Set - | Map - | undefined - | bigint - | Date - | RegExp - -// Here's where we excluded `ClassInstance` (which was `any`) from the union. -type SuperJSONValue = - | JSONValue - | SerializableJSONValue - | SuperJSONArray - | SuperJSONObject - -interface SuperJSONArray extends Array {} - -interface SuperJSONObject { - [key: string]: SuperJSONValue -} diff --git a/waspc/data/Generator/templates/server/src/_types/taggedEntities.ts b/waspc/data/Generator/templates/server/src/_types/taggedEntities.ts deleted file mode 100644 index 4e38484729..0000000000 --- a/waspc/data/Generator/templates/server/src/_types/taggedEntities.ts +++ /dev/null @@ -1,26 +0,0 @@ -{{={= =}=}} -// Wasp internally uses the types defined in this file for typing entity maps in -// operation contexts. -// -// We must explicitly tag all entities with their name to avoid issues with -// structural typing. See https://github.com/wasp-lang/wasp/pull/982 for details. -import { - type Entity, - type EntityName, - {=# entities =} - type {= name =}, - {=/ entities =} -} from '../entities' - -{=# entities =} -export type {= internalTypeName =} = WithName<{= name =}, "{= name =}"> -{=/ entities =} - -export type _Entity = - {=# entities =} - | {= internalTypeName =} - {=/ entities =} - | never - -type WithName = - E & { _entityName: Name } diff --git a/waspc/data/Generator/templates/server/src/actions/types.ts b/waspc/data/Generator/templates/server/src/actions/types.ts index c56080ce20..e0fa2e9237 100644 --- a/waspc/data/Generator/templates/server/src/actions/types.ts +++ b/waspc/data/Generator/templates/server/src/actions/types.ts @@ -12,7 +12,7 @@ import { type AuthenticatedAction, {=/ shouldImportAuthenticatedOperation =} type Payload, -} from '../_types' +} from 'wasp/server/_types' {=# operations =} export type {= typeName =} = diff --git a/waspc/data/Generator/templates/server/src/auth/user.ts b/waspc/data/Generator/templates/server/src/auth/user.ts index a5d987fc4e..8440b3d48c 100644 --- a/waspc/data/Generator/templates/server/src/auth/user.ts +++ b/waspc/data/Generator/templates/server/src/auth/user.ts @@ -2,7 +2,7 @@ // We have them duplicated in this file and in data/Generator/templates/react-app/src/auth/user.ts // If you are changing the logic here, make sure to change it there as well. -import type { SanitizedUser as User, ProviderName, DeserializedAuthEntity } from '../_types/index' +import type { SanitizedUser as User, ProviderName, DeserializedAuthEntity } from 'wasp/server/_types/index' export function getEmail(user: User): string | null { return findUserIdentity(user, "email")?.providerUserId ?? null; diff --git a/waspc/data/Generator/templates/server/src/auth/utils.ts b/waspc/data/Generator/templates/server/src/auth/utils.ts index 0ff4c7d29e..37e845d2af 100644 --- a/waspc/data/Generator/templates/server/src/auth/utils.ts +++ b/waspc/data/Generator/templates/server/src/auth/utils.ts @@ -8,7 +8,7 @@ import { type {= userEntityUpper =}, type {= authEntityUpper =}, type {= authIdentityEntityUpper =}, -} from '../entities/index.js' +} from 'wasp/entities/index.js' import { Prisma } from '@prisma/client'; import { throwValidationError } from './validation.js' diff --git a/waspc/data/Generator/templates/server/src/entities/index.ts b/waspc/data/Generator/templates/server/src/entities/index.ts deleted file mode 100644 index 591309c09d..0000000000 --- a/waspc/data/Generator/templates/server/src/entities/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -{{={= =}=}} -import { - {=# entities =} - type {= name =}, - {=/ entities =} -} from "@prisma/client" - -export { - {=# entities =} - type {= name =}, - {=/ entities =} - {=# isAuthEnabled =} - type {= authEntityName =}, - type {= authIdentityEntityName =}, - {=/ isAuthEnabled =} -} from "@prisma/client" - -export type Entity = - {=# entities =} - | {= name =} - {=/ entities =} - | never - -export type EntityName = - {=# entities =} - | "{= name =}" - {=/ entities =} - | never diff --git a/waspc/data/Generator/templates/server/src/queries/types.ts b/waspc/data/Generator/templates/server/src/queries/types.ts index 405b05d9c8..33055e7c74 100644 --- a/waspc/data/Generator/templates/server/src/queries/types.ts +++ b/waspc/data/Generator/templates/server/src/queries/types.ts @@ -13,7 +13,7 @@ import { type AuthenticatedQuery, {=/ shouldImportAuthenticatedOperation =} type Payload, -} from '../_types' +} from 'wasp/server/_types' {=# operations =} export type {= typeName =} = diff --git a/waspc/data/Generator/templates/server/src/utils.ts b/waspc/data/Generator/templates/server/src/utils.ts index 3a38f87ede..44f05bfc7c 100644 --- a/waspc/data/Generator/templates/server/src/utils.ts +++ b/waspc/data/Generator/templates/server/src/utils.ts @@ -7,7 +7,7 @@ import { dirname } from 'path' import { fileURLToPath } from 'url' {=# isAuthEnabled =} -import { type SanitizedUser } from './_types/index.js' +import { type SanitizedUser } from 'wasp/server/_types/index.js' {=/ isAuthEnabled =} type RequestWithExtraFields = Request & { diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/index.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/index.ts index ad1de55e09..a29ec5ad16 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/index.ts +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/index.ts @@ -11,7 +11,6 @@ import { type EmailProviderData, type UsernameProviderData, type OAuthProviderData, - // todo(filip): marker } from 'wasp/auth/utils' import { type _Entity } from "./taggedEntities" import { type Payload } from "./serialization"; @@ -97,5 +96,4 @@ export type SanitizedUser = User & { } | null } -// todo(filip): marker export type { ProviderName } from 'wasp/auth/utils' diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/taggedEntities.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/taggedEntities.ts index 3331b3f893..c82affed3b 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/taggedEntities.ts +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/taggedEntities.ts @@ -8,7 +8,7 @@ import { type EntityName, type User, type Task, -} from '../../entities' +} from 'wasp/entities' export type _User = WithName export type _Task = WithName diff --git a/waspc/src/Wasp/Generator/SdkGenerator.hs b/waspc/src/Wasp/Generator/SdkGenerator.hs index 9b3091ad60..04b02e0830 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE TypeApplications #-} + module Wasp.Generator.SdkGenerator ( genSdk, installNpmDependencies, @@ -7,13 +9,20 @@ where import Data.Aeson (object) import qualified Data.Aeson as Aeson import Data.Aeson.Types ((.=)) +import Data.Maybe (fromMaybe, isJust) import GHC.IO (unsafePerformIO) import StrongPath import qualified StrongPath as SP import Wasp.AppSpec +import qualified Wasp.AppSpec as AS +import qualified Wasp.AppSpec.App as AS.App +import qualified Wasp.AppSpec.App.Auth as AS.App.Auth import qualified Wasp.AppSpec.App.Dependency as AS.Dependency +import qualified Wasp.AppSpec.Entity as AS.Entity import Wasp.AppSpec.Valid (isAuthEnabled) -import Wasp.Generator.Common (ProjectRootDir, prismaVersion) +import qualified Wasp.AppSpec.Valid as AS.Valid +import Wasp.Generator.Common (ProjectRootDir, makeJsonWithEntityData, prismaVersion) +import qualified Wasp.Generator.DbGenerator.Auth as DbAuth import Wasp.Generator.FileDraft (FileDraft, createCopyDirFileDraft, createTemplateFileDraft) import Wasp.Generator.FileDraft.CopyDirFileDraft (CopyDirFileDraftDstDirStrategy (RemoveExistingDstDir)) import qualified Wasp.Generator.Job as J @@ -23,48 +32,102 @@ import qualified Wasp.Generator.NpmDependencies as N import Wasp.Generator.Templates (TemplatesDir, getTemplatesDirAbsPath) import Wasp.Project.Common (WaspProjectDir) import qualified Wasp.SemanticVersion as SV -import Wasp.Util ((<++>)) +import Wasp.Util (toLowerFirst, (<++>)) genSdk :: AppSpec -> Generator [FileDraft] -genSdk spec = (:) <$> genPackageJson spec <*> genHardcodedSdkModules <++> genSdkModules +genSdk spec = (:) <$> genPackageJson spec <*> genHardcodedSdkModules <++> genSdkModules spec data SdkRootDir data SdkTemplatesDir -genSdkModules :: Generator [FileDraft] -genSdkModules = +genSdkModules :: AppSpec -> Generator [FileDraft] +genSdkModules spec = sequence [ genFileCopy [relfile|api/index.ts|], genFileCopy [relfile|api/events.ts|] ] + <++> genTypesAndEntitiesDirs spec where genFileCopy = return . mkTmplFd genHardcodedSdkModules :: Generator [FileDraft] genHardcodedSdkModules = return - [ copyModule [reldir|auth|], - copyModule [reldir|core|], - copyModule [reldir|entities|], - copyModule [reldir|ext-src|], - copyModule [reldir|operations|], - copyModule [reldir|rpc|], - copyModule [reldir|server|], - copyModule [reldir|types|], - copyModule [reldir|universal|] + [ copyFolder [reldir|auth|], + copyFolder [reldir|core|], + copyFolder [reldir|ext-src|], + copyFolder [reldir|operations|], + copyFolder [reldir|rpc|], + copyFolder [reldir|server/actions|], + copyFolder [reldir|server/queries|], + copyFile [relfile|server/dbClient.ts|], + copyFile [relfile|server/utils.ts|], + copyFolder [reldir|types|], + copyFolder [reldir|universal|] ] where - copyModule :: Path' (Rel SdkTemplatesDir) (Dir d) -> FileDraft - copyModule modul = + copyFolder :: Path' (Rel SdkTemplatesDir) (Dir d) -> FileDraft + copyFolder modul = createCopyDirFileDraft RemoveExistingDstDir (dstFolder castRel modul) (srcFolder modul) + copyFile :: Path' (Rel SdkTemplatesDir) File' -> FileDraft + copyFile = mkTmplFd dstFolder = sdkRootDirInProjectRootDir srcFolder = absSdkTemplatesDir absSdkTemplatesDir = unsafePerformIO getTemplatesDirAbsPath sdkTemplatesDirInTemplatesDir +genTypesAndEntitiesDirs :: AppSpec -> Generator [FileDraft] +genTypesAndEntitiesDirs spec = + return + [ entitiesIndexFileDraft, + taggedEntitiesFileDraft, + serializationFileDraft, + typesIndexFileDraft + ] + where + entitiesIndexFileDraft = + mkTmplFdWithDstAndData + [relfile|entities/index.ts|] + [relfile|entities/index.ts|] + ( Just $ + object + [ "entities" .= allEntities, + "isAuthEnabled" .= isJust maybeUserEntityName, + "authEntityName" .= DbAuth.authEntityName, + "authIdentityEntityName" .= DbAuth.authIdentityEntityName + ] + ) + taggedEntitiesFileDraft = + mkTmplFdWithDstAndData + [relfile|server/_types/taggedEntities.ts|] + [relfile|server/_types/taggedEntities.ts|] + (Just $ object ["entities" .= allEntities]) + serializationFileDraft = + mkTmplFd + [relfile|server/_types/serialization.ts|] + typesIndexFileDraft = + mkTmplFdWithDstAndData + [relfile|server/_types/index.ts|] + [relfile|server/_types/index.ts|] + ( Just $ + object + [ "entities" .= allEntities, + "isAuthEnabled" .= isJust maybeUserEntityName, + "userEntityName" .= userEntityName, + "authEntityName" .= DbAuth.authEntityName, + "authFieldOnUserEntityName" .= DbAuth.authFieldOnUserEntityName, + "authIdentityEntityName" .= DbAuth.authIdentityEntityName, + "identitiesFieldOnAuthEntityName" .= DbAuth.identitiesFieldOnAuthEntityName, + "userFieldName" .= toLowerFirst userEntityName + ] + ) + userEntityName = fromMaybe "" maybeUserEntityName + allEntities = map (makeJsonWithEntityData . fst) $ AS.getDecls @AS.Entity.Entity spec + maybeUserEntityName = AS.refName . AS.App.Auth.userEntity <$> AS.App.auth (snd $ AS.Valid.getApp spec) + genPackageJson :: AppSpec -> Generator FileDraft genPackageJson spec = return $ diff --git a/waspc/src/Wasp/Generator/ServerGenerator.hs b/waspc/src/Wasp/Generator/ServerGenerator.hs index f7874d0480..78ef5f6368 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator.hs @@ -16,7 +16,6 @@ import qualified Data.Aeson as Aeson import qualified Data.ByteString.Lazy.UTF8 as ByteStringLazyUTF8 import Data.Maybe ( fromJust, - fromMaybe, isJust, maybeToList, ) @@ -44,9 +43,7 @@ import Wasp.AppSpec.Valid (getApp, getLowestNodeVersionUserAllows, isAuthEnabled import Wasp.Env (envVarsToDotEnvContent) import Wasp.Generator.Common ( ServerRootDir, - makeJsonWithEntityData, ) -import qualified Wasp.Generator.DbGenerator.Auth as DbAuth import Wasp.Generator.FileDraft (FileDraft, createTextFileDraft) import Wasp.Generator.Monad (Generator) import qualified Wasp.Generator.NpmDependencies as N @@ -66,7 +63,7 @@ import Wasp.Generator.ServerGenerator.WebSocketG (depsRequiredByWebSockets, genW import qualified Wasp.Node.Version as NodeVersion import Wasp.Project.Db (databaseUrlEnvVarName) import qualified Wasp.SemanticVersion as SV -import Wasp.Util (toLowerFirst, (<++>)) +import Wasp.Util ((<++>)) genServer :: AppSpec -> Generator [FileDraft] genServer spec = @@ -225,7 +222,6 @@ genSrcDir spec = ] <++> genServerUtils spec <++> genRoutesDir spec - <++> genTypesAndEntitiesDirs spec <++> genOperationsRoutes spec <++> genOperations spec <++> genAuth spec @@ -296,55 +292,6 @@ genRoutesIndex spec = "areThereAnyCrudRoutes" .= (not . null $ AS.getCruds spec) ] -genTypesAndEntitiesDirs :: AppSpec -> Generator [FileDraft] -genTypesAndEntitiesDirs spec = - return - [ entitiesIndexFileDraft, - taggedEntitiesFileDraft, - serializationFileDraft, - typesIndexFileDraft - ] - where - entitiesIndexFileDraft = - C.mkTmplFdWithDstAndData - [relfile|src/entities/index.ts|] - [relfile|src/entities/index.ts|] - ( Just $ - object - [ "entities" .= allEntities, - "isAuthEnabled" .= isJust maybeUserEntityName, - "authEntityName" .= DbAuth.authEntityName, - "authIdentityEntityName" .= DbAuth.authIdentityEntityName - ] - ) - taggedEntitiesFileDraft = - C.mkTmplFdWithDstAndData - [relfile|src/_types/taggedEntities.ts|] - [relfile|src/_types/taggedEntities.ts|] - (Just $ object ["entities" .= allEntities]) - serializationFileDraft = - C.mkSrcTmplFd - [relfile|_types/serialization.ts|] - typesIndexFileDraft = - C.mkTmplFdWithDstAndData - [relfile|src/_types/index.ts|] - [relfile|src/_types/index.ts|] - ( Just $ - object - [ "entities" .= allEntities, - "isAuthEnabled" .= isJust maybeUserEntityName, - "userEntityName" .= userEntityName, - "authEntityName" .= DbAuth.authEntityName, - "authFieldOnUserEntityName" .= DbAuth.authFieldOnUserEntityName, - "authIdentityEntityName" .= DbAuth.authIdentityEntityName, - "identitiesFieldOnAuthEntityName" .= DbAuth.identitiesFieldOnAuthEntityName, - "userFieldName" .= toLowerFirst userEntityName - ] - ) - userEntityName = fromMaybe "" maybeUserEntityName - allEntities = map (makeJsonWithEntityData . fst) $ AS.getDecls @AS.Entity.Entity spec - maybeUserEntityName = AS.refName . AS.App.Auth.userEntity <$> AS.App.auth (snd $ getApp spec) - operationsRouteInRootRouter :: String operationsRouteInRootRouter = "operations" From de4cf8adcf8b3a7893f37b8aa3841d32ef43accc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Wed, 10 Jan 2024 19:06:46 +0100 Subject: [PATCH 42/54] Make relative imports aboslute in api sdk --- waspc/data/Generator/templates/sdk/api/index.ts | 6 +++--- .../todo-typescript/.wasp/out/sdk/wasp/api/index.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/waspc/data/Generator/templates/sdk/api/index.ts b/waspc/data/Generator/templates/sdk/api/index.ts index 7170ebab19..9aad1ead59 100644 --- a/waspc/data/Generator/templates/sdk/api/index.ts +++ b/waspc/data/Generator/templates/sdk/api/index.ts @@ -1,8 +1,8 @@ import axios, { type AxiosError } from 'axios' -import config from '../core/config' -import { storage } from '../core/storage' -import { apiEventsEmitter } from '../api/events' +import config from 'wasp/core/config' +import { storage } from 'wasp/core/storage' +import { apiEventsEmitter } from 'wasp/api/events' const api = axios.create({ baseURL: config.apiUrl, diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/index.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/index.ts index 7170ebab19..9aad1ead59 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/index.ts +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/index.ts @@ -1,8 +1,8 @@ import axios, { type AxiosError } from 'axios' -import config from '../core/config' -import { storage } from '../core/storage' -import { apiEventsEmitter } from '../api/events' +import config from 'wasp/core/config' +import { storage } from 'wasp/core/storage' +import { apiEventsEmitter } from 'wasp/api/events' const api = axios.create({ baseURL: config.apiUrl, From 6ea38e7bb06bdfcfcd96e074c78e47c68f5dc918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Wed, 10 Jan 2024 17:38:34 +0100 Subject: [PATCH 43/54] Update restructuring sdk base for cleaner diffs From 6a0748cb507f0c7d1428719117f2ef8a9aa40b5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Wed, 10 Jan 2024 17:47:07 +0100 Subject: [PATCH 44/54] Update cleanstart script for cleaner diffs From 281ddcd0a1031d2e1d0f04b5f05651362ccd64a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Tue, 16 Jan 2024 14:59:39 +0100 Subject: [PATCH 45/54] Add ext-src (operations), dbClient, and public to proper SDK gen --- waspc/cli/src/Wasp/Cli/Command/Build.hs | 4 +- waspc/cli/src/Wasp/Cli/Command/Compile.hs | 4 +- waspc/cli/src/Wasp/Cli/Command/Watch.hs | 8 +-- waspc/cli/src/Wasp/Cli/Common.hs | 13 ---- .../data/Generator/templates/sdk/package.json | 3 +- .../sdk/server/{dbClient.ts => dbClient.js} | 0 .../templates/server/src/actions/_action.ts | 2 +- .../src/auth/providers/oauth/createRouter.ts | 2 +- .../templates/server/src/auth/utils.ts | 2 +- .../templates/server/src/core/auth.js | 2 +- .../templates/server/src/crud/_operations.ts | 2 +- .../templates/server/src/dbClient.ts | 13 ---- .../Generator/templates/server/src/dbSeed.ts | 2 +- .../templates/server/src/jobs/_job.ts | 2 +- .../templates/server/src/queries/_query.ts | 2 +- .../templates/server/src/routes/apis/index.ts | 2 +- .../templates/server/src/webSocket/index.ts | 2 +- .../server/src/webSocket/initialization.ts | 2 +- .../.wasp/out/sdk/wasp/package.json | 3 +- .../.wasp/out/sdk/wasp/server/dbClient.ts | 12 ---- waspc/src/Wasp/AppSpec.hs | 14 ++-- waspc/src/Wasp/AppSpec/ExtImport.hs | 2 +- waspc/src/Wasp/AppSpec/ExternalCode.hs | 52 -------------- waspc/src/Wasp/AppSpec/ExternalFiles.hs | 67 +++++++++++++++++++ waspc/src/Wasp/AppSpec/Valid.hs | 2 +- waspc/src/Wasp/CompileOptions.hs | 6 +- waspc/src/Wasp/Generator/DbGenerator.hs | 2 +- .../Wasp/Generator/ExternalCodeGenerator.hs | 38 ----------- .../Generator/ExternalCodeGenerator/Common.hs | 26 +------ .../Generator/ExternalCodeGenerator/Js.hs | 44 ------------ waspc/src/Wasp/Generator/JsImport.hs | 16 ----- waspc/src/Wasp/Generator/SdkGenerator.hs | 52 +++++++++++++- waspc/src/Wasp/Generator/ServerGenerator.hs | 26 +------ .../ServerGenerator/ExternalCodeGenerator.hs | 43 ------------ waspc/src/Wasp/Generator/WebAppGenerator.hs | 38 ++++------- .../Wasp/Generator/WebAppGenerator/Common.hs | 13 +++- .../WebAppGenerator/ExternalCodeGenerator.hs | 57 ---------------- .../Generator/WebAppGenerator/JsImport.hs | 32 +-------- waspc/src/Wasp/Project/Analyze.hs | 20 ++---- waspc/src/Wasp/Project/Common.hs | 10 +++ .../ExternalFiles.hs} | 43 ++++++------ waspc/src/Wasp/Project/Vite.hs | 13 ++-- .../Waspignore.hs} | 28 +++++++- waspc/src/Wasp/Project/WebApp.hs | 9 --- waspc/test/AppSpec/ValidTest.hs | 5 +- .../Generator/ExternalCodeGenerator/JsTest.hs | 17 ----- waspc/test/Generator/WebAppGeneratorTest.hs | 5 +- .../WaspignoreTest.hs} | 4 +- waspc/waspc.cabal | 14 ++-- waspc/waspls/src/Wasp/LSP/ExtImport/Path.hs | 2 +- 50 files changed, 261 insertions(+), 521 deletions(-) rename waspc/data/Generator/templates/sdk/server/{dbClient.ts => dbClient.js} (100%) delete mode 100644 waspc/data/Generator/templates/server/src/dbClient.ts delete mode 100644 waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/dbClient.ts delete mode 100644 waspc/src/Wasp/AppSpec/ExternalCode.hs create mode 100644 waspc/src/Wasp/AppSpec/ExternalFiles.hs delete mode 100644 waspc/src/Wasp/Generator/ExternalCodeGenerator.hs delete mode 100644 waspc/src/Wasp/Generator/ExternalCodeGenerator/Js.hs delete mode 100644 waspc/src/Wasp/Generator/ServerGenerator/ExternalCodeGenerator.hs delete mode 100644 waspc/src/Wasp/Generator/WebAppGenerator/ExternalCodeGenerator.hs rename waspc/src/Wasp/{ExternalCode.hs => Project/ExternalFiles.hs} (57%) rename waspc/src/Wasp/{WaspignoreFile.hs => Project/Waspignore.hs} (71%) delete mode 100644 waspc/src/Wasp/Project/WebApp.hs delete mode 100644 waspc/test/Generator/ExternalCodeGenerator/JsTest.hs rename waspc/test/{WaspignoreFileTest.hs => Project/WaspignoreTest.hs} (94%) diff --git a/waspc/cli/src/Wasp/Cli/Command/Build.hs b/waspc/cli/src/Wasp/Cli/Command/Build.hs index 1f3b6e8d28..cd049fa91f 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Build.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Build.hs @@ -65,9 +65,7 @@ buildIO waspProjectDir buildDir = compileIOWithOptions options waspProjectDir bu where options = CompileOptions - { externalClientCodeDirPath = waspProjectDir Common.extClientCodeDirInWaspProjectDir, - externalServerCodeDirPath = waspProjectDir Common.extServerCodeDirInWaspProjectDir, - externalSharedCodeDirPath = waspProjectDir Common.extSharedCodeDirInWaspProjectDir, + { waspProjectDirPath = waspProjectDir, isBuild = True, sendMessage = cliSendMessage, -- Ignore "DB needs migration warnings" during build, as that is not a required step. diff --git a/waspc/cli/src/Wasp/Cli/Command/Compile.hs b/waspc/cli/src/Wasp/Cli/Command/Compile.hs index f5a208aecf..4f92593faf 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Compile.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Compile.hs @@ -115,9 +115,7 @@ compileIOWithOptions options waspProjectDir outDir = defaultCompileOptions :: Path' Abs (Dir WaspProjectDir) -> CompileOptions defaultCompileOptions waspProjectDir = CompileOptions - { externalServerCodeDirPath = waspProjectDir Common.extServerCodeDirInWaspProjectDir, - externalClientCodeDirPath = waspProjectDir Common.extClientCodeDirInWaspProjectDir, - externalSharedCodeDirPath = waspProjectDir Common.extSharedCodeDirInWaspProjectDir, + { waspProjectDirPath = waspProjectDir, isBuild = False, sendMessage = cliSendMessage, generatorWarningsFilter = id diff --git a/waspc/cli/src/Wasp/Cli/Command/Watch.hs b/waspc/cli/src/Wasp/Cli/Command/Watch.hs index 5689e0be56..2da3e77d19 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Watch.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Watch.hs @@ -15,11 +15,11 @@ import qualified StrongPath as SP import qualified System.FSNotify as FSN import qualified System.FilePath as FP import Wasp.Cli.Command.Compile (compileIO, printCompilationResult) -import qualified Wasp.Cli.Common as Common import Wasp.Cli.Message (cliSendMessage) import qualified Wasp.Generator.Common as Wasp.Generator import qualified Wasp.Message as Msg import Wasp.Project (CompileError, CompileWarning, WaspProjectDir) +import qualified Wasp.Project.Common as ProjectCommon -- TODO: Idea: Read .gitignore file, and ignore everything from it. This will then also cover the -- .wasp dir, and users can easily add any custom stuff they want ignored. But, we also have to @@ -41,9 +41,9 @@ watch waspProjectDir outDir ongoingCompilationResultMVar = FSN.withManager $ \mg chan <- newChan _ <- FSN.watchDirChan mgr (SP.fromAbsDir waspProjectDir) eventFilter chan let watchProjectSubdirTree path = FSN.watchTreeChan mgr (SP.fromAbsDir $ waspProjectDir path) eventFilter chan - _ <- watchProjectSubdirTree Common.extClientCodeDirInWaspProjectDir - _ <- watchProjectSubdirTree Common.extServerCodeDirInWaspProjectDir - _ <- watchProjectSubdirTree Common.extSharedCodeDirInWaspProjectDir + -- todo(filip): check if this still works + _ <- watchProjectSubdirTree ProjectCommon.extCodeDirInWaspProjectDir + _ <- watchProjectSubdirTree ProjectCommon.extPublicDirInWaspProjectDir listenForEvents chan currentTime where listenForEvents :: Chan FSN.Event -> UTCTime -> IO () diff --git a/waspc/cli/src/Wasp/Cli/Common.hs b/waspc/cli/src/Wasp/Cli/Common.hs index 54b3bd4942..0daa1155d3 100644 --- a/waspc/cli/src/Wasp/Cli/Common.hs +++ b/waspc/cli/src/Wasp/Cli/Common.hs @@ -6,9 +6,6 @@ module Wasp.Cli.Common dotWaspRootFileInWaspProjectDir, dotWaspInfoFileInGeneratedCodeDir, nodeModulesDirInWaspProjectDir, - extServerCodeDirInWaspProjectDir, - extClientCodeDirInWaspProjectDir, - extSharedCodeDirInWaspProjectDir, generatedCodeDirInDotWaspDir, buildDirInDotWaspDir, waspSays, @@ -18,7 +15,6 @@ module Wasp.Cli.Common where import StrongPath (Dir, File', Path', Rel, reldir, relfile) -import Wasp.AppSpec.ExternalCode (SourceExternalCodeDir) import qualified Wasp.Generator.Common import Wasp.Project (WaspProjectDir) import qualified Wasp.Util.Terminal as Term @@ -49,15 +45,6 @@ dotWaspRootFileInWaspProjectDir = [relfile|.wasproot|] dotWaspInfoFileInGeneratedCodeDir :: Path' (Rel Wasp.Generator.Common.ProjectRootDir) File' dotWaspInfoFileInGeneratedCodeDir = [relfile|.waspinfo|] -extServerCodeDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir SourceExternalCodeDir) -extServerCodeDirInWaspProjectDir = [reldir|src|] - -extClientCodeDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir SourceExternalCodeDir) -extClientCodeDirInWaspProjectDir = [reldir|src|] - -extSharedCodeDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir SourceExternalCodeDir) -extSharedCodeDirInWaspProjectDir = [reldir|src|] - waspSays :: String -> IO () waspSays what = putStrLn $ Term.applyStyles [Term.Yellow] what diff --git a/waspc/data/Generator/templates/sdk/package.json b/waspc/data/Generator/templates/sdk/package.json index b6abbdfd84..569661c957 100644 --- a/waspc/data/Generator/templates/sdk/package.json +++ b/waspc/data/Generator/templates/sdk/package.json @@ -24,7 +24,8 @@ "./operations": "./operations/index.ts", "./operations/*": "./operations/*", "./universal/url": "./universal/url.ts", - "./universal/types": "./universal/url.ts" + "./universal/types": "./universal/url.ts", + "./server/dbClient": "./server/dbClient.js" }, "license": "ISC", "include": [ diff --git a/waspc/data/Generator/templates/sdk/server/dbClient.ts b/waspc/data/Generator/templates/sdk/server/dbClient.js similarity index 100% rename from waspc/data/Generator/templates/sdk/server/dbClient.ts rename to waspc/data/Generator/templates/sdk/server/dbClient.js diff --git a/waspc/data/Generator/templates/server/src/actions/_action.ts b/waspc/data/Generator/templates/server/src/actions/_action.ts index 1bc6c0ad40..11b092cb1a 100644 --- a/waspc/data/Generator/templates/server/src/actions/_action.ts +++ b/waspc/data/Generator/templates/server/src/actions/_action.ts @@ -1,5 +1,5 @@ {{={= =}=}} -import prisma from '../dbClient.js' +import prisma from 'wasp/server/dbClient' {=& jsFn.importStatement =} diff --git a/waspc/data/Generator/templates/server/src/auth/providers/oauth/createRouter.ts b/waspc/data/Generator/templates/server/src/auth/providers/oauth/createRouter.ts index d8a4fe0b62..ccae975623 100644 --- a/waspc/data/Generator/templates/server/src/auth/providers/oauth/createRouter.ts +++ b/waspc/data/Generator/templates/server/src/auth/providers/oauth/createRouter.ts @@ -3,7 +3,7 @@ import { Router } from "express" import passport from "passport" -import prisma from '../../../dbClient.js' +import prisma from 'wasp/server/dbClient' import waspServerConfig from '../../../config.js' import { type ProviderName, diff --git a/waspc/data/Generator/templates/server/src/auth/utils.ts b/waspc/data/Generator/templates/server/src/auth/utils.ts index 37e845d2af..4cf9d69203 100644 --- a/waspc/data/Generator/templates/server/src/auth/utils.ts +++ b/waspc/data/Generator/templates/server/src/auth/utils.ts @@ -2,7 +2,7 @@ import { hashPassword, sign, verify } from '../core/auth.js' import AuthError from '../core/AuthError.js' import HttpError from '../core/HttpError.js' -import prisma from '../dbClient.js' +import prisma from 'wasp/server/dbClient' import { sleep } from '../utils.js' import { type {= userEntityUpper =}, diff --git a/waspc/data/Generator/templates/server/src/core/auth.js b/waspc/data/Generator/templates/server/src/core/auth.js index 33105b1cbb..497885affa 100644 --- a/waspc/data/Generator/templates/server/src/core/auth.js +++ b/waspc/data/Generator/templates/server/src/core/auth.js @@ -4,7 +4,7 @@ import SecurePassword from 'secure-password' import util from 'util' import { randomInt } from 'node:crypto' -import prisma from '../dbClient.js' +import prisma from 'wasp/server/dbClient' import { handleRejection } from '../utils.js' import HttpError from '../core/HttpError.js' import config from '../config.js' diff --git a/waspc/data/Generator/templates/server/src/crud/_operations.ts b/waspc/data/Generator/templates/server/src/crud/_operations.ts index 7ffb5a1fd2..11626b6599 100644 --- a/waspc/data/Generator/templates/server/src/crud/_operations.ts +++ b/waspc/data/Generator/templates/server/src/crud/_operations.ts @@ -1,5 +1,5 @@ {{={= =}=}} -import prisma from "../dbClient.js"; +import prisma from "wasp/server/dbClient"; import type { {=# isAuthEnabled =} diff --git a/waspc/data/Generator/templates/server/src/dbClient.ts b/waspc/data/Generator/templates/server/src/dbClient.ts deleted file mode 100644 index e6319ff696..0000000000 --- a/waspc/data/Generator/templates/server/src/dbClient.ts +++ /dev/null @@ -1,13 +0,0 @@ -{{={= =}=}} -import Prisma from '@prisma/client' - - -const createDbClient = () => { - const prismaClient = new Prisma.PrismaClient() - - return prismaClient -} - -const dbClient = createDbClient() - -export default dbClient diff --git a/waspc/data/Generator/templates/server/src/dbSeed.ts b/waspc/data/Generator/templates/server/src/dbSeed.ts index a4d89ddd6b..1f4d840781 100644 --- a/waspc/data/Generator/templates/server/src/dbSeed.ts +++ b/waspc/data/Generator/templates/server/src/dbSeed.ts @@ -6,7 +6,7 @@ // TODO: Consider in the future moving it into a a separate project (maybe db/ ?), while still // maintaining access to logic from the server/ . -import prismaClient from './dbClient.js' +import prismaClient from 'wasp/server/dbClient' import type { DbSeedFn } from './dbSeed/types.js' {=# dbSeeds =} diff --git a/waspc/data/Generator/templates/server/src/jobs/_job.ts b/waspc/data/Generator/templates/server/src/jobs/_job.ts index ef619f17d8..21a30a45f3 100644 --- a/waspc/data/Generator/templates/server/src/jobs/_job.ts +++ b/waspc/data/Generator/templates/server/src/jobs/_job.ts @@ -1,5 +1,5 @@ {{={= =}=}} -import prisma from '../dbClient.js' +import prisma from 'wasp/server/dbClient' import type { JSONValue, JSONObject } from '../_types/serialization.js' import { createJob, type JobFn } from './{= jobExecutorRelativePath =}' {=& jobPerformFnImportStatement =} diff --git a/waspc/data/Generator/templates/server/src/queries/_query.ts b/waspc/data/Generator/templates/server/src/queries/_query.ts index 2d6c17d482..24118fd432 100644 --- a/waspc/data/Generator/templates/server/src/queries/_query.ts +++ b/waspc/data/Generator/templates/server/src/queries/_query.ts @@ -1,5 +1,5 @@ {{={= =}=}} -import prisma from '../dbClient.js' +import prisma from 'wasp/server/dbClient' {=& jsFn.importStatement =} diff --git a/waspc/data/Generator/templates/server/src/routes/apis/index.ts b/waspc/data/Generator/templates/server/src/routes/apis/index.ts index 5c928f81dc..8356536a3f 100644 --- a/waspc/data/Generator/templates/server/src/routes/apis/index.ts +++ b/waspc/data/Generator/templates/server/src/routes/apis/index.ts @@ -1,6 +1,6 @@ {{={= =}=}} import express from 'express' -import prisma from '../../dbClient.js' +import prisma from 'wasp/server/dbClient' import { handleRejection } from '../../utils.js' import { MiddlewareConfigFn, globalMiddlewareConfigForExpress } from '../../middleware/index.js' {=# isAuthEnabled =} diff --git a/waspc/data/Generator/templates/server/src/webSocket/index.ts b/waspc/data/Generator/templates/server/src/webSocket/index.ts index 3393b500bc..e3cdd9d620 100644 --- a/waspc/data/Generator/templates/server/src/webSocket/index.ts +++ b/waspc/data/Generator/templates/server/src/webSocket/index.ts @@ -3,7 +3,7 @@ import { Server } from 'socket.io' import { EventsMap, DefaultEventsMap } from '@socket.io/component-emitter' -import prisma from '../dbClient.js' +import prisma from 'wasp/server/dbClient' {=# isAuthEnabled =} import { type SanitizedUser } from '../_types/index.js' {=/ isAuthEnabled =} diff --git a/waspc/data/Generator/templates/server/src/webSocket/initialization.ts b/waspc/data/Generator/templates/server/src/webSocket/initialization.ts index f467d0de7d..82789ef03e 100644 --- a/waspc/data/Generator/templates/server/src/webSocket/initialization.ts +++ b/waspc/data/Generator/templates/server/src/webSocket/initialization.ts @@ -5,7 +5,7 @@ import { Server, Socket } from 'socket.io' import type { ServerType } from './index.js' import config from '../config.js' -import prisma from '../dbClient.js' +import prisma from 'wasp/server/dbClient' {=# isAuthEnabled =} import { getUserFromToken } from '../core/auth.js' diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json index ead89c3038..69fd0b8197 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json @@ -23,7 +23,8 @@ "./operations": "./operations/index.ts", "./operations/*": "./operations/*", "./universal/url": "./universal/url.ts", - "./universal/types": "./universal/url.ts" + "./universal/types": "./universal/url.ts", + "./server/dbClient": "./server/dbClient.js" }, "license": "ISC", "include": [ diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/dbClient.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/dbClient.ts deleted file mode 100644 index 66e7801be3..0000000000 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/dbClient.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Prisma from '@prisma/client' - - -const createDbClient = () => { - const prismaClient = new Prisma.PrismaClient() - - return prismaClient -} - -const dbClient = createDbClient() - -export default dbClient diff --git a/waspc/src/Wasp/AppSpec.hs b/waspc/src/Wasp/AppSpec.hs index 555dbe724e..ed75ad9d08 100644 --- a/waspc/src/Wasp/AppSpec.hs +++ b/waspc/src/Wasp/AppSpec.hs @@ -38,8 +38,8 @@ import Wasp.AppSpec.Core.Decl (Decl, IsDecl, takeDecls) import Wasp.AppSpec.Core.Ref (Ref, refName) import Wasp.AppSpec.Crud (Crud) import Wasp.AppSpec.Entity (Entity) -import Wasp.AppSpec.ExternalCode (SourceExternalCodeDir) -import qualified Wasp.AppSpec.ExternalCode as ExternalCode +import Wasp.AppSpec.ExternalFiles (SourceExternalCodeDir) +import qualified Wasp.AppSpec.ExternalFiles as ExternalFiles import Wasp.AppSpec.Job (Job) import Wasp.AppSpec.Operation (Operation) import qualified Wasp.AppSpec.Operation as AS.Operation @@ -64,13 +64,9 @@ data AppSpec = AppSpec packageJson :: PackageJson, -- | Absolute path to the directory containing the wasp project. waspProjectDir :: Path' Abs (Dir WaspProjectDir), - -- | List of external server code files (they are referenced/used in the declarations). - externalServerFiles :: [ExternalCode.File], - -- | List of external client code files (they are referenced/used in the declarations). - externalClientFiles :: [ExternalCode.File], - -- | List of files with external code shared between the server and the client. - externalSharedFiles :: [ExternalCode.File], - -- | Absolute path to the directory in wasp project source that contains external code files. + -- | List of external code files (they are referenced/used in the declarations). + externalCodeFiles :: [ExternalFiles.CodeFile], + externalPublicFiles :: [ExternalFiles.PublicFile], migrationsDir :: Maybe (Path' Abs (Dir DbMigrationsDir)), -- | Env variables to be provided to the server only during the development. devEnvVarsServer :: [EnvVar], diff --git a/waspc/src/Wasp/AppSpec/ExtImport.hs b/waspc/src/Wasp/AppSpec/ExtImport.hs index 4680d75e37..cc2b347826 100644 --- a/waspc/src/Wasp/AppSpec/ExtImport.hs +++ b/waspc/src/Wasp/AppSpec/ExtImport.hs @@ -13,7 +13,7 @@ import Data.Aeson (FromJSON, ToJSON) import Data.Data (Data) import GHC.Generics (Generic) import StrongPath (File', Path, Posix, Rel) -import Wasp.AppSpec.ExternalCode (SourceExternalCodeDir) +import Wasp.AppSpec.ExternalFiles (SourceExternalCodeDir) data ExtImport = ExtImport { -- | What is being imported. diff --git a/waspc/src/Wasp/AppSpec/ExternalCode.hs b/waspc/src/Wasp/AppSpec/ExternalCode.hs deleted file mode 100644 index 7f83db7310..0000000000 --- a/waspc/src/Wasp/AppSpec/ExternalCode.hs +++ /dev/null @@ -1,52 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} - -module Wasp.AppSpec.ExternalCode - ( -- | Wasp project consists of Wasp code (.wasp files) and external code (e.g. .js files) that is - -- used/referenced by the Wasp code. - -- Therefore, the whole specification of the web app is not just Wasp code, but a combination of - -- Wasp code and external code. - -- Main unit of external code is File, and external code is currently all organized in a single - -- directory in Wasp project which we call source external code dir (source because it is in the - -- Wasp project \/ source dir, and not in the generated project \/ source). - File (..), - filePathInExtCodeDir, - fileAbsPath, - fileText, - SourceExternalCodeDir, - ) -where - -import Data.Data (Data) -import Data.Text (Text) -import qualified Data.Text.Lazy as TextL -import StrongPath (Abs, Dir, File', Path', Rel, ()) - --- | Directory in Wasp source that contains external code. --- External code files are obtained from it. -data SourceExternalCodeDir deriving (Data) - -data File = File - { _pathInExtCodeDir :: !(Path' (Rel SourceExternalCodeDir) File'), - _extCodeDirPath :: !(Path' Abs (Dir SourceExternalCodeDir)), - -- | File content. Since it is lazy, it might throw error when evaluated, - -- since reading will happen only then. E.g. it will throw error if file is not a textual file. - _text :: TextL.Text - } - -instance Show File where - show = show . _pathInExtCodeDir - -instance Eq File where - f1 == f2 = _pathInExtCodeDir f1 == _pathInExtCodeDir f2 - --- | Returns path relative to the external code directory. -filePathInExtCodeDir :: File -> Path' (Rel SourceExternalCodeDir) File' -filePathInExtCodeDir = _pathInExtCodeDir - --- | Unsafe method: throws error if text could not be read (if file is not a textual file)! -fileText :: File -> Text -fileText = TextL.toStrict . _text - --- | Returns absolute path of the external code file. -fileAbsPath :: File -> Path' Abs File' -fileAbsPath file = _extCodeDirPath file _pathInExtCodeDir file diff --git a/waspc/src/Wasp/AppSpec/ExternalFiles.hs b/waspc/src/Wasp/AppSpec/ExternalFiles.hs new file mode 100644 index 0000000000..f75b39c69d --- /dev/null +++ b/waspc/src/Wasp/AppSpec/ExternalFiles.hs @@ -0,0 +1,67 @@ +{-# LANGUAGE DeriveDataTypeable #-} + +module Wasp.AppSpec.ExternalFiles + ( -- | Wasp project consists of Wasp code (.wasp files) and external files. + -- External files can be either: + -- - External code files (e.g., JS, TS) used/referenced by Wasp code. These + -- files reside in the \/src directory which we call the "source external code dir". + -- - External (static) public files served unaltered by Wasp. These + -- files reside in the \/public directory which we call the "source external public dir". + -- We call these directories "source" directories because they're found in + -- the project's source dir, not in the generated project's source dir. + -- + -- Therefore, the whole specification of the web app is not just Wasp code, but a combination of + -- Wasp code and external files. + CodeFile (..), + PublicFile (..), + filePathInExtCodeDir, + fileAbsPath, + fileText, + SourceExternalCodeDir, + SourceExternalPublicDir, + ) +where + +import Data.Data (Data) +import Data.Text (Text) +import qualified Data.Text.Lazy as TextL +import StrongPath (Abs, Dir, File', Path', Rel, ()) + +-- | Directory in the Wasp project that contains external code. +-- External code files are obtained from it. +data SourceExternalCodeDir deriving (Data) + +-- | Directory in Wasp project that contains external public static files. +-- Public files are obtained from it. +data SourceExternalPublicDir deriving (Data) + +data CodeFile = CodeFile + { _pathInExtCodeDir :: !(Path' (Rel SourceExternalCodeDir) File'), + _extCodeDirPath :: !(Path' Abs (Dir SourceExternalCodeDir)), + -- | File content. Since it is lazy, it might throw error when evaluated, + -- since reading will happen only then. E.g. it will throw error if file is not a textual file. + _text :: TextL.Text + } + +data PublicFile = PublicFile + { _pathInPublicDir :: !(Path' (Rel SourceExternalPublicDir) File'), + _publicDirPath :: !(Path' Abs (Dir SourceExternalPublicDir)) + } + +instance Show CodeFile where + show = show . _pathInExtCodeDir + +instance Eq CodeFile where + f1 == f2 = _pathInExtCodeDir f1 == _pathInExtCodeDir f2 + +-- | Returns path relative to the external code directory. +filePathInExtCodeDir :: CodeFile -> Path' (Rel SourceExternalCodeDir) File' +filePathInExtCodeDir = _pathInExtCodeDir + +-- | Unsafe method: throws error if text could not be read (if file is not a textual file)! +fileText :: CodeFile -> Text +fileText = TextL.toStrict . _text + +-- | Returns absolute path of the external code file. +fileAbsPath :: CodeFile -> Path' Abs File' +fileAbsPath file = _extCodeDirPath file _pathInExtCodeDir file diff --git a/waspc/src/Wasp/AppSpec/Valid.hs b/waspc/src/Wasp/AppSpec/Valid.hs index c55fffd7f2..02bb1d10a4 100644 --- a/waspc/src/Wasp/AppSpec/Valid.hs +++ b/waspc/src/Wasp/AppSpec/Valid.hs @@ -298,7 +298,7 @@ validateWebAppBaseDir :: AppSpec -> [ValidationError] validateWebAppBaseDir spec = case maybeBaseDir of Just baseDir | not (startsWithSlash baseDir) -> - [GenericValidationError "The app.client.baseDir should start with a slash e.g. \"/test\""] + [GenericValidationError "The app.client.baseDir should start with a slash e.g. \"/test\""] _anyOtherCase -> [] where maybeBaseDir = Client.baseDir =<< AS.App.client (snd $ getApp spec) diff --git a/waspc/src/Wasp/CompileOptions.hs b/waspc/src/Wasp/CompileOptions.hs index d0174cd9b7..02eb4d0ff3 100644 --- a/waspc/src/Wasp/CompileOptions.hs +++ b/waspc/src/Wasp/CompileOptions.hs @@ -4,17 +4,15 @@ module Wasp.CompileOptions where import StrongPath (Abs, Dir, Path') -import Wasp.AppSpec.ExternalCode (SourceExternalCodeDir) import Wasp.Generator.Monad (GeneratorWarning) import Wasp.Message (SendMessage) +import Wasp.Project.Common (WaspProjectDir) -- TODO(martin): Should these be merged with Wasp data? Is it really a separate thing or not? -- It would be easier to pass around if it is part of Wasp data. But is it semantically correct? -- Maybe it is, even more than this! data CompileOptions = CompileOptions - { externalServerCodeDirPath :: !(Path' Abs (Dir SourceExternalCodeDir)), - externalClientCodeDirPath :: !(Path' Abs (Dir SourceExternalCodeDir)), - externalSharedCodeDirPath :: !(Path' Abs (Dir SourceExternalCodeDir)), + { waspProjectDirPath :: !(Path' Abs (Dir WaspProjectDir)), isBuild :: !Bool, -- We give the compiler the ability to send messages. The code that -- invokes the compiler (such as the CLI) can then implement a way diff --git a/waspc/src/Wasp/Generator/DbGenerator.hs b/waspc/src/Wasp/Generator/DbGenerator.hs index ff976660fc..bd2411f113 100644 --- a/waspc/src/Wasp/Generator/DbGenerator.hs +++ b/waspc/src/Wasp/Generator/DbGenerator.hs @@ -200,7 +200,7 @@ generatePrismaClient spec projectRootDir = generatePrismaClientIfEntitiesExist :: IO (Maybe GeneratorError) generatePrismaClientIfEntitiesExist | entitiesExist = - either (Just . GenericGeneratorError) (const Nothing) <$> DbOps.generatePrismaClient projectRootDir + either (Just . GenericGeneratorError) (const Nothing) <$> DbOps.generatePrismaClient projectRootDir | otherwise = return Nothing entitiesExist = not . null $ getEntities spec diff --git a/waspc/src/Wasp/Generator/ExternalCodeGenerator.hs b/waspc/src/Wasp/Generator/ExternalCodeGenerator.hs deleted file mode 100644 index f2875db8eb..0000000000 --- a/waspc/src/Wasp/Generator/ExternalCodeGenerator.hs +++ /dev/null @@ -1,38 +0,0 @@ -module Wasp.Generator.ExternalCodeGenerator - ( genExternalCodeDir, - ) -where - -import Data.Maybe (mapMaybe) -import qualified StrongPath as SP -import qualified System.FilePath as FP -import qualified Wasp.AppSpec.ExternalCode as EC -import qualified Wasp.Generator.ExternalCodeGenerator.Common as C -import Wasp.Generator.ExternalCodeGenerator.Js (genSourceFile) -import Wasp.Generator.FileDraft (FileDraft) -import qualified Wasp.Generator.FileDraft as FD -import Wasp.Generator.Monad (Generator) - --- | Takes external code files from Wasp and generates them in new location as part of the generated project. --- It might not just copy them but also do some changes on them, as needed. -genExternalCodeDir :: - C.ExternalCodeGeneratorStrategy -> - [EC.File] -> - Generator [FD.FileDraft] -genExternalCodeDir strategy = sequence . mapMaybe (genFile strategy) - -genFile :: C.ExternalCodeGeneratorStrategy -> EC.File -> Maybe (Generator FD.FileDraft) -genFile strategy file - | fileName == "tsconfig.json" = Nothing - | extension `elem` [".js", ".jsx", ".ts", ".tsx"] = Just $ genSourceFile strategy file - | otherwise = Just $ genResourceFile strategy file - where - extension = FP.takeExtension filePath - fileName = FP.takeFileName filePath - filePath = SP.toFilePath $ EC.filePathInExtCodeDir file - -genResourceFile :: C.ExternalCodeGeneratorStrategy -> EC.File -> Generator FileDraft -genResourceFile strategy file = return $ FD.createCopyFileDraft relDstPath absSrcPath - where - relDstPath = C._resolveDstFilePath strategy $ EC.filePathInExtCodeDir file - absSrcPath = EC.fileAbsPath file diff --git a/waspc/src/Wasp/Generator/ExternalCodeGenerator/Common.hs b/waspc/src/Wasp/Generator/ExternalCodeGenerator/Common.hs index 184f7dc435..e8d44e3f8a 100644 --- a/waspc/src/Wasp/Generator/ExternalCodeGenerator/Common.hs +++ b/waspc/src/Wasp/Generator/ExternalCodeGenerator/Common.hs @@ -1,17 +1,9 @@ module Wasp.Generator.ExternalCodeGenerator.Common - ( ExternalCodeGeneratorStrategy (..), - GeneratedExternalCodeDir, - castRelPathFromSrcToGenExtCodeDir, - asGenExtFile, + ( GeneratedExternalCodeDir, ) where -import Data.Text (Text) -import StrongPath (File', Path', Rel) -import qualified StrongPath as SP -import Wasp.AppSpec.ExternalCode (SourceExternalCodeDir) -import Wasp.Generator.Common (ProjectRootDir) - +-- todo(filip): Where should I put this? -- TODO: consider refactoring the usage of GeneratedExternalCodeDir since -- generated code might end up in multiple places (e.g. ext-src/ but also public/). -- Name should probably be narrowed down to something that represent only the ext-src/ @@ -19,17 +11,3 @@ import Wasp.Generator.Common (ProjectRootDir) -- | Path to the directory where ext code will be generated. data GeneratedExternalCodeDir - -asGenExtFile :: Path' (Rel d) File' -> Path' (Rel GeneratedExternalCodeDir) File' -asGenExtFile = SP.castRel - -castRelPathFromSrcToGenExtCodeDir :: Path' (Rel SourceExternalCodeDir) a -> Path' (Rel GeneratedExternalCodeDir) a -castRelPathFromSrcToGenExtCodeDir = SP.castRel - -data ExternalCodeGeneratorStrategy = ExternalCodeGeneratorStrategy - { -- | Takes a path where the external code js file will be generated. - -- Also takes text of the file. Returns text where special @wasp imports have been replaced with - -- imports that will work. - _resolveJsFileWaspImports :: Path' (Rel GeneratedExternalCodeDir) File' -> Text -> Text, - _resolveDstFilePath :: Path' (Rel SourceExternalCodeDir) File' -> Path' (Rel ProjectRootDir) File' - } diff --git a/waspc/src/Wasp/Generator/ExternalCodeGenerator/Js.hs b/waspc/src/Wasp/Generator/ExternalCodeGenerator/Js.hs deleted file mode 100644 index 1cd956a56c..0000000000 --- a/waspc/src/Wasp/Generator/ExternalCodeGenerator/Js.hs +++ /dev/null @@ -1,44 +0,0 @@ -module Wasp.Generator.ExternalCodeGenerator.Js - ( genSourceFile, - resolveJsFileWaspImportsForExtCodeDir, - ) -where - -import Data.Maybe (fromJust) -import Data.Text (Text, unpack) -import qualified Data.Text as T -import FilePath.Extra (reversePosixPath) -import StrongPath (Dir, File', Path', Rel, ()) -import qualified StrongPath as SP -import qualified Text.Regex.TDFA as TR -import qualified Wasp.AppSpec.ExternalCode as EC -import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir) -import qualified Wasp.Generator.ExternalCodeGenerator.Common as C -import qualified Wasp.Generator.FileDraft as FD -import Wasp.Generator.Monad (Generator) - -genSourceFile :: C.ExternalCodeGeneratorStrategy -> EC.File -> Generator FD.FileDraft -genSourceFile strategy file = return $ FD.createTextFileDraft dstPath text - where - filePathInSrcExtCodeDir = EC.filePathInExtCodeDir file - text = EC.fileText file - dstPath = C._resolveDstFilePath strategy filePathInSrcExtCodeDir - --- | Replaces imports that start with "@wasp/" with imports that start from the src dir of the app. -resolveJsFileWaspImportsForExtCodeDir :: - -- | Relative path of ext code dir in src dir of app (web app, server (app), ...) - Path' (Rel ()) (Dir GeneratedExternalCodeDir) -> - -- | Path where this JS file will be generated. - Path' (Rel GeneratedExternalCodeDir) File' -> - -- | Original text of the file. - Text -> - -- | Text of the file with special "@wasp" imports resolved (replaced with normal JS imports). - Text -resolveJsFileWaspImportsForExtCodeDir extCodeDirInAppSrcDir jsFileDstPathInExtCodeDir jsFileText = - let matches = concat (unpack jsFileText TR.=~ ("(from +['\"]@wasp/)" :: String) :: [[String]]) - in foldr replaceFromWasp jsFileText matches - where - replaceFromWasp fromWasp = T.replace (T.pack fromWasp) (T.pack $ transformFromWasp fromWasp) - transformFromWasp fromWasp = reverse (drop (length ("@wasp/" :: String)) $ reverse fromWasp) ++ pathPrefix ++ "/" - pathPrefix = reversePosixPath $ SP.fromRelDirP $ fromJust $ SP.relDirToPosix $ SP.parent jsFileDstPathInAppSrcDir - jsFileDstPathInAppSrcDir = extCodeDirInAppSrcDir jsFileDstPathInExtCodeDir diff --git a/waspc/src/Wasp/Generator/JsImport.hs b/waspc/src/Wasp/Generator/JsImport.hs index c5da3f7a74..25db2c7df3 100644 --- a/waspc/src/Wasp/Generator/JsImport.hs +++ b/waspc/src/Wasp/Generator/JsImport.hs @@ -34,22 +34,6 @@ extImportToJsImport pathFromSrcDirToExtCodeDir pathFromImportLocationToSrcDir ex extImportNameToJsImportName (EI.ExtImportModule name) = JsImportModule name extImportNameToJsImportName (EI.ExtImportField name) = JsImportField name --- filip: attempt to simplify how we generate imports. I wanted to generate a --- module import (e.g., '@ext-src/something') and couldn't do it --- jsImportToImportJsonRaw :: Maybe (FilePath, JsImportName, Maybe JsImportAlias) -> Aeson.Value --- jsImportToImportJsonRaw importData = maybe notDefinedValue mkTmplData importData --- where --- notDefinedValue = object ["isDefined" .= False] - --- mkTmplData :: (FilePath, JsImportName, Maybe JsImportAlias) -> Aeson.Value --- mkTmplData (importPath, importName, maybeImportAlias) = --- let (jsImportStmt, jsImportIdentifier) = getJsImportStmtAndIdentifierRaw importPath importName maybeImportAlias --- in object --- [ "isDefined" .= True, --- "importStatement" .= jsImportStmt, --- "importIdentifier" .= jsImportIdentifier --- ] - jsImportToImportJson :: Maybe JsImport -> Aeson.Value jsImportToImportJson maybeJsImport = maybe notDefinedValue mkTmplData maybeJsImport where diff --git a/waspc/src/Wasp/Generator/SdkGenerator.hs b/waspc/src/Wasp/Generator/SdkGenerator.hs index 04b02e0830..63c7bc7688 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator.hs @@ -3,27 +3,32 @@ module Wasp.Generator.SdkGenerator ( genSdk, installNpmDependencies, + genExternalCodeDir, ) where import Data.Aeson (object) import qualified Data.Aeson as Aeson import Data.Aeson.Types ((.=)) -import Data.Maybe (fromMaybe, isJust) +import Data.Maybe (fromMaybe, isJust, mapMaybe) import GHC.IO (unsafePerformIO) import StrongPath import qualified StrongPath as SP +import qualified System.FilePath as FP import Wasp.AppSpec import qualified Wasp.AppSpec as AS import qualified Wasp.AppSpec.App as AS.App import qualified Wasp.AppSpec.App.Auth as AS.App.Auth import qualified Wasp.AppSpec.App.Dependency as AS.Dependency import qualified Wasp.AppSpec.Entity as AS.Entity +import qualified Wasp.AppSpec.ExternalFiles as EC import Wasp.AppSpec.Valid (isAuthEnabled) import qualified Wasp.AppSpec.Valid as AS.Valid import Wasp.Generator.Common (ProjectRootDir, makeJsonWithEntityData, prismaVersion) import qualified Wasp.Generator.DbGenerator.Auth as DbAuth +import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir) import Wasp.Generator.FileDraft (FileDraft, createCopyDirFileDraft, createTemplateFileDraft) +import qualified Wasp.Generator.FileDraft as FD import Wasp.Generator.FileDraft.CopyDirFileDraft (CopyDirFileDraftDstDirStrategy (RemoveExistingDstDir)) import qualified Wasp.Generator.Job as J import Wasp.Generator.Job.Process (runNodeCommandAsJob) @@ -35,7 +40,15 @@ import qualified Wasp.SemanticVersion as SV import Wasp.Util (toLowerFirst, (<++>)) genSdk :: AppSpec -> Generator [FileDraft] -genSdk spec = (:) <$> genPackageJson spec <*> genHardcodedSdkModules <++> genSdkModules spec +genSdk spec = + sequence + [ genFileCopy [relfile|server/dbClient.js|], + genPackageJson spec + ] + <++> genHardcodedSdkModules + <++> genSdkModules spec + where + genFileCopy = return . mkTmplFd data SdkRootDir @@ -47,6 +60,7 @@ genSdkModules spec = [ genFileCopy [relfile|api/index.ts|], genFileCopy [relfile|api/events.ts|] ] + <++> genExternalCodeDir (AS.externalCodeFiles spec) <++> genTypesAndEntitiesDirs spec where genFileCopy = return . mkTmplFd @@ -61,7 +75,6 @@ genHardcodedSdkModules = copyFolder [reldir|rpc|], copyFolder [reldir|server/actions|], copyFolder [reldir|server/queries|], - copyFile [relfile|server/dbClient.ts|], copyFile [relfile|server/utils.ts|], copyFolder [reldir|types|], copyFolder [reldir|universal|] @@ -192,8 +205,41 @@ sdkRootDirInProjectRootDir = [reldir|sdk/wasp|] sdkTemplatesDirInTemplatesDir :: Path' (Rel TemplatesDir) (Dir SdkTemplatesDir) sdkTemplatesDirInTemplatesDir = [reldir|sdk|] +extSrcDirInSdkRootDir :: Path' (Rel SdkRootDir) (Dir GeneratedExternalCodeDir) +extSrcDirInSdkRootDir = [reldir|ext-src|] + -- todo(filip): figure out where this belongs -- also, fix imports for wasp project installNpmDependencies :: Path' Abs (Dir WaspProjectDir) -> J.Job installNpmDependencies projectDir = runNodeCommandAsJob projectDir "npm" ["install"] J.Wasp + +-- todo(filip): consider reorganizing/splitting the file. + +-- | Takes external code files from Wasp and generates them in new location as part of the generated project. +-- It might not just copy them but also do some changes on them, as needed. +genExternalCodeDir :: [EC.CodeFile] -> Generator [FileDraft] +genExternalCodeDir = sequence . mapMaybe genFile + +genFile :: EC.CodeFile -> Maybe (Generator FileDraft) +genFile file + | fileName == "tsconfig.json" = Nothing + | extension `elem` [".js", ".jsx", ".ts", ".tsx"] = Just $ genSourceFile file + | otherwise = Just $ genResourceFile file + where + extension = FP.takeExtension filePath + fileName = FP.takeFileName filePath + filePath = SP.toFilePath $ EC.filePathInExtCodeDir file + +genResourceFile :: EC.CodeFile -> Generator FileDraft +genResourceFile file = return $ FD.createCopyFileDraft relDstPath absSrcPath + where + relDstPath = sdkRootDirInProjectRootDir extSrcDirInSdkRootDir SP.castRel (EC._pathInExtCodeDir file) + absSrcPath = EC.fileAbsPath file + +genSourceFile :: EC.CodeFile -> Generator FD.FileDraft +genSourceFile file = return $ FD.createTextFileDraft relDstPath text + where + filePathInSrcExtCodeDir = EC.filePathInExtCodeDir file + text = EC.fileText file + relDstPath = sdkRootDirInProjectRootDir extSrcDirInSdkRootDir SP.castRel filePathInSrcExtCodeDir diff --git a/waspc/src/Wasp/Generator/ServerGenerator.hs b/waspc/src/Wasp/Generator/ServerGenerator.hs index 78ef5f6368..5009f517a4 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator.hs @@ -15,8 +15,7 @@ import Data.Aeson (object, (.=)) import qualified Data.Aeson as Aeson import qualified Data.ByteString.Lazy.UTF8 as ByteStringLazyUTF8 import Data.Maybe - ( fromJust, - isJust, + ( isJust, maybeToList, ) import StrongPath @@ -26,7 +25,6 @@ import StrongPath Path', Posix, Rel, - reldir, reldirP, relfile, (), @@ -76,9 +74,6 @@ genServer spec = genGitignore ] <++> genSrcDir spec - -- Filip: I don't generate external source folders as we're importing the user's code direclty (see ServerGenerator/JsImport.hs). - -- <++> genExternalCodeDir extServerCodeGeneratorStrategy (AS.externalServerFiles spec) - -- <++> genExternalCodeDir extSharedCodeGeneratorStrategy (AS.externalSharedFiles spec) <++> genDotEnv spec <++> genJobs spec <++> genJobExecutors spec @@ -216,7 +211,6 @@ genSrcDir spec = [ genFileCopy [relfile|app.js|], genFileCopy [relfile|core/AuthError.js|], genFileCopy [relfile|core/HttpError.js|], - genDbClient spec, genConfigFile spec, genServerJs spec ] @@ -232,24 +226,6 @@ genSrcDir spec = where genFileCopy = return . C.mkSrcTmplFd -genDbClient :: AppSpec -> Generator FileDraft -genDbClient spec = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) - where - maybeAuth = AS.App.auth $ snd $ getApp spec - - dbClientRelToSrcP = [relfile|dbClient.ts|] - tmplFile = C.asTmplFile $ [reldir|src|] dbClientRelToSrcP - dstFile = C.serverSrcDirInServerRootDir C.asServerSrcFile dbClientRelToSrcP - - tmplData = - if isJust maybeAuth - then - object - [ "isAuthEnabled" .= True, - "userEntityUpper" .= (AS.refName (AS.App.Auth.userEntity $ fromJust maybeAuth) :: String) - ] - else object [] - genServerJs :: AppSpec -> Generator FileDraft genServerJs spec = return $ diff --git a/waspc/src/Wasp/Generator/ServerGenerator/ExternalCodeGenerator.hs b/waspc/src/Wasp/Generator/ServerGenerator/ExternalCodeGenerator.hs deleted file mode 100644 index 7d69a5b2a2..0000000000 --- a/waspc/src/Wasp/Generator/ServerGenerator/ExternalCodeGenerator.hs +++ /dev/null @@ -1,43 +0,0 @@ -module Wasp.Generator.ServerGenerator.ExternalCodeGenerator - ( extServerCodeGeneratorStrategy, - extServerCodeDirInServerSrcDir, - extSharedCodeGeneratorStrategy, - ) -where - -import StrongPath (Dir, Path', Rel, reldir, ()) -import qualified StrongPath as SP -import Wasp.Generator.ExternalCodeGenerator.Common - ( ExternalCodeGeneratorStrategy (..), - GeneratedExternalCodeDir, - castRelPathFromSrcToGenExtCodeDir, - ) -import Wasp.Generator.ExternalCodeGenerator.Js (resolveJsFileWaspImportsForExtCodeDir) -import qualified Wasp.Generator.ServerGenerator.Common as C - -extServerCodeGeneratorStrategy :: ExternalCodeGeneratorStrategy -extServerCodeGeneratorStrategy = mkExtCodeGeneratorStrategy extServerCodeDirInServerSrcDir - -extSharedCodeGeneratorStrategy :: ExternalCodeGeneratorStrategy -extSharedCodeGeneratorStrategy = mkExtCodeGeneratorStrategy extSharedCodeDirInServerSrcDir - --- | Relative path to the directory where external server code will be generated. --- Relative to the server src dir. -extServerCodeDirInServerSrcDir :: Path' (Rel C.ServerSrcDir) (Dir GeneratedExternalCodeDir) -extServerCodeDirInServerSrcDir = [reldir|ext-src|] - --- | Relative path to the directory where external shared code will be generated. --- Relative to the server src dir. -extSharedCodeDirInServerSrcDir :: Path' (Rel C.ServerSrcDir) (Dir GeneratedExternalCodeDir) -extSharedCodeDirInServerSrcDir = [reldir|shared|] - -mkExtCodeGeneratorStrategy :: Path' (Rel C.ServerSrcDir) (Dir GeneratedExternalCodeDir) -> ExternalCodeGeneratorStrategy -mkExtCodeGeneratorStrategy extCodeDirInServerSrcDir = - ExternalCodeGeneratorStrategy - { _resolveJsFileWaspImports = resolveJsFileWaspImportsForExtCodeDir (SP.castRel extCodeDirInServerSrcDir), - _resolveDstFilePath = \filePath -> - C.serverRootDirInProjectRootDir - C.serverSrcDirInServerRootDir - extCodeDirInServerSrcDir - castRelPathFromSrcToGenExtCodeDir filePath - } diff --git a/waspc/src/Wasp/Generator/WebAppGenerator.hs b/waspc/src/Wasp/Generator/WebAppGenerator.hs index d873dd69db..26866eb32e 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator.hs @@ -31,7 +31,7 @@ import qualified Wasp.AppSpec.App.Client as AS.App.Client import qualified Wasp.AppSpec.App.Dependency as AS.Dependency import Wasp.AppSpec.App.WebSocket (WebSocket (..)) import qualified Wasp.AppSpec.Entity as AS.Entity -import Wasp.AppSpec.ExternalCode (SourceExternalCodeDir) +import Wasp.AppSpec.ExternalFiles (SourceExternalCodeDir) import Wasp.AppSpec.Valid (getApp) import Wasp.Env (envVarsToDotEnvContent) import Wasp.Generator.Common @@ -39,8 +39,6 @@ import Wasp.Generator.Common ) import qualified Wasp.Generator.ConfigFile as G.CF import qualified Wasp.Generator.DbGenerator.Auth as DbAuth -import Wasp.Generator.ExternalCodeGenerator (genExternalCodeDir) -import qualified Wasp.Generator.ExternalCodeGenerator.Common as ECC import Wasp.Generator.FileDraft (FileDraft, createTextFileDraft) import qualified Wasp.Generator.FileDraft as FD import Wasp.Generator.JsImport (jsImportToImportJson) @@ -49,10 +47,6 @@ import qualified Wasp.Generator.NpmDependencies as N import Wasp.Generator.WebAppGenerator.AuthG (genAuth) import qualified Wasp.Generator.WebAppGenerator.Common as C import Wasp.Generator.WebAppGenerator.CrudG (genCrud) -import Wasp.Generator.WebAppGenerator.ExternalCodeGenerator - ( extClientCodeGeneratorStrategy, - ) -import qualified Wasp.Generator.WebAppGenerator.ExternalCodeGenerator as EC import Wasp.Generator.WebAppGenerator.JsImport (extImportToImportJson) import Wasp.Generator.WebAppGenerator.OperationsGenerator (genOperations) import Wasp.Generator.WebAppGenerator.RouterGenerator (genRouter) @@ -67,7 +61,6 @@ import Wasp.Util ((<++>)) genWebApp :: AppSpec -> Generator [FileDraft] genWebApp spec = do - extClientCodeFileDrafts <- genExternalCodeDir extClientCodeGeneratorStrategy (AS.externalClientFiles spec) sequence [ genFileCopy [relfile|README.md|], genFileCopy [relfile|tsconfig.json|], @@ -83,10 +76,7 @@ genWebApp spec = do genViteConfig spec ] <++> genSrcDir spec - -- Filip: I don't generate external source folders as we're importing the user's code direclty (see ServerGenerator/JsImport.hs). - -- <++> return extClientCodeFileDrafts - -- <++> genExternalCodeDir extSharedCodeGeneratorStrategy (AS.externalSharedFiles spec) - <++> genPublicDir spec extClientCodeFileDrafts + <++> genPublicDir spec <++> genDotEnv spec <++> genUniversalDir <++> genEnvValidationScript @@ -201,25 +191,24 @@ genGitignore = (C.asTmplFile [relfile|gitignore|]) (C.asWebAppFile [relfile|.gitignore|]) -genPublicDir :: AppSpec -> [FileDraft] -> Generator [FileDraft] -genPublicDir spec extCodeFileDrafts = +genPublicDir :: AppSpec -> Generator [FileDraft] +genPublicDir spec = return $ - ifUserDidntProvideFile genFaviconFd + extPublicFileDrafts + ++ ifUserDidntProvideFile genFaviconFd ++ ifUserDidntProvideFile genManifestFd where + publicFiles = AS.externalPublicFiles spec + extPublicFileDrafts = map C.mkPublicFileDraft publicFiles genFaviconFd = C.mkTmplFd (C.asTmplFile [relfile|public/favicon.ico|]) genManifestFd = C.mkTmplFdWithData tmplFile tmplData where tmplData = object ["appName" .= (fst (getApp spec) :: String)] tmplFile = C.asTmplFile [relfile|public/manifest.json|] - ifUserDidntProvideFile fileDraft = - if checkIfFileDraftExists fileDraft - then [] - else [fileDraft] - + ifUserDidntProvideFile fileDraft = [fileDraft | not (checkIfFileDraftExists fileDraft)] checkIfFileDraftExists = (`elem` existingDstPaths) . FD.getDstPath - existingDstPaths = map FD.getDstPath extCodeFileDrafts + existingDstPaths = map FD.getDstPath extPublicFileDrafts genIndexHtml :: AppSpec -> Generator FileDraft genIndexHtml spec = @@ -328,6 +317,7 @@ genWebSocketProvider spec = return $ C.mkTmplFdWithData tmplFile tmplData tmplData = object ["autoConnect" .= map toLower (show shouldAutoConnect)] tmplFile = C.asTmplFile [relfile|src/webSocket/WebSocketProvider.tsx|] +-- todo(filip): Take care of this as well genViteConfig :: AppSpec -> Generator FileDraft genViteConfig spec = return $ C.mkTmplFdWithData tmplFile tmplData where @@ -343,10 +333,6 @@ genViteConfig spec = return $ C.mkTmplFdWithData tmplFile tmplData makeCustomViteConfigJsImport pathToConfig = makeJsImport importPath importName where importPath = C.toViteImportPath $ fromJust $ SP.relFileToPosix pathToConfigInSrc - pathToConfigInSrc = - SP.castRel $ - C.webAppSrcDirInWebAppRootDir - EC.extClientCodeDirInWebAppSrcDir - ECC.castRelPathFromSrcToGenExtCodeDir pathToConfig + pathToConfigInSrc = SP.castRel $ C.webAppSrcDirInWebAppRootDir SP.castRel pathToConfig importName = JsImportModule "customViteConfig" diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/Common.hs b/waspc/src/Wasp/Generator/WebAppGenerator/Common.hs index 06005ca2bb..ef001efdef 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/Common.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/Common.hs @@ -6,6 +6,7 @@ module Wasp.Generator.WebAppGenerator.Common mkTmplFdWithDst, mkTmplFdWithData, mkTmplFdWithDstAndData, + mkPublicFileDraft, webAppSrcDirInProjectRootDir, webAppTemplatesDirInTemplatesDir, asTmplFile, @@ -34,6 +35,8 @@ import System.FilePath (splitExtension) import Wasp.AppSpec (AppSpec) import qualified Wasp.AppSpec.App as AS.App import qualified Wasp.AppSpec.App.Client as AS.App.Client +import Wasp.AppSpec.ExternalFiles (PublicFile (..)) +import qualified Wasp.AppSpec.ExternalFiles as EF import Wasp.AppSpec.Valid (getApp) import Wasp.Generator.Common ( GeneratedSrcDir, @@ -43,7 +46,7 @@ import Wasp.Generator.Common WebAppRootDir, universalTemplatesDirInTemplatesDir, ) -import Wasp.Generator.FileDraft (FileDraft, createTemplateFileDraft) +import Wasp.Generator.FileDraft (FileDraft, createCopyFileDraft, createTemplateFileDraft) import Wasp.Generator.Templates (TemplatesDir) data WebAppSrcDir @@ -108,6 +111,14 @@ mkTmplFdWithDst src dst = mkTmplFdWithDstAndData src dst Nothing mkTmplFdWithData :: Path' (Rel WebAppTemplatesDir) File' -> Aeson.Value -> FileDraft mkTmplFdWithData src tmplData = mkTmplFdWithDstAndData src (SP.castRel src) (Just tmplData) +mkPublicFileDraft :: PublicFile -> FileDraft +mkPublicFileDraft (PublicFile _pathInPublicDir _publicDirPath) = createCopyFileDraft dstPath srcPath + where + dstPath = webAppRootDirInProjectRootDir publicDirInWebAppRootDir _pathInPublicDir + srcPath = _publicDirPath _pathInPublicDir + publicDirInWebAppRootDir :: Path' (Rel WebAppRootDir) (Dir EF.SourceExternalPublicDir) + publicDirInWebAppRootDir = [reldir|public|] + mkTmplFdWithDstAndData :: Path' (Rel WebAppTemplatesDir) File' -> Path' (Rel WebAppRootDir) File' -> diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/ExternalCodeGenerator.hs b/waspc/src/Wasp/Generator/WebAppGenerator/ExternalCodeGenerator.hs deleted file mode 100644 index 7f17a18841..0000000000 --- a/waspc/src/Wasp/Generator/WebAppGenerator/ExternalCodeGenerator.hs +++ /dev/null @@ -1,57 +0,0 @@ -module Wasp.Generator.WebAppGenerator.ExternalCodeGenerator - ( extClientCodeGeneratorStrategy, - extSharedCodeGeneratorStrategy, - extClientCodeDirInWebAppSrcDir, - ) -where - -import Data.Maybe (fromJust) -import StrongPath (Dir, Path', Rel, reldir, ()) -import qualified StrongPath as SP -import Wasp.Generator.ExternalCodeGenerator.Common - ( ExternalCodeGeneratorStrategy (..), - GeneratedExternalCodeDir, - castRelPathFromSrcToGenExtCodeDir, - ) -import Wasp.Generator.ExternalCodeGenerator.Js (resolveJsFileWaspImportsForExtCodeDir) -import qualified Wasp.Generator.WebAppGenerator.Common as C -import Wasp.Util.FilePath (removePathPrefix) - -extClientCodeGeneratorStrategy :: ExternalCodeGeneratorStrategy -extClientCodeGeneratorStrategy = mkExtCodeGeneratorStrategy extClientCodeDirInWebAppSrcDir - -extSharedCodeGeneratorStrategy :: ExternalCodeGeneratorStrategy -extSharedCodeGeneratorStrategy = mkExtCodeGeneratorStrategy extSharedCodeDirInWebAppSrcDir - --- | Relative path to the directory where external client code will be generated. --- Relative to web app src dir. -extClientCodeDirInWebAppSrcDir :: Path' (Rel C.WebAppSrcDir) (Dir GeneratedExternalCodeDir) -extClientCodeDirInWebAppSrcDir = [reldir|ext-src|] - --- | Relative path to the directory where external shared code will be generated. --- Relative to web app src dir. -extSharedCodeDirInWebAppSrcDir :: Path' (Rel C.WebAppSrcDir) (Dir GeneratedExternalCodeDir) -extSharedCodeDirInWebAppSrcDir = [reldir|shared|] - -mkExtCodeGeneratorStrategy :: Path' (Rel C.WebAppSrcDir) (Dir GeneratedExternalCodeDir) -> ExternalCodeGeneratorStrategy -mkExtCodeGeneratorStrategy extCodeDirInWebAppSrcDir = - ExternalCodeGeneratorStrategy - { _resolveJsFileWaspImports = resolveJsFileWaspImportsForExtCodeDir (SP.castRel extCodeDirInWebAppSrcDir), - _resolveDstFilePath = resolveDstFilePath - } - where - resolveDstFilePath filePath = - case maybeFilePathInStaticAssetsDir of - Just filePathInStaticAssetsDir -> - C.webAppRootDirInProjectRootDir - C.staticAssetsDirInWebAppDir - fromJust (SP.parseRelFile filePathInStaticAssetsDir) - Nothing -> - C.webAppRootDirInProjectRootDir - C.webAppSrcDirInWebAppRootDir - extCodeDirInWebAppSrcDir - castRelPathFromSrcToGenExtCodeDir filePath - where - maybeFilePathInStaticAssetsDir = removePathPrefix staticAssetsDir (SP.fromRelFile filePath) - - staticAssetsDir = SP.fromRelDir C.staticAssetsDirInWebAppDir diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/JsImport.hs b/waspc/src/Wasp/Generator/WebAppGenerator/JsImport.hs index 8e83c4c427..850967ee6b 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/JsImport.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/JsImport.hs @@ -9,10 +9,7 @@ import qualified Wasp.Generator.JsImport as GJI import Wasp.Generator.WebAppGenerator.Common (WebAppSrcDir) import Wasp.JsImport ( JsImport, - JsImportIdentifier, - JsImportStatement, ) -import qualified Wasp.JsImport as JI extImportToImportJson :: Path Posix (Rel importLocation) (Dir WebAppSrcDir) -> @@ -22,32 +19,6 @@ extImportToImportJson pathFromImportLocationToSrcDir maybeExtImport = GJI.jsImpo where jsImport = extImportToJsImport pathFromImportLocationToSrcDir <$> maybeExtImport --- extImportToImportJson :: --- Path Posix (Rel importLocation) (Dir WebAppSrcDir) -> --- Maybe ExtImport -> --- Aeson.Value --- extImportToImportJson _ maybeExtImport = case maybeExtImport of --- Nothing -> object ["isDefined" .= False] --- Just extImport -> makeImportObject extImport --- where --- makeImportObject (ExtImport importName importPath) = --- let importClause = makeImportClause importName --- importPathStr = "ext-sdrc/" ++ SP.toFilePath importPath --- in object --- [ "isDefined" .= True, --- "importStatement" .= ("import " ++ importClause ++ "from \"" ++ importPathStr ++ "\""), --- "importIdentifier" .= importName --- ] --- makeImportClause = \case --- EI.ExtImportModule name -> name --- EI.ExtImportField name -> "{ " ++ name ++ " - -getJsImportStmtAndIdentifier :: - Path Posix (Rel importLocation) (Dir WebAppSrcDir) -> - EI.ExtImport -> - (JsImportStatement, JsImportIdentifier) -getJsImportStmtAndIdentifier pathFromImportLocationToSrcDir = JI.getJsImportStmtAndIdentifier . extImportToJsImport pathFromImportLocationToSrcDir - extImportToJsImport :: Path Posix (Rel importLocation) (Dir WebAppSrcDir) -> EI.ExtImport -> @@ -55,4 +26,7 @@ extImportToJsImport :: extImportToJsImport = GJI.extImportToJsImport webAppExtDir where -- filip: read notes in ServerGenerator/JsImport.hs + -- todo(filip): use WaspProjectDirInProjectRootDir (once you add it for + -- Prisma stuff) and other stuff from WebAppGenerator/Common to build this + -- directory. Do the same for the server webAppExtDir = [reldirP|../../../../src|] diff --git a/waspc/src/Wasp/Project/Analyze.hs b/waspc/src/Wasp/Project/Analyze.hs index b11326a4ec..7261d889b0 100644 --- a/waspc/src/Wasp/Project/Analyze.hs +++ b/waspc/src/Wasp/Project/Analyze.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE DeriveGeneric #-} - module Wasp.Project.Analyze ( analyzeWaspProject, readPackageJsonFile, @@ -22,13 +20,13 @@ import Wasp.CompileOptions (CompileOptions) import qualified Wasp.CompileOptions as CompileOptions import qualified Wasp.ConfigFile as CF import Wasp.Error (showCompilerErrorForTerminal) -import qualified Wasp.ExternalCode as ExternalCode import qualified Wasp.Generator.ConfigFile as G.CF import Wasp.Project.Common (CompileError, CompileWarning, WaspProjectDir, findFileInWaspProjectDir) import Wasp.Project.Db (makeDevDatabaseUrl) import Wasp.Project.Db.Migrations (findMigrationsDir) import Wasp.Project.Deployment (loadUserDockerfileContents) import Wasp.Project.Env (readDotEnvClient, readDotEnvServer) +import qualified Wasp.Project.ExternalFiles as ExternalFiles import Wasp.Project.Vite (findCustomViteConfigPath) import Wasp.Util (maybeToEither) import qualified Wasp.Util.IO as IOUtil @@ -67,14 +65,9 @@ constructAppSpec :: [AS.Decl] -> IO (Either [CompileError] AS.AppSpec, [CompileWarning]) constructAppSpec waspDir options packageJson decls = do - externalServerCodeFiles <- - ExternalCode.readFiles (CompileOptions.externalServerCodeDirPath options) - - let externalClientCodeDirPath = CompileOptions.externalClientCodeDirPath options - externalClientCodeFiles <- ExternalCode.readFiles externalClientCodeDirPath + externalCodeFiles <- ExternalFiles.readCodeFiles waspDir + externalPublicFiles <- ExternalFiles.readPublicFiles waspDir - externalSharedCodeFiles <- - ExternalCode.readFiles (CompileOptions.externalSharedCodeDirPath options) maybeMigrationsDir <- findMigrationsDir waspDir maybeUserDockerfileContents <- loadUserDockerfileContents waspDir configFiles <- CF.discoverConfigFiles waspDir G.CF.configFileRelocationMap @@ -82,15 +75,14 @@ constructAppSpec waspDir options packageJson decls = do serverEnvVars <- readDotEnvServer waspDir clientEnvVars <- readDotEnvClient waspDir - let customViteConfigPath = findCustomViteConfigPath externalClientCodeFiles + let customViteConfigPath = findCustomViteConfigPath externalCodeFiles let appSpec = AS.AppSpec { AS.decls = decls, AS.packageJson = packageJson, AS.waspProjectDir = waspDir, - AS.externalClientFiles = externalClientCodeFiles, - AS.externalServerFiles = externalServerCodeFiles, - AS.externalSharedFiles = externalSharedCodeFiles, + AS.externalCodeFiles = externalCodeFiles, + AS.externalPublicFiles = externalPublicFiles, AS.migrationsDir = maybeMigrationsDir, AS.devEnvVarsServer = serverEnvVars, AS.devEnvVarsClient = clientEnvVars, diff --git a/waspc/src/Wasp/Project/Common.hs b/waspc/src/Wasp/Project/Common.hs index 4ad3919143..84b959484c 100644 --- a/waspc/src/Wasp/Project/Common.hs +++ b/waspc/src/Wasp/Project/Common.hs @@ -1,5 +1,7 @@ module Wasp.Project.Common ( findFileInWaspProjectDir, + extCodeDirInWaspProjectDir, + extPublicDirInWaspProjectDir, CompileError, CompileWarning, WaspProjectDir, @@ -7,7 +9,9 @@ module Wasp.Project.Common where import StrongPath (Abs, Dir, File', Path', Rel, toFilePath, ()) +import StrongPath.TH (reldir) import System.Directory (doesFileExist) +import Wasp.AppSpec.ExternalFiles (SourceExternalCodeDir, SourceExternalPublicDir) data WaspProjectDir -- Root dir of Wasp project, containing source files. @@ -23,3 +27,9 @@ findFileInWaspProjectDir waspDir file = do let fileAbsFp = waspDir file fileExists <- doesFileExist $ toFilePath fileAbsFp return $ if fileExists then Just fileAbsFp else Nothing + +extCodeDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir SourceExternalCodeDir) +extCodeDirInWaspProjectDir = [reldir|src|] + +extPublicDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir SourceExternalPublicDir) +extPublicDirInWaspProjectDir = [reldir|public|] diff --git a/waspc/src/Wasp/ExternalCode.hs b/waspc/src/Wasp/Project/ExternalFiles.hs similarity index 57% rename from waspc/src/Wasp/ExternalCode.hs rename to waspc/src/Wasp/Project/ExternalFiles.hs index 40bf95fca0..3519140e9e 100644 --- a/waspc/src/Wasp/ExternalCode.hs +++ b/waspc/src/Wasp/Project/ExternalFiles.hs @@ -1,32 +1,37 @@ -module Wasp.ExternalCode - ( readFiles, +module Wasp.Project.ExternalFiles + ( readPublicFiles, + readCodeFiles, ) where import Data.Maybe (catMaybes) import qualified Data.Text.Lazy as TextL import qualified Data.Text.Lazy.IO as TextL.IO -import StrongPath (Abs, Dir, File', Path', Rel, relfile, ()) +import StrongPath (Abs, Dir, Path', ()) import qualified StrongPath as SP import System.IO.Error (isDoesNotExistError) import UnliftIO.Exception (catch, throwIO) -import Wasp.AppSpec.ExternalCode (File (..), SourceExternalCodeDir) -import qualified Wasp.Util.IO -import Wasp.WaspignoreFile (ignores, readWaspignoreFile) +import Wasp.AppSpec.ExternalFiles (CodeFile (CodeFile), PublicFile (PublicFile)) +import Wasp.Project.Common (WaspProjectDir, extCodeDirInWaspProjectDir, extPublicDirInWaspProjectDir) +import Wasp.Project.Waspignore (getNotIgnoredRelFilePaths, waspIgnorePathInWaspProjectDir) -waspignorePathInExtCodeDir :: Path' (Rel SourceExternalCodeDir) File' -waspignorePathInExtCodeDir = [relfile|.waspignore|] +-- | Returns all files contained in the specified ext public dir +-- except files ignores by the specified waspignore file. +readPublicFiles :: Path' Abs (Dir WaspProjectDir) -> IO [PublicFile] +readPublicFiles waspProjectDir = do + let externalPublicDirPath = waspProjectDir extPublicDirInWaspProjectDir + let waspignoreFilePath = waspProjectDir waspIgnorePathInWaspProjectDir + relFilePaths <- getNotIgnoredRelFilePaths waspignoreFilePath externalPublicDirPath + return $ map (`PublicFile` externalPublicDirPath) relFilePaths --- | Returns all files contained in the specified external code dir, recursively, +-- | Returns all files contained in the specified ext code dir -- except files ignores by the specified waspignore file. -readFiles :: Path' Abs (Dir SourceExternalCodeDir) -> IO [File] -readFiles extCodeDirPath = do - let waspignoreFilePath = extCodeDirPath waspignorePathInExtCodeDir - waspignoreFile <- readWaspignoreFile waspignoreFilePath - relFilePaths <- - filter (not . ignores waspignoreFile . SP.toFilePath) - <$> Wasp.Util.IO.listDirectoryDeep extCodeDirPath - let absFilePaths = map (extCodeDirPath ) relFilePaths +readCodeFiles :: Path' Abs (Dir WaspProjectDir) -> IO [CodeFile] +readCodeFiles waspProjectDir = do + let externalCodeDirPath = waspProjectDir extCodeDirInWaspProjectDir + let waspignoreFilePath = waspProjectDir waspIgnorePathInWaspProjectDir + relFilePaths <- getNotIgnoredRelFilePaths waspignoreFilePath externalCodeDirPath + let absFiles = map (externalCodeDirPath ) relFilePaths -- NOTE: We read text from all the files, regardless if they are text files or not, because -- we don't know if they are a text file or not. -- Since we do lazy reading (Text.Lazy), this is not a problem as long as we don't try to use @@ -40,8 +45,8 @@ readFiles extCodeDirPath = do -- or create new file draft that will support that. -- In generator, when creating TextFileDraft, give it function/logic for text transformation, -- and it will be taken care of when draft will be written to the disk. - fileTexts <- catMaybes <$> mapM (tryReadFile . SP.toFilePath) absFilePaths - let files = zipWith (`File` extCodeDirPath) relFilePaths fileTexts + fileTexts <- catMaybes <$> mapM (tryReadFile . SP.toFilePath) absFiles + let files = zipWith (`CodeFile` externalCodeDirPath) relFilePaths fileTexts return files where -- NOTE(matija): we had cases (e.g. tmp Vim files) where a file initially existed diff --git a/waspc/src/Wasp/Project/Vite.hs b/waspc/src/Wasp/Project/Vite.hs index 6208f4fcfb..12f7809524 100644 --- a/waspc/src/Wasp/Project/Vite.hs +++ b/waspc/src/Wasp/Project/Vite.hs @@ -2,17 +2,18 @@ module Wasp.Project.Vite where import Data.List (find) import StrongPath (File', Path', Rel, relfile) -import Wasp.AppSpec.ExternalCode (SourceExternalCodeDir) -import qualified Wasp.AppSpec.ExternalCode as ExternalCode +import Wasp.AppSpec.ExternalFiles (SourceExternalCodeDir) +import qualified Wasp.AppSpec.ExternalFiles as ExternalFiles -findCustomViteConfigPath :: [ExternalCode.File] -> Maybe (Path' (Rel SourceExternalCodeDir) File') -findCustomViteConfigPath externalClientCodeFiles = ExternalCode._pathInExtCodeDir <$> maybeCustomViteConfigPath +-- todo(filip): move this +findCustomViteConfigPath :: [ExternalFiles.CodeFile] -> Maybe (Path' (Rel SourceExternalCodeDir) File') +findCustomViteConfigPath externalClientCodeFiles = ExternalFiles._pathInExtCodeDir <$> maybeCustomViteConfigPath where maybeCustomViteConfigPath = find isCustomViteConfig externalClientCodeFiles - isCustomViteConfig :: ExternalCode.File -> Bool + isCustomViteConfig :: ExternalFiles.CodeFile -> Bool isCustomViteConfig - ExternalCode.File + ExternalFiles.CodeFile { _pathInExtCodeDir = path } = path == pathToViteTsConfig || path == pathToViteJsConfig diff --git a/waspc/src/Wasp/WaspignoreFile.hs b/waspc/src/Wasp/Project/Waspignore.hs similarity index 71% rename from waspc/src/Wasp/WaspignoreFile.hs rename to waspc/src/Wasp/Project/Waspignore.hs index 7b45778408..3bcd3e5897 100644 --- a/waspc/src/Wasp/WaspignoreFile.hs +++ b/waspc/src/Wasp/Project/Waspignore.hs @@ -1,19 +1,43 @@ -module Wasp.WaspignoreFile +module Wasp.Project.Waspignore ( WaspignoreFile, parseWaspignoreFile, + getNotIgnoredRelFilePaths, readWaspignoreFile, + waspIgnorePathInWaspProjectDir, ignores, ) where -import StrongPath (Abs, File', Path') +import StrongPath (Abs, Dir, File', Path', Rel) +import qualified StrongPath as SP +import StrongPath.TH (relfile) import System.FilePath.Glob (Pattern, compile, match) import System.IO.Error (isDoesNotExistError) import UnliftIO.Exception (catch, throwIO) +import Wasp.AppSpec.ExternalFiles (SourceExternalCodeDir, SourceExternalPublicDir) +import Wasp.Project.Common import qualified Wasp.Util.IO as IOUtil +class AffectedByWaspignoreFile a + newtype WaspignoreFile = WaspignoreFile [Pattern] +instance AffectedByWaspignoreFile SourceExternalCodeDir + +instance AffectedByWaspignoreFile SourceExternalPublicDir + +getNotIgnoredRelFilePaths :: + AffectedByWaspignoreFile d => + Path' Abs File' -> + Path' Abs (Dir d) -> + IO [Path' (Rel d) File'] +getNotIgnoredRelFilePaths waspignoreFilePath externalDirPath = do + waspignoreFile <- readWaspignoreFile waspignoreFilePath + filter (not . ignores waspignoreFile . SP.toFilePath) <$> IOUtil.listDirectoryDeep externalDirPath + +waspIgnorePathInWaspProjectDir :: Path' (Rel WaspProjectDir) File' +waspIgnorePathInWaspProjectDir = [relfile|.waspignore|] + -- | These patterns are ignored by every 'WaspignoreFile' defaultIgnorePatterns :: [Pattern] defaultIgnorePatterns = map compile [".waspignore"] diff --git a/waspc/src/Wasp/Project/WebApp.hs b/waspc/src/Wasp/Project/WebApp.hs deleted file mode 100644 index aba8bf692a..0000000000 --- a/waspc/src/Wasp/Project/WebApp.hs +++ /dev/null @@ -1,9 +0,0 @@ -module Wasp.Project.WebApp where - -import StrongPath (Dir, Path', Rel, reldir) -import Wasp.AppSpec.ExternalCode (SourceExternalCodeDir) - -data StaticAssetsDir - -staticAssetsDirInExtClientCodeDir :: Path' (Rel SourceExternalCodeDir) (Dir StaticAssetsDir) -staticAssetsDirInExtClientCodeDir = [reldir|public|] diff --git a/waspc/test/AppSpec/ValidTest.hs b/waspc/test/AppSpec/ValidTest.hs index 29863a02ce..fb2a07d490 100644 --- a/waspc/test/AppSpec/ValidTest.hs +++ b/waspc/test/AppSpec/ValidTest.hs @@ -258,9 +258,8 @@ spec_AppSpecValid = do AS.AppSpec { AS.decls = [basicAppDecl], AS.waspProjectDir = systemSPRoot SP. [SP.reldir|test/|], - AS.externalClientFiles = [], - AS.externalServerFiles = [], - AS.externalSharedFiles = [], + AS.externalCodeFiles = [], + AS.externalPublicFiles = [], AS.isBuild = False, AS.migrationsDir = Nothing, AS.devEnvVarsClient = [], diff --git a/waspc/test/Generator/ExternalCodeGenerator/JsTest.hs b/waspc/test/Generator/ExternalCodeGenerator/JsTest.hs deleted file mode 100644 index 00228bffd9..0000000000 --- a/waspc/test/Generator/ExternalCodeGenerator/JsTest.hs +++ /dev/null @@ -1,17 +0,0 @@ -module Generator.ExternalCodeGenerator.JsTest where - -import qualified StrongPath as SP -import Test.Tasty.Hspec -import Wasp.Generator.ExternalCodeGenerator.Common (asGenExtFile) -import Wasp.Generator.ExternalCodeGenerator.Js as Js - -spec_resolveJsFileWaspImportsForExtCodeDir :: Spec -spec_resolveJsFileWaspImportsForExtCodeDir = do - (asGenExtFile [SP.relfile|extFile.js|], "import foo from 'bar'") ~> "import foo from 'bar'" - (asGenExtFile [SP.relfile|extFile.js|], "import foo from '@wasp/bar'") ~> "import foo from '../bar'" - (asGenExtFile [SP.relfile|a/extFile.js|], "import foo from \"@wasp/bar/foo\"") - ~> "import foo from \"../../bar/foo\"" - where - (path, text) ~> expectedText = - it (SP.toFilePath path ++ " " ++ show text ++ " -> " ++ show expectedText) $ do - Js.resolveJsFileWaspImportsForExtCodeDir [SP.reldir|src|] path text `shouldBe` expectedText diff --git a/waspc/test/Generator/WebAppGeneratorTest.hs b/waspc/test/Generator/WebAppGeneratorTest.hs index 145e6e493e..a4f5cc7f0b 100644 --- a/waspc/test/Generator/WebAppGeneratorTest.hs +++ b/waspc/test/Generator/WebAppGeneratorTest.hs @@ -46,9 +46,8 @@ spec_WebAppGenerator = do } ], AS.waspProjectDir = systemSPRoot SP. [SP.reldir|test/|], - AS.externalClientFiles = [], - AS.externalServerFiles = [], - AS.externalSharedFiles = [], + AS.externalCodeFiles = [], + AS.externalPublicFiles = [], AS.isBuild = False, AS.migrationsDir = Nothing, AS.devEnvVarsServer = [], diff --git a/waspc/test/WaspignoreFileTest.hs b/waspc/test/Project/WaspignoreTest.hs similarity index 94% rename from waspc/test/WaspignoreFileTest.hs rename to waspc/test/Project/WaspignoreTest.hs index 3392aa1de1..eedd110100 100644 --- a/waspc/test/WaspignoreFileTest.hs +++ b/waspc/test/Project/WaspignoreTest.hs @@ -1,8 +1,8 @@ -module WaspignoreFileTest where +module Project.WaspignoreTest where import Test.Tasty.Hspec import Test.Tasty.QuickCheck (arbitraryPrintableChar, forAll, listOf, property) -import Wasp.WaspignoreFile (ignores, parseWaspignoreFile) +import Wasp.Project.Waspignore (ignores, parseWaspignoreFile) spec_IgnoreFile :: Spec spec_IgnoreFile = do diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index 0b93ed1f92..6cd6512bde 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -238,7 +238,7 @@ library Wasp.AppSpec.Core.Ref Wasp.AppSpec.Entity Wasp.AppSpec.Entity.Field - Wasp.AppSpec.ExternalCode + Wasp.AppSpec.ExternalFiles Wasp.AppSpec.ExtImport Wasp.AppSpec.Job Wasp.AppSpec.JSON @@ -255,7 +255,6 @@ library Wasp.Db.Postgres Wasp.Error Wasp.Env - Wasp.ExternalCode Wasp.Generator Wasp.Generator.AuthProviders Wasp.Generator.AuthProviders.Common @@ -273,9 +272,7 @@ library Wasp.Generator.DbGenerator.Jobs Wasp.Generator.DbGenerator.Operations Wasp.Generator.DockerGenerator - Wasp.Generator.ExternalCodeGenerator Wasp.Generator.ExternalCodeGenerator.Common - Wasp.Generator.ExternalCodeGenerator.Js Wasp.Generator.FileDraft Wasp.Generator.FileDraft.CopyDirFileDraft Wasp.Generator.FileDraft.CopyFileDraft @@ -306,7 +303,6 @@ library Wasp.Generator.ServerGenerator.EmailSender.Providers Wasp.Generator.ServerGenerator.Common Wasp.Generator.ServerGenerator.ConfigG - Wasp.Generator.ServerGenerator.ExternalCodeGenerator Wasp.Generator.ServerGenerator.JobGenerator Wasp.Generator.ServerGenerator.OperationsG Wasp.Generator.ServerGenerator.OperationsRoutesG @@ -327,7 +323,6 @@ library Wasp.Generator.WebAppGenerator.Auth.EmailAuthG Wasp.Generator.WebAppGenerator.Auth.Common Wasp.Generator.WebAppGenerator.Common - Wasp.Generator.WebAppGenerator.ExternalCodeGenerator Wasp.Generator.WebAppGenerator.OperationsGenerator Wasp.Generator.WebAppGenerator.OperationsGenerator.ResourcesG Wasp.Generator.WebAppGenerator.RouterGenerator @@ -351,7 +346,7 @@ library Wasp.Project.Db.Dev.Postgres Wasp.Project.Deployment Wasp.Project.Env - Wasp.Project.WebApp + Wasp.Project.ExternalFiles Wasp.Project.Studio Wasp.Project.Vite Wasp.Psl.Ast.Model @@ -380,7 +375,7 @@ library Wasp.Util.StrongPath Wasp.Util.WebRouterPath Wasp.Version - Wasp.WaspignoreFile + Wasp.Project.Waspignore library waspls import: common-all @@ -590,7 +585,6 @@ test-suite waspc-test FilePath.ExtraTest Fixtures Generator.DbGeneratorTest - Generator.ExternalCodeGenerator.JsTest Generator.FileDraft.CopyFileDraftTest Generator.FileDraft.CopyAndModifyTextFileDraftTest Generator.FileDraft.TemplateFileDraftTest @@ -614,7 +608,7 @@ test-suite waspc-test SemanticVersionTest SemanticVersion.VersionBoundTest SemanticVersion.VersionTest - WaspignoreFileTest + Project.WaspignoreTest Paths_waspc Generator.NpmDependenciesTest JsImportTest diff --git a/waspc/waspls/src/Wasp/LSP/ExtImport/Path.hs b/waspc/waspls/src/Wasp/LSP/ExtImport/Path.hs index a10fa90a7a..2ea3bd9d6e 100644 --- a/waspc/waspls/src/Wasp/LSP/ExtImport/Path.hs +++ b/waspc/waspls/src/Wasp/LSP/ExtImport/Path.hs @@ -24,7 +24,7 @@ import GHC.Generics (Generic) import qualified Path as P import qualified StrongPath as SP import qualified StrongPath.Path as SP -import Wasp.AppSpec.ExternalCode (SourceExternalCodeDir) +import Wasp.AppSpec.ExternalFiles (SourceExternalCodeDir) import Wasp.LSP.ServerMonads.HasProjectRootDir (HasProjectRootDir (getProjectRootDir)) import Wasp.Project.Common (WaspProjectDir) import Wasp.Util.IO (doesFileExist) From 0a0ac89b030778f02d126bc05db42a48751493b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Thu, 18 Jan 2024 00:46:04 +0100 Subject: [PATCH 46/54] Start building SDK during project setup --- waspc/data/Cli/templates/basic/package.json | 2 + .../data/Generator/templates/sdk/package.json | 34 +- .../sdk/server/{dbClient.js => dbClient.ts} | 0 .../Generator/templates/sdk/tsconfig.json | 37 ++ .../.wasp/out/sdk/wasp/package.json | 36 +- .../todo-typescript/package-lock.json | 562 +++++++++++++++++- waspc/examples/todo-typescript/package.json | 2 + waspc/src/Wasp/Generator/SdkGenerator.hs | 71 ++- waspc/src/Wasp/Generator/Setup.hs | 23 +- waspc/src/Wasp/Generator/WebAppGenerator.hs | 1 - 10 files changed, 709 insertions(+), 59 deletions(-) rename waspc/data/Generator/templates/sdk/server/{dbClient.js => dbClient.ts} (100%) create mode 100644 waspc/data/Generator/templates/sdk/tsconfig.json diff --git a/waspc/data/Cli/templates/basic/package.json b/waspc/data/Cli/templates/basic/package.json index 7e6222db84..a3aceaf30c 100644 --- a/waspc/data/Cli/templates/basic/package.json +++ b/waspc/data/Cli/templates/basic/package.json @@ -5,6 +5,8 @@ "react": "^18.2.0" }, "devDependencies": { + "typescript": "^5.1.0", + "vite": "^4.3.9", "@types/react": "^18.0.37", "prisma": "4.16.2" } diff --git a/waspc/data/Generator/templates/sdk/package.json b/waspc/data/Generator/templates/sdk/package.json index 569661c957..61cb7e87be 100644 --- a/waspc/data/Generator/templates/sdk/package.json +++ b/waspc/data/Generator/templates/sdk/package.json @@ -9,23 +9,23 @@ "types": "tsc --declaration --emitDeclarationOnly --stripInternal --declarationDir dist" }, "exports": { - "./core/HttpError": "./core/HttpError.js", - "./core/config": "./core/config.js", - "./core/stitches.config": "./core/stitches.config.js", - "./core/storage": "./core/storage.ts", - "./rpc": "./rpc/index.ts", - "./rpc/queries": "./rpc/queries/index.ts", - "./rpc/actions": "./rpc/actions/index.ts", - "./rpc/queryClient": "./rpc/queryClient.ts", - "./types": "./types/index.ts", - "./auth/*": "./auth/*", - "./api": "./api/index.ts", - "./api/*": "./api/*", - "./operations": "./operations/index.ts", - "./operations/*": "./operations/*", - "./universal/url": "./universal/url.ts", - "./universal/types": "./universal/url.ts", - "./server/dbClient": "./server/dbClient.js" + "./core/HttpError": "./dist/core/HttpError.js", + "./core/config": "./dist/core/config.js", + "./core/stitches.config": "./dist/core/stitches.config.js", + "./core/storage": "./dist/core/storage.js", + "./rpc": "./dist/rpc/index.js", + "./rpc/queries": "./dist/rpc/queries/index.js", + "./rpc/actions": "./dist/rpc/actions/index.js", + "./rpc/queryClient": "./dist/rpc/queryClient.js", + "./types": "./dist/types/index.js", + "./auth/*": "./dist/auth/*", + "./api": "./dist/api/index.js", + "./api/*": "./dist/api/*", + "./operations": "./dist/operations/index.js", + "./operations/*": "./dist/operations/*", + "./universal/url": "./dist/universal/url.js", + "./universal/types": "./dist/universal/url.js", + "./server/dbClient": "./dist/server/dbClient.js" }, "license": "ISC", "include": [ diff --git a/waspc/data/Generator/templates/sdk/server/dbClient.js b/waspc/data/Generator/templates/sdk/server/dbClient.ts similarity index 100% rename from waspc/data/Generator/templates/sdk/server/dbClient.js rename to waspc/data/Generator/templates/sdk/server/dbClient.ts diff --git a/waspc/data/Generator/templates/sdk/tsconfig.json b/waspc/data/Generator/templates/sdk/tsconfig.json new file mode 100644 index 0000000000..007635ff4c --- /dev/null +++ b/waspc/data/Generator/templates/sdk/tsconfig.json @@ -0,0 +1,37 @@ +{{={= =}=}} +{ + "extends": "@tsconfig/node{= majorNodeVersion =}/tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "lib": [ + "esnext", + "dom", + "DOM.Iterable" + ], + "strict": false, + // Overriding this because we want to use top-level await + "module": "esnext", + "target": "es2017", + // Enable source map for debugging and go-to-definition + "sourceMap": true, + // The remaining settings should match the extended nodeXY/tsconfig.json, but I kept + // them here to be explicit. + // Enable default imports in TypeScript. + "esModuleInterop": true, + "moduleResolution": "node", + "outDir": "dist", + "allowJs": true + }, + "paths": { + "wasp/*": [ + "/*" + ] + }, + "include": [ + "." + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json index 69fd0b8197..cae3228cd6 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json @@ -8,23 +8,23 @@ "types": "tsc --declaration --emitDeclarationOnly --stripInternal --declarationDir dist" }, "exports": { - "./core/HttpError": "./core/HttpError.js", - "./core/config": "./core/config.js", - "./core/stitches.config": "./core/stitches.config.js", - "./core/storage": "./core/storage.ts", - "./rpc": "./rpc/index.ts", - "./rpc/queries": "./rpc/queries/index.ts", - "./rpc/actions": "./rpc/actions/index.ts", - "./rpc/queryClient": "./rpc/queryClient.ts", - "./types": "./types/index.ts", - "./auth/*": "./auth/*", - "./api": "./api/index.ts", - "./api/*": "./api/*", - "./operations": "./operations/index.ts", - "./operations/*": "./operations/*", - "./universal/url": "./universal/url.ts", - "./universal/types": "./universal/url.ts", - "./server/dbClient": "./server/dbClient.js" + "./core/HttpError": "./dist/core/HttpError.js", + "./core/config": "./dist/core/config.js", + "./core/stitches.config": "./dist/core/stitches.config.js", + "./core/storage": "./dist/core/storage.js", + "./rpc": "./dist/rpc/index.js", + "./rpc/queries": "./dist/rpc/queries/index.js", + "./rpc/actions": "./dist/rpc/actions/index.js", + "./rpc/queryClient": "./dist/rpc/queryClient.js", + "./types": "./dist/types/index.js", + "./auth/*": "./dist/auth/*", + "./api": "./dist/api/index.js", + "./api/*": "./dist/api/*", + "./operations": "./dist/operations/index.js", + "./operations/*": "./dist/operations/*", + "./universal/url": "./dist/universal/url.js", + "./universal/types": "./dist/universal/url.js", + "./server/dbClient": "./dist/server/dbClient.js" }, "license": "ISC", "include": [ @@ -45,6 +45,6 @@ "@types/express-serve-static-core": "^4.17.13", "@stitches/react": "^1.2.8" }, - "devDependencies": { + "devDependencies": {"@tsconfig/node18": "latest" } } diff --git a/waspc/examples/todo-typescript/package-lock.json b/waspc/examples/todo-typescript/package-lock.json index c438c1ae41..76cde59255 100644 --- a/waspc/examples/todo-typescript/package-lock.json +++ b/waspc/examples/todo-typescript/package-lock.json @@ -11,7 +11,9 @@ }, "devDependencies": { "@types/react": "^18.0.37", - "prisma": "4.16.2" + "prisma": "4.16.2", + "typescript": "^5.1.0", + "vite": "^4.3.9" } }, ".wasp/out/sdk/wasp": { @@ -33,7 +35,9 @@ "secure-password": "^4.0.0", "superjson": "^1.12.2" }, - "devDependencies": {} + "devDependencies": { + "@tsconfig/node18": "latest" + } }, "node_modules/@babel/runtime": { "version": "7.23.8", @@ -46,6 +50,358 @@ "node": ">=6.9.0" } }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@prisma/client": { "version": "4.16.2", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.16.2.tgz", @@ -120,6 +476,12 @@ } } }, + "node_modules/@tsconfig/node18": { + "version": "18.2.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.2.tgz", + "integrity": "sha512-d6McJeGsuoRlwWZmVIeE8CUA27lu6jLjvv1JzqmpsytOYYbVi1tHZEnwCNVOXnj4pyLvneZlFlpXUK+X9wBWyw==", + "dev": true + }, "node_modules/@types/express-serve-static-core": { "version": "4.17.41", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", @@ -397,6 +759,43 @@ "node": ">= 0.8" } }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -516,6 +915,20 @@ "node": ">= 0.6" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -831,6 +1244,24 @@ "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz", "integrity": "sha512-C40jQ3NzfkP53NsO8kEOFd79p4b9kDXQMwgiY1z8ZwrDZgUyom0AHwGegF4Dm99L+YoYhuaB0ceerUcXmqr1rQ==" }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -889,6 +1320,40 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.4.33", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prisma": { "version": "4.16.2", "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.16.2.tgz", @@ -1054,6 +1519,22 @@ "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" }, + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1178,6 +1659,15 @@ "node-gyp-build": "^4.3.0" } }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1227,6 +1717,19 @@ "node": ">= 0.6" } }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -1269,6 +1772,61 @@ "node": ">= 0.8" } }, + "node_modules/vite": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", + "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, "node_modules/wasp": { "resolved": ".wasp/out/sdk/wasp", "link": true diff --git a/waspc/examples/todo-typescript/package.json b/waspc/examples/todo-typescript/package.json index 7e6222db84..a3aceaf30c 100644 --- a/waspc/examples/todo-typescript/package.json +++ b/waspc/examples/todo-typescript/package.json @@ -5,6 +5,8 @@ "react": "^18.2.0" }, "devDependencies": { + "typescript": "^5.1.0", + "vite": "^4.3.9", "@types/react": "^18.0.37", "prisma": "4.16.2" } diff --git a/waspc/src/Wasp/Generator/SdkGenerator.hs b/waspc/src/Wasp/Generator/SdkGenerator.hs index 63c7bc7688..2c553d5bbc 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator.hs @@ -4,9 +4,13 @@ module Wasp.Generator.SdkGenerator ( genSdk, installNpmDependencies, genExternalCodeDir, + sdkRootDirInProjectRootDir, + buildSdk, ) where +import Control.Concurrent (newChan) +import Control.Concurrent.Async (concurrently) import Data.Aeson (object) import qualified Data.Aeson as Aeson import Data.Aeson.Types ((.=)) @@ -14,6 +18,7 @@ import Data.Maybe (fromMaybe, isJust, mapMaybe) import GHC.IO (unsafePerformIO) import StrongPath import qualified StrongPath as SP +import System.Exit (ExitCode (..)) import qualified System.FilePath as FP import Wasp.AppSpec import qualified Wasp.AppSpec as AS @@ -22,7 +27,7 @@ import qualified Wasp.AppSpec.App.Auth as AS.App.Auth import qualified Wasp.AppSpec.App.Dependency as AS.Dependency import qualified Wasp.AppSpec.Entity as AS.Entity import qualified Wasp.AppSpec.ExternalFiles as EC -import Wasp.AppSpec.Valid (isAuthEnabled) +import Wasp.AppSpec.Valid (getLowestNodeVersionUserAllows, isAuthEnabled) import qualified Wasp.AppSpec.Valid as AS.Valid import Wasp.Generator.Common (ProjectRootDir, makeJsonWithEntityData, prismaVersion) import qualified Wasp.Generator.DbGenerator.Auth as DbAuth @@ -31,42 +36,54 @@ import Wasp.Generator.FileDraft (FileDraft, createCopyDirFileDraft, createTempla import qualified Wasp.Generator.FileDraft as FD import Wasp.Generator.FileDraft.CopyDirFileDraft (CopyDirFileDraftDstDirStrategy (RemoveExistingDstDir)) import qualified Wasp.Generator.Job as J +import Wasp.Generator.Job.IO (readJobMessagesAndPrintThemPrefixed) import Wasp.Generator.Job.Process (runNodeCommandAsJob) import Wasp.Generator.Monad (Generator) import qualified Wasp.Generator.NpmDependencies as N import Wasp.Generator.Templates (TemplatesDir, getTemplatesDirAbsPath) +import qualified Wasp.Node.Version as NodeVersion import Wasp.Project.Common (WaspProjectDir) import qualified Wasp.SemanticVersion as SV import Wasp.Util (toLowerFirst, (<++>)) -genSdk :: AppSpec -> Generator [FileDraft] -genSdk spec = - sequence - [ genFileCopy [relfile|server/dbClient.js|], - genPackageJson spec - ] - <++> genHardcodedSdkModules - <++> genSdkModules spec - where - genFileCopy = return . mkTmplFd - data SdkRootDir data SdkTemplatesDir -genSdkModules :: AppSpec -> Generator [FileDraft] -genSdkModules spec = +genSdk :: AppSpec -> Generator [FileDraft] +genSdk spec = + genSdkHardcoded + <++> genSdkReal spec + +buildSdk :: Path' Abs (Dir ProjectRootDir) -> IO (Either String ()) +buildSdk projectRootDir = do + chan <- newChan + (_, exitCode) <- + concurrently + (readJobMessagesAndPrintThemPrefixed chan) + (runNodeCommandAsJob dstDir "npx" ["tsc"] J.Wasp chan) + case exitCode of + ExitSuccess -> return $ Right () + ExitFailure code -> return $ Left $ "SDK build failed with exit code: " ++ show code + where + dstDir = projectRootDir sdkRootDirInProjectRootDir + +genSdkReal :: AppSpec -> Generator [FileDraft] +genSdkReal spec = sequence [ genFileCopy [relfile|api/index.ts|], - genFileCopy [relfile|api/events.ts|] + genFileCopy [relfile|api/events.ts|], + genFileCopy [relfile|server/dbClient.ts|], + genTsConfigJson, + genPackageJson spec ] <++> genExternalCodeDir (AS.externalCodeFiles spec) <++> genTypesAndEntitiesDirs spec where genFileCopy = return . mkTmplFd -genHardcodedSdkModules :: Generator [FileDraft] -genHardcodedSdkModules = +genSdkHardcoded :: Generator [FileDraft] +genSdkHardcoded = return [ copyFolder [reldir|auth|], copyFolder [reldir|core|], @@ -173,8 +190,26 @@ genPackageJson spec = ("@types/express-serve-static-core", "^4.17.13") ] ++ depsRequiredForAuth spec, - N.devDependencies = AS.Dependency.fromList [] + N.devDependencies = + AS.Dependency.fromList + [ ("@tsconfig/node" <> majorNodeVersionStr, "latest") + ] } + majorNodeVersionStr = show (SV.major $ getLowestNodeVersionUserAllows spec) + +-- todo(filip): remove this duplication, we have almost the same thing in the +-- ServerGenerator. +genTsConfigJson :: Generator FileDraft +genTsConfigJson = do + return $ + mkTmplFdWithDstAndData + [relfile|tsconfig.json|] + [relfile|tsconfig.json|] + ( Just $ + object + [ "majorNodeVersion" .= show (SV.major NodeVersion.oldestWaspSupportedNodeVersion) + ] + ) depsRequiredForAuth :: AppSpec -> [AS.Dependency.Dependency] depsRequiredForAuth spec = diff --git a/waspc/src/Wasp/Generator/Setup.hs b/waspc/src/Wasp/Generator/Setup.hs index f680b9f728..3c81bd071d 100644 --- a/waspc/src/Wasp/Generator/Setup.hs +++ b/waspc/src/Wasp/Generator/Setup.hs @@ -10,12 +10,20 @@ import Wasp.Generator.Common (ProjectRootDir) import qualified Wasp.Generator.DbGenerator as DbGenerator import Wasp.Generator.Monad (GeneratorError (..), GeneratorWarning (..)) import Wasp.Generator.NpmInstall (installNpmDependenciesWithInstallRecord, isNpmInstallNeeded) +import qualified Wasp.Generator.SdkGenerator as SdkGenerator import qualified Wasp.Message as Msg runSetup :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> Msg.SendMessage -> IO ([GeneratorWarning], [GeneratorError]) -runSetup spec dstDir sendMessage = do - runNpmInstallIfNeeded spec dstDir sendMessage >>= \case - npmInstallResults@(_, []) -> (npmInstallResults <>) <$> setUpDatabase spec dstDir sendMessage +runSetup spec projectRootDir sendMessage = do + runNpmInstallIfNeeded spec projectRootDir sendMessage >>= \case + npmInstallResults@(_, []) -> + setUpDatabase spec projectRootDir sendMessage >>= \case + setUpDatabaseResults@(_, []) -> do + -- todo(filip): Should we consider building SDK as part of code generation? + -- todo(filip): Avoid building on each setup if we don't need to. + buildsSdkResults <- buildSdk projectRootDir sendMessage + return $ npmInstallResults <> setUpDatabaseResults <> buildsSdkResults + setUpDatabaseResults -> return $ npmInstallResults <> setUpDatabaseResults npmInstallResults -> return npmInstallResults runNpmInstallIfNeeded :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> Msg.SendMessage -> IO ([GeneratorWarning], [GeneratorError]) @@ -36,3 +44,12 @@ setUpDatabase spec dstDir sendMessage = do (dbGeneratorWarnings, dbGeneratorErrors) <- DbGenerator.postWriteDbGeneratorActions spec dstDir when (null dbGeneratorErrors) (sendMessage $ Msg.Success "Database successfully set up.") return (dbGeneratorWarnings, dbGeneratorErrors) + +buildSdk :: Path' Abs (Dir ProjectRootDir) -> Msg.SendMessage -> IO ([GeneratorWarning], [GeneratorError]) +buildSdk projectRootDir sendMessage = do + sendMessage $ Msg.Start "Building SDK..." + SdkGenerator.buildSdk projectRootDir >>= \case + Left errorMesage -> return ([], [GenericGeneratorError errorMesage]) + Right () -> do + sendMessage $ Msg.Success "SDK built successfully." + return ([], []) diff --git a/waspc/src/Wasp/Generator/WebAppGenerator.hs b/waspc/src/Wasp/Generator/WebAppGenerator.hs index 26866eb32e..4c88cd942c 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator.hs @@ -143,7 +143,6 @@ npmDepsForWasp spec = AS.Dependency.fromList [ -- TODO: Allow users to choose whether they want to use TypeScript -- in their projects and install these dependencies accordingly. - ("vite", "^4.3.9"), ("typescript", "^5.1.0"), ("@types/react", "^18.0.37"), ("@types/react-dom", "^18.0.11"), From d7101430c447c671f41d604dcf03ebf033b59547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Thu, 18 Jan 2024 00:53:10 +0100 Subject: [PATCH 47/54] Add universal to proper SDK gen --- .../react-app/scripts/validate-env.mjs | 2 +- .../templates/react-app/src/queries/core.d.ts | 2 +- .../data/Generator/templates/sdk/package.json | 3 +- .../{ => sdk}/universal/validators.js | 0 .../templates/server/scripts/validate-env.mjs | 2 +- .../server/src/auth/providers/types.ts | 2 +- .../Generator/templates/server/src/config.js | 2 +- .../Generator/templates/universal/types.ts | 31 ------------------- .../data/Generator/templates/universal/url.ts | 3 -- .../.wasp/out/sdk/wasp/package.json | 3 +- waspc/src/Wasp/Generator/SdkGenerator.hs | 12 +++++-- waspc/src/Wasp/Generator/ServerGenerator.hs | 11 +------ waspc/src/Wasp/Generator/WebAppGenerator.hs | 11 +------ 13 files changed, 21 insertions(+), 63 deletions(-) rename waspc/data/Generator/templates/{ => sdk}/universal/validators.js (100%) delete mode 100644 waspc/data/Generator/templates/universal/types.ts delete mode 100644 waspc/data/Generator/templates/universal/url.ts diff --git a/waspc/data/Generator/templates/react-app/scripts/validate-env.mjs b/waspc/data/Generator/templates/react-app/scripts/validate-env.mjs index 27d6a9fd59..18ee507c9e 100644 --- a/waspc/data/Generator/templates/react-app/scripts/validate-env.mjs +++ b/waspc/data/Generator/templates/react-app/scripts/validate-env.mjs @@ -1,4 +1,4 @@ -import { throwIfNotValidAbsoluteURL } from './universal/validators.mjs'; +import { throwIfNotValidAbsoluteURL } from 'wasp/universal/validators'; console.info("🔍 Validating environment variables..."); throwIfNotValidAbsoluteURL(process.env.REACT_APP_API_URL, 'Environemnt variable REACT_APP_API_URL'); diff --git a/waspc/data/Generator/templates/react-app/src/queries/core.d.ts b/waspc/data/Generator/templates/react-app/src/queries/core.d.ts index e1bdbe4783..90e30187a9 100644 --- a/waspc/data/Generator/templates/react-app/src/queries/core.d.ts +++ b/waspc/data/Generator/templates/react-app/src/queries/core.d.ts @@ -1,6 +1,6 @@ import { type Query } from '.' import { Route } from '../types'; -import type { Expand, _Awaited, _ReturnType } from '../universal/types' +import type { Expand, _Awaited, _ReturnType } from 'wasp/universal/types' export function createQuery( queryRoute: string, diff --git a/waspc/data/Generator/templates/sdk/package.json b/waspc/data/Generator/templates/sdk/package.json index 61cb7e87be..a9c28953c0 100644 --- a/waspc/data/Generator/templates/sdk/package.json +++ b/waspc/data/Generator/templates/sdk/package.json @@ -24,7 +24,8 @@ "./operations": "./dist/operations/index.js", "./operations/*": "./dist/operations/*", "./universal/url": "./dist/universal/url.js", - "./universal/types": "./dist/universal/url.js", + "./universal/types": "./dist/universal/types.js", + "./universal/validators": "./dist/universal/validators.js", "./server/dbClient": "./dist/server/dbClient.js" }, "license": "ISC", diff --git a/waspc/data/Generator/templates/universal/validators.js b/waspc/data/Generator/templates/sdk/universal/validators.js similarity index 100% rename from waspc/data/Generator/templates/universal/validators.js rename to waspc/data/Generator/templates/sdk/universal/validators.js diff --git a/waspc/data/Generator/templates/server/scripts/validate-env.mjs b/waspc/data/Generator/templates/server/scripts/validate-env.mjs index fb68580bbb..ac264b7961 100644 --- a/waspc/data/Generator/templates/server/scripts/validate-env.mjs +++ b/waspc/data/Generator/templates/server/scripts/validate-env.mjs @@ -1,4 +1,4 @@ -import { throwIfNotValidAbsoluteURL } from './universal/validators.mjs'; +import { throwIfNotValidAbsoluteURL } from 'wasp/universal/validators'; console.info("🔍 Validating environment variables..."); throwIfNotValidAbsoluteURL(process.env.WASP_WEB_CLIENT_URL, 'Environment variable WASP_WEB_CLIENT_URL'); diff --git a/waspc/data/Generator/templates/server/src/auth/providers/types.ts b/waspc/data/Generator/templates/server/src/auth/providers/types.ts index e2ff6e09a7..96f1be6a5a 100644 --- a/waspc/data/Generator/templates/server/src/auth/providers/types.ts +++ b/waspc/data/Generator/templates/server/src/auth/providers/types.ts @@ -1,7 +1,7 @@ {{={= =}=}} import type { Router, Request } from 'express' import type { Prisma } from '@prisma/client' -import type { Expand } from '../../universal/types' +import type { Expand } from 'wasp/universal/types' import type { ProviderName } from '../utils' type UserEntityCreateInput = Prisma.{= userEntityUpper =}CreateInput diff --git a/waspc/data/Generator/templates/server/src/config.js b/waspc/data/Generator/templates/server/src/config.js index e38a10e965..aee9bbf323 100644 --- a/waspc/data/Generator/templates/server/src/config.js +++ b/waspc/data/Generator/templates/server/src/config.js @@ -1,7 +1,7 @@ {{={= =}=}} import merge from 'lodash.merge' -import { stripTrailingSlash } from "./universal/url.js"; +import { stripTrailingSlash } from "wasp/universal/url"; const env = process.env.NODE_ENV || 'development' diff --git a/waspc/data/Generator/templates/universal/types.ts b/waspc/data/Generator/templates/universal/types.ts deleted file mode 100644 index 8cadbd740d..0000000000 --- a/waspc/data/Generator/templates/universal/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -// This is a helper type used exclusively for DX purposes. It's a No-op for the -// compiler, but expands the type's representatoin in IDEs (i.e., inlines all -// type constructors) to make it more readable for the user. -// -// It expands this SO answer to functions: https://stackoverflow.com/a/57683652 -export type Expand = T extends (...args: infer A) => infer R - ? (...args: A) => R - : T extends infer O - ? { [K in keyof O]: O[K] } - : never - -// TypeScript's native Awaited type exhibits strange behavior in VS Code (see -// https://github.com/wasp-lang/wasp/pull/1090#discussion_r1159687537 for -// details). Until it's fixed, we're using our own type for this. -// -// TODO: investigate further. This most likely has something to do with an -// unsatisfied 'extends' constraints. A mismatch is probably happening with -// function parameter types and/or return types (check '_ReturnType' below for -// more). -export type _Awaited = T extends Promise - ? _Awaited - : T - -// TypeScript's native ReturnType does not work for functions of type '(...args: -// never[]) => unknown' (and that's what operations currently use). -// -// TODO: investigate how to properly specify the 'extends' constraint for function -// type (i.e., any vs never and unknown) and stick with that. Take DX into -// consideration. -export type _ReturnType unknown> = - T extends (...args: never[]) => infer R ? R : never diff --git a/waspc/data/Generator/templates/universal/url.ts b/waspc/data/Generator/templates/universal/url.ts deleted file mode 100644 index d21c06c65c..0000000000 --- a/waspc/data/Generator/templates/universal/url.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function stripTrailingSlash(url?: string): string | undefined { - return url?.replace(/\/$/, ""); -} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json index cae3228cd6..9f6d462214 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json @@ -23,7 +23,8 @@ "./operations": "./dist/operations/index.js", "./operations/*": "./dist/operations/*", "./universal/url": "./dist/universal/url.js", - "./universal/types": "./dist/universal/url.js", + "./universal/types": "./dist/universal/types.js", + "./universal/validators": "./dist/universal/validators.js", "./server/dbClient": "./dist/server/dbClient.js" }, "license": "ISC", diff --git a/waspc/src/Wasp/Generator/SdkGenerator.hs b/waspc/src/Wasp/Generator/SdkGenerator.hs index 2c553d5bbc..fb8a630300 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator.hs @@ -77,6 +77,7 @@ genSdkReal spec = genTsConfigJson, genPackageJson spec ] + <++> genUniversalDir <++> genExternalCodeDir (AS.externalCodeFiles spec) <++> genTypesAndEntitiesDirs spec where @@ -93,8 +94,7 @@ genSdkHardcoded = copyFolder [reldir|server/actions|], copyFolder [reldir|server/queries|], copyFile [relfile|server/utils.ts|], - copyFolder [reldir|types|], - copyFolder [reldir|universal|] + copyFolder [reldir|types|] ] where copyFolder :: Path' (Rel SdkTemplatesDir) (Dir d) -> FileDraft @@ -278,3 +278,11 @@ genSourceFile file = return $ FD.createTextFileDraft relDstPath text filePathInSrcExtCodeDir = EC.filePathInExtCodeDir file text = EC.fileText file relDstPath = sdkRootDirInProjectRootDir extSrcDirInSdkRootDir SP.castRel filePathInSrcExtCodeDir + +genUniversalDir :: Generator [FileDraft] +genUniversalDir = + return + [ mkTmplFd [relfile|universal/url.ts|], + mkTmplFd [relfile|universal/types.ts|], + mkTmplFd [relfile|universal/validators.js|] + ] diff --git a/waspc/src/Wasp/Generator/ServerGenerator.hs b/waspc/src/Wasp/Generator/ServerGenerator.hs index 5009f517a4..4af805a608 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator.hs @@ -78,7 +78,6 @@ genServer spec = <++> genJobs spec <++> genJobExecutors spec <++> genPatches spec - <++> genUniversalDir <++> genEnvValidationScript <++> genExportedTypesDir spec <++> genApis spec @@ -315,18 +314,10 @@ getPackageJsonOverrides = map buildOverrideData (designateLastElement overrides) map (\(x1, x2, x3) -> (x1, x2, x3, False)) (init l) ++ map (\(x1, x2, x3) -> (x1, x2, x3, True)) [last l] -genUniversalDir :: Generator [FileDraft] -genUniversalDir = - return - [ C.mkUniversalTmplFdWithDst [relfile|url.ts|] [relfile|src/universal/url.ts|], - C.mkUniversalTmplFdWithDst [relfile|types.ts|] [relfile|src/universal/types.ts|] - ] - genEnvValidationScript :: Generator [FileDraft] genEnvValidationScript = return - [ C.mkTmplFd [relfile|scripts/validate-env.mjs|], - C.mkUniversalTmplFdWithDst [relfile|validators.js|] [relfile|scripts/universal/validators.mjs|] + [ C.mkTmplFd [relfile|scripts/validate-env.mjs|] ] genExportedTypesDir :: AppSpec -> Generator [FileDraft] diff --git a/waspc/src/Wasp/Generator/WebAppGenerator.hs b/waspc/src/Wasp/Generator/WebAppGenerator.hs index 4c88cd942c..b553df8da0 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator.hs @@ -78,7 +78,6 @@ genWebApp spec = do <++> genSrcDir spec <++> genPublicDir spec <++> genDotEnv spec - <++> genUniversalDir <++> genEnvValidationScript <++> genCrud spec where @@ -283,18 +282,10 @@ getIndexTs spec = relPathToWebAppSrcDir :: Path Posix (Rel importLocation) (Dir C.WebAppSrcDir) relPathToWebAppSrcDir = [reldirP|./|] -genUniversalDir :: Generator [FileDraft] -genUniversalDir = - return - [ C.mkUniversalTmplFdWithDst [relfile|url.ts|] [relfile|src/universal/url.ts|], - C.mkUniversalTmplFdWithDst [relfile|types.ts|] [relfile|src/universal/types.ts|] - ] - genEnvValidationScript :: Generator [FileDraft] genEnvValidationScript = return - [ C.mkTmplFd [relfile|scripts/validate-env.mjs|], - C.mkUniversalTmplFdWithDst [relfile|validators.js|] [relfile|scripts/universal/validators.mjs|] + [ C.mkTmplFd [relfile|scripts/validate-env.mjs|] ] genWebSockets :: AppSpec -> Generator [FileDraft] From fcfe160b78d851572c9005c6bae3afd7b941b974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Thu, 18 Jan 2024 01:14:37 +0100 Subject: [PATCH 48/54] Add server/utils to proper SDK gen --- waspc/data/Generator/templates/sdk/package.json | 3 ++- .../Generator/templates/sdk/server/utils.ts | 7 ++++++- .../server/src/auth/providers/email/utils.ts | 2 +- .../server/src/auth/providers/index.ts | 2 +- .../server/src/auth/providers/username/login.ts | 2 +- .../src/auth/providers/username/signup.ts | 2 +- .../templates/server/src/auth/utils.ts | 2 +- .../Generator/templates/server/src/core/auth.js | 2 +- .../server/src/middleware/operations.ts | 2 +- .../templates/server/src/routes/apis/index.ts | 2 +- .../templates/server/src/routes/auth/me.js | 2 +- .../.wasp/out/sdk/wasp/package.json | 3 ++- .../.wasp/out/sdk/wasp/server/utils.ts | 2 +- waspc/src/Wasp/Generator/SdkGenerator.hs | 17 ++++++++++++++--- waspc/src/Wasp/Generator/ServerGenerator.hs | 6 ------ 15 files changed, 34 insertions(+), 22 deletions(-) diff --git a/waspc/data/Generator/templates/sdk/package.json b/waspc/data/Generator/templates/sdk/package.json index a9c28953c0..bcfaf60f1d 100644 --- a/waspc/data/Generator/templates/sdk/package.json +++ b/waspc/data/Generator/templates/sdk/package.json @@ -26,7 +26,8 @@ "./universal/url": "./dist/universal/url.js", "./universal/types": "./dist/universal/types.js", "./universal/validators": "./dist/universal/validators.js", - "./server/dbClient": "./dist/server/dbClient.js" + "./server/dbClient": "./dist/server/dbClient.js", + "./server/utils": "./dist/server/utils.js" }, "license": "ISC", "include": [ diff --git a/waspc/data/Generator/templates/sdk/server/utils.ts b/waspc/data/Generator/templates/sdk/server/utils.ts index a930149d08..44f05bfc7c 100644 --- a/waspc/data/Generator/templates/sdk/server/utils.ts +++ b/waspc/data/Generator/templates/sdk/server/utils.ts @@ -1,3 +1,4 @@ +{{={= =}=}} import crypto from 'crypto' import { Request, Response, NextFunction } from 'express' @@ -5,10 +6,14 @@ import { readdir } from 'fs' import { dirname } from 'path' import { fileURLToPath } from 'url' -import { type SanitizedUser } from './_types/index.js' +{=# isAuthEnabled =} +import { type SanitizedUser } from 'wasp/server/_types/index.js' +{=/ isAuthEnabled =} type RequestWithExtraFields = Request & { + {=# isAuthEnabled =} user?: SanitizedUser + {=/ isAuthEnabled =} } /** diff --git a/waspc/data/Generator/templates/server/src/auth/providers/email/utils.ts b/waspc/data/Generator/templates/server/src/auth/providers/email/utils.ts index a158b91482..ba366e44b6 100644 --- a/waspc/data/Generator/templates/server/src/auth/providers/email/utils.ts +++ b/waspc/data/Generator/templates/server/src/auth/providers/email/utils.ts @@ -8,7 +8,7 @@ import { findAuthIdentity, deserializeAndSanitizeProviderData, type EmailProviderData, -} from '../../utils.js'; +} from 'wasp/server/utils'; import waspServerConfig from '../../../config.js'; import { type {= userEntityUpper =}, type {= authEntityUpper =} } from '../../../entities/index.js' diff --git a/waspc/data/Generator/templates/server/src/auth/providers/index.ts b/waspc/data/Generator/templates/server/src/auth/providers/index.ts index f3e3326fbe..44208c0375 100644 --- a/waspc/data/Generator/templates/server/src/auth/providers/index.ts +++ b/waspc/data/Generator/templates/server/src/auth/providers/index.ts @@ -3,7 +3,7 @@ import { join } from 'path' import { Router } from "express"; -import { getDirPathFromFileUrl, importJsFilesFromDir } from "../../utils.js"; +import { getDirPathFromFileUrl, importJsFilesFromDir } from "wasp/server/utils"; import { ProviderConfig } from "./types"; diff --git a/waspc/data/Generator/templates/server/src/auth/providers/username/login.ts b/waspc/data/Generator/templates/server/src/auth/providers/username/login.ts index 9bb5841bf5..26dd4cbad9 100644 --- a/waspc/data/Generator/templates/server/src/auth/providers/username/login.ts +++ b/waspc/data/Generator/templates/server/src/auth/providers/username/login.ts @@ -1,6 +1,6 @@ {{={= =}=}} import { verifyPassword, throwInvalidCredentialsError } from '../../../core/auth.js' -import { handleRejection } from '../../../utils.js' +import { handleRejection } from 'wasp/server/utils' import { createProviderId, diff --git a/waspc/data/Generator/templates/server/src/auth/providers/username/signup.ts b/waspc/data/Generator/templates/server/src/auth/providers/username/signup.ts index 478749b9b2..bc15868451 100644 --- a/waspc/data/Generator/templates/server/src/auth/providers/username/signup.ts +++ b/waspc/data/Generator/templates/server/src/auth/providers/username/signup.ts @@ -1,5 +1,5 @@ {{={= =}=}} -import { handleRejection } from '../../../utils.js' +import { handleRejection } from 'wasp/server/utils' import { createProviderId, createUser, diff --git a/waspc/data/Generator/templates/server/src/auth/utils.ts b/waspc/data/Generator/templates/server/src/auth/utils.ts index 4cf9d69203..36facceed5 100644 --- a/waspc/data/Generator/templates/server/src/auth/utils.ts +++ b/waspc/data/Generator/templates/server/src/auth/utils.ts @@ -3,7 +3,7 @@ import { hashPassword, sign, verify } from '../core/auth.js' import AuthError from '../core/AuthError.js' import HttpError from '../core/HttpError.js' import prisma from 'wasp/server/dbClient' -import { sleep } from '../utils.js' +import { sleep } from 'wasp/server/utils' import { type {= userEntityUpper =}, type {= authEntityUpper =}, diff --git a/waspc/data/Generator/templates/server/src/core/auth.js b/waspc/data/Generator/templates/server/src/core/auth.js index 497885affa..c415d73457 100644 --- a/waspc/data/Generator/templates/server/src/core/auth.js +++ b/waspc/data/Generator/templates/server/src/core/auth.js @@ -5,7 +5,7 @@ import util from 'util' import { randomInt } from 'node:crypto' import prisma from 'wasp/server/dbClient' -import { handleRejection } from '../utils.js' +import { handleRejection } from 'wasp/server/utils' import HttpError from '../core/HttpError.js' import config from '../config.js' import { deserializeAndSanitizeProviderData } from '../auth/utils.js' diff --git a/waspc/data/Generator/templates/server/src/middleware/operations.ts b/waspc/data/Generator/templates/server/src/middleware/operations.ts index 677ac113c8..856256c79a 100644 --- a/waspc/data/Generator/templates/server/src/middleware/operations.ts +++ b/waspc/data/Generator/templates/server/src/middleware/operations.ts @@ -3,7 +3,7 @@ import { deserialize as superjsonDeserialize, serialize as superjsonSerialize, } from 'superjson' -import { handleRejection } from '../utils.js' +import { handleRejection } from 'wasp/server/utils' export function createOperation (handlerFn) { return handleRejection(async (req, res) => { diff --git a/waspc/data/Generator/templates/server/src/routes/apis/index.ts b/waspc/data/Generator/templates/server/src/routes/apis/index.ts index 8356536a3f..702b742bb1 100644 --- a/waspc/data/Generator/templates/server/src/routes/apis/index.ts +++ b/waspc/data/Generator/templates/server/src/routes/apis/index.ts @@ -1,7 +1,7 @@ {{={= =}=}} import express from 'express' import prisma from 'wasp/server/dbClient' -import { handleRejection } from '../../utils.js' +import { handleRejection } from 'wasp/server/utils' import { MiddlewareConfigFn, globalMiddlewareConfigForExpress } from '../../middleware/index.js' {=# isAuthEnabled =} import auth from '../../core/auth.js' diff --git a/waspc/data/Generator/templates/server/src/routes/auth/me.js b/waspc/data/Generator/templates/server/src/routes/auth/me.js index acffd77f18..e21057e319 100644 --- a/waspc/data/Generator/templates/server/src/routes/auth/me.js +++ b/waspc/data/Generator/templates/server/src/routes/auth/me.js @@ -1,6 +1,6 @@ {{={= =}=}} import { serialize as superjsonSerialize } from 'superjson' -import { handleRejection } from '../../utils.js' +import { handleRejection } from 'wasp/server/utils' import { throwInvalidCredentialsError } from '../../core/auth.js' export default handleRejection(async (req, res) => { diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json index 9f6d462214..aa4bd7773b 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json @@ -25,7 +25,8 @@ "./universal/url": "./dist/universal/url.js", "./universal/types": "./dist/universal/types.js", "./universal/validators": "./dist/universal/validators.js", - "./server/dbClient": "./dist/server/dbClient.js" + "./server/dbClient": "./dist/server/dbClient.js", + "./server/utils": "./dist/server/utils.js" }, "license": "ISC", "include": [ diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/utils.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/utils.ts index a930149d08..df3052cd57 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/utils.ts +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/utils.ts @@ -5,7 +5,7 @@ import { readdir } from 'fs' import { dirname } from 'path' import { fileURLToPath } from 'url' -import { type SanitizedUser } from './_types/index.js' +import { type SanitizedUser } from 'wasp/server/_types/index.js' type RequestWithExtraFields = Request & { user?: SanitizedUser diff --git a/waspc/src/Wasp/Generator/SdkGenerator.hs b/waspc/src/Wasp/Generator/SdkGenerator.hs index fb8a630300..6b373e11e2 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator.hs @@ -75,6 +75,7 @@ genSdkReal spec = genFileCopy [relfile|api/events.ts|], genFileCopy [relfile|server/dbClient.ts|], genTsConfigJson, + genServerUtils spec, genPackageJson spec ] <++> genUniversalDir @@ -93,7 +94,6 @@ genSdkHardcoded = copyFolder [reldir|rpc|], copyFolder [reldir|server/actions|], copyFolder [reldir|server/queries|], - copyFile [relfile|server/utils.ts|], copyFolder [reldir|types|] ] where @@ -103,8 +103,6 @@ genSdkHardcoded = RemoveExistingDstDir (dstFolder castRel modul) (srcFolder modul) - copyFile :: Path' (Rel SdkTemplatesDir) File' -> FileDraft - copyFile = mkTmplFd dstFolder = sdkRootDirInProjectRootDir srcFolder = absSdkTemplatesDir absSdkTemplatesDir = unsafePerformIO getTemplatesDirAbsPath sdkTemplatesDirInTemplatesDir @@ -231,6 +229,14 @@ mkTmplFdWithDstAndData relSrcPath relDstPath tmplData = mkTmplFdWithDst :: Path' (Rel SdkTemplatesDir) File' -> Path' (Rel SdkRootDir) File' -> FileDraft mkTmplFdWithDst src dst = mkTmplFdWithDstAndData src dst Nothing +mkTmplFdWithData :: + Path' (Rel SdkTemplatesDir) File' -> + Maybe Aeson.Value -> + FileDraft +mkTmplFdWithData relSrcPath tmplData = mkTmplFdWithDstAndData relSrcPath relDstPath tmplData + where + relDstPath = castRel relSrcPath + mkTmplFd :: Path' (Rel SdkTemplatesDir) File' -> FileDraft mkTmplFd path = mkTmplFdWithDst path (SP.castRel path) @@ -286,3 +292,8 @@ genUniversalDir = mkTmplFd [relfile|universal/types.ts|], mkTmplFd [relfile|universal/validators.js|] ] + +genServerUtils :: AppSpec -> Generator FileDraft +genServerUtils spec = return $ mkTmplFdWithData [relfile|server/utils.ts|] (Just tmplData) + where + tmplData = object ["isAuthEnabled" .= (isAuthEnabled spec :: Bool)] \ No newline at end of file diff --git a/waspc/src/Wasp/Generator/ServerGenerator.hs b/waspc/src/Wasp/Generator/ServerGenerator.hs index 4af805a608..039775296d 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator.hs @@ -213,7 +213,6 @@ genSrcDir spec = genConfigFile spec, genServerJs spec ] - <++> genServerUtils spec <++> genRoutesDir spec <++> genOperationsRoutes spec <++> genOperations spec @@ -368,8 +367,3 @@ genOperationsMiddleware spec = (Just tmplData) where tmplData = object ["isAuthEnabled" .= (isAuthEnabled spec :: Bool)] - -genServerUtils :: AppSpec -> Generator [FileDraft] -genServerUtils spec = return [C.mkTmplFdWithData [relfile|src/utils.ts|] (Just tmplData)] - where - tmplData = object ["isAuthEnabled" .= (isAuthEnabled spec :: Bool)] From 0c6ee6c9ffd839a7be6fd2d509599fc77fecf333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Fri, 19 Jan 2024 13:19:03 +0100 Subject: [PATCH 49/54] Add server actions and queries to proper SDK gen --- .../data/Generator/templates/sdk/package.json | 33 +++- .../templates/sdk/server/actions/index.ts | 49 ++---- .../templates/sdk/server/actions/types.ts | 46 ++--- .../templates/sdk/server/queries/index.ts | 25 ++- .../templates/sdk/server/queries/types.ts | 39 ++++- .../Generator/templates/sdk/tsconfig.json | 11 +- .../.wasp/out/sdk/wasp/package.json | 5 +- .../out/sdk/wasp/server/actions/index.ts | 27 ++- .../out/sdk/wasp/server/actions/types.ts | 3 +- .../out/sdk/wasp/server/queries/index.ts | 9 +- .../out/sdk/wasp/server/queries/types.ts | 20 ++- waspc/examples/todo-typescript/main.wasp | 8 +- waspc/src/Wasp/Generator/JsImport.hs | 10 +- waspc/src/Wasp/Generator/SdkGenerator.hs | 83 +++------ .../src/Wasp/Generator/SdkGenerator/Common.hs | 59 +++++++ .../SdkGenerator/ServerOpsGenerator.hs | 160 ++++++++++++++++++ .../Wasp/Generator/ServerGenerator/CrudG.hs | 3 +- .../ServerGenerator/OperationsRoutesG.hs | 34 ++-- waspc/src/Wasp/Generator/WebAppGenerator.hs | 3 +- .../WebAppGenerator/OperationsGenerator.hs | 4 +- waspc/src/Wasp/JsImport.hs | 23 ++- waspc/waspc.cabal | 2 + 22 files changed, 462 insertions(+), 194 deletions(-) create mode 100644 waspc/src/Wasp/Generator/SdkGenerator/Common.hs create mode 100644 waspc/src/Wasp/Generator/SdkGenerator/ServerOpsGenerator.hs diff --git a/waspc/data/Generator/templates/sdk/package.json b/waspc/data/Generator/templates/sdk/package.json index bcfaf60f1d..9ceeee6cb9 100644 --- a/waspc/data/Generator/templates/sdk/package.json +++ b/waspc/data/Generator/templates/sdk/package.json @@ -9,25 +9,56 @@ "types": "tsc --declaration --emitDeclarationOnly --stripInternal --declarationDir dist" }, "exports": { + {=! todo(filip): Check all exports when done with SDK generation =} + {=! Some of the statements in the comments might become incorrect. =} + {=! "our code" means: "web-app", "server" or "SDK", or "some combination of the three". =} + {=! Used by users, documented. =} "./core/HttpError": "./dist/core/HttpError.js", + {=! Used by our code, uncodumented (but accessible) for users. =} "./core/config": "./dist/core/config.js", + {=! Used by our code, uncodumented (but accessible) for users. =} "./core/stitches.config": "./dist/core/stitches.config.js", + {=! Used by our code, uncodumented (but accessible) for users. =} "./core/storage": "./dist/core/storage.js", + {=! Used by users, documented. =} "./rpc": "./dist/rpc/index.js", + {=! Used by users, documented. =} "./rpc/queries": "./dist/rpc/queries/index.js", + {=! Used by users, documented. =} "./rpc/actions": "./dist/rpc/actions/index.js", + {=! Used by our code, uncodumented (but accessible) for users. =} "./rpc/queryClient": "./dist/rpc/queryClient.js", + {=! Used by users, documented. =} "./types": "./dist/types/index.js", + {=! Parts are used by users, documented. Parts are probably used by our code, undocumented (but accessible). =} "./auth/*": "./dist/auth/*", + {=! Used by users, documented. =} "./api": "./dist/api/index.js", + {=! Parts are used by users, documented. Parts are probably used by our code, undocumented (but accessible). =} "./api/*": "./dist/api/*", + {=! Used by users, documented. =} "./operations": "./dist/operations/index.js", + {=! If we import a symbol like "import something form 'wasp/something'", we must =} + {=! expose it here (which leaks it to our users). We could avoid this by =} + {=! using relative imports inside SDK code (instead of library imports), =} + {=! but I didn't have time to implement it. =} + "./ext-src/*": "./dist/ext-src/*.js", + {=! Used by our code, uncodumented (but accessible) for users. =} "./operations/*": "./dist/operations/*", + {=! Used by our code, uncodumented (but accessible) for users. =} "./universal/url": "./dist/universal/url.js", + {=! Used by our code, uncodumented (but accessible) for users. =} "./universal/types": "./dist/universal/types.js", + {=! Used by our code, uncodumented (but accessible) for users. =} "./universal/validators": "./dist/universal/validators.js", + {=! Used by our code, uncodumented (but accessible) for users. =} "./server/dbClient": "./dist/server/dbClient.js", - "./server/utils": "./dist/server/utils.js" + {=! Parts are used by users, documented. Parts are probably used by our code, undocumented (but accessible). =} + "./server/utils": "./dist/server/utils.js", + {=! Used by our code, uncodumented (but accessible) for users. =} + "./server/actions": "./dist/server/actions/index.js", + {=! Used by our code, uncodumented (but accessible) for users. =} + "./server/queries": "./dist/server/queries/index.js" }, "license": "ISC", "include": [ diff --git a/waspc/data/Generator/templates/sdk/server/actions/index.ts b/waspc/data/Generator/templates/sdk/server/actions/index.ts index 5f9800cf8d..c1832fd6d6 100644 --- a/waspc/data/Generator/templates/sdk/server/actions/index.ts +++ b/waspc/data/Generator/templates/sdk/server/actions/index.ts @@ -1,39 +1,26 @@ -import prisma from 'wasp/server/dbClient.js' -import { - updateTask as updateTaskUser, - createTask as createTaskUser, - deleteTasks as deleteTasksUser, -} from 'wasp/ext-src/actions.js' +{{={= =}=}} +import prisma from 'wasp/server/dbClient' +{=! TODO: This template is exactly the same at the moment as one for queries, + consider in the future if it is worth removing this duplication. =} -export type UpdateTask = typeof updateTask +{=! TODO: This will generate multiple import statements even though they're + importing symbols from the same file. We should improve our importing machinery + to support multiple imports from the same file =} +{=# operations =} +{=& jsFn.importStatement =} +{=/ operations =} +{=# operations =} -export const updateTask = async (args, context) => { - return (updateTaskUser as any)(args, { - ...context, - entities: { - Task: prisma.task, - }, - }) -} - -export type CreateTask = typeof createTask - -export const createTask = async (args, context) => { - return (createTaskUser as any)(args, { - ...context, - entities: { - Task: prisma.task, - }, - }) -} - -export type DeleteTasks = typeof deleteTasks +export type {= operationTypeName =} = typeof {= jsFn.importIdentifier =} -export const deleteTasks = async (args, context) => { - return (deleteTasksUser as any)(args, { +export const {= operationName =} = async (args, context) => { + return ({= jsFn.importIdentifier =} as any)(args, { ...context, entities: { - Task: prisma.task, + {=# entities =} + {= name =}: prisma.{= prismaIdentifier =}, + {=/ entities =} }, }) } +{=/ operations =} diff --git a/waspc/data/Generator/templates/sdk/server/actions/types.ts b/waspc/data/Generator/templates/sdk/server/actions/types.ts index 8e03d4963e..15a045acdf 100644 --- a/waspc/data/Generator/templates/sdk/server/actions/types.ts +++ b/waspc/data/Generator/templates/sdk/server/actions/types.ts @@ -1,34 +1,34 @@ +{{={= =}=}} +{=! TODO: This template is exactly the same at the moment as one for query + types, consider whether it makes sense to address this in the future. =} import { - type _Task, + {=# allEntities =} + type {= internalTypeName =}, + {=/ allEntities =} + {=# shouldImportNonAuthenticatedOperation =} + type Action, + {=/ shouldImportNonAuthenticatedOperation =} + {=# shouldImportAuthenticatedOperation =} type AuthenticatedAction, + {=/ shouldImportAuthenticatedOperation =} type Payload, -} from '../_types' +} from 'wasp/server/_types' -export type CreateTask = +{=# operations =} +export type {= typeName =} = + {=# usesAuth =} AuthenticatedAction< + {=/ usesAuth =} + {=^ usesAuth =} + Action< + {=/ usesAuth =} [ - _Task, + {=# entities =} + {= internalTypeName =}, + {=/ entities =} ], Input, Output > -export type UpdateTask = - AuthenticatedAction< - [ - _Task, - ], - Input, - Output - > - -export type DeleteTasks = - AuthenticatedAction< - [ - _Task, - ], - Input, - Output - > - - +{=/ operations =} \ No newline at end of file diff --git a/waspc/data/Generator/templates/sdk/server/queries/index.ts b/waspc/data/Generator/templates/sdk/server/queries/index.ts index 3c49adc9dc..92cb7b7f4f 100644 --- a/waspc/data/Generator/templates/sdk/server/queries/index.ts +++ b/waspc/data/Generator/templates/sdk/server/queries/index.ts @@ -1,13 +1,26 @@ -import prisma from 'wasp/server/dbClient.js' -import { getTasks as getTasksUser } from 'wasp/ext-src/queries.js' +{{={= =}=}} +import prisma from 'wasp/server/dbClient' +{=! TODO: This template is exactly the same at the moment as one for actions, + consider in the future if it is worth removing this duplication. =} -export type GetTasks = typeof getTasksUser +{=! TODO: This will generate multiple import statements even though they're + importing symbols from the same file. We should improve our importing machinery + to support multiple imports from the same file =} +{=# operations =} +{=& jsFn.importStatement =} +{=/ operations =} +{=# operations =} -export const getTasks = async (args, context) => { - return (getTasksUser as any)(args, { +export type {= operationTypeName =} = typeof {= jsFn.importIdentifier =} + +export const {= operationName =} = async (args, context) => { + return ({= jsFn.importIdentifier =} as any)(args, { ...context, entities: { - Task: prisma.task, + {=# entities =} + {= name =}: prisma.{= prismaIdentifier =}, + {=/ entities =} }, }) } +{=/ operations =} diff --git a/waspc/data/Generator/templates/sdk/server/queries/types.ts b/waspc/data/Generator/templates/sdk/server/queries/types.ts index 0617ad4559..3ae08913a3 100644 --- a/waspc/data/Generator/templates/sdk/server/queries/types.ts +++ b/waspc/data/Generator/templates/sdk/server/queries/types.ts @@ -1,6 +1,35 @@ -import { type _Task, type AuthenticatedQuery, type Payload } from "../_types"; +{{={= =}=}} +{=! TODO: This template is exactly the same at the moment as one for action + types, consider whether it makes sense to address this in the future. =} -export type GetTasks< - Input extends Payload = never, - Output extends Payload = Payload -> = AuthenticatedQuery<[_Task], Input, Output>; +import { + {=# allEntities =} + type {= internalTypeName =}, + {=/ allEntities =} + {=# shouldImportNonAuthenticatedOperation =} + type Query, + {=/ shouldImportNonAuthenticatedOperation =} + {=# shouldImportAuthenticatedOperation =} + type AuthenticatedQuery, + {=/ shouldImportAuthenticatedOperation =} + type Payload, +} from 'wasp/server/_types' + +{=# operations =} +export type {= typeName =} = + {=# usesAuth =} + AuthenticatedQuery< + {=/ usesAuth =} + {=^ usesAuth =} + Query< + {=/ usesAuth =} + [ + {=# entities =} + {= internalTypeName =}, + {=/ entities =} + ], + Input, + Output + > + +{=/ operations =} \ No newline at end of file diff --git a/waspc/data/Generator/templates/sdk/tsconfig.json b/waspc/data/Generator/templates/sdk/tsconfig.json index 007635ff4c..1d861dcecc 100644 --- a/waspc/data/Generator/templates/sdk/tsconfig.json +++ b/waspc/data/Generator/templates/sdk/tsconfig.json @@ -21,11 +21,12 @@ "moduleResolution": "node", "outDir": "dist", "allowJs": true - }, - "paths": { - "wasp/*": [ - "/*" - ] + // todo(filip): Only works with common js, see https://www.typescriptlang.org/tsconfig#paths and daily-article. + // "paths": { + // "@wasp/*": [ + // "./*.js" + // ] + // } }, "include": [ "." diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json index aa4bd7773b..13ab00fecf 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json @@ -21,12 +21,15 @@ "./api": "./dist/api/index.js", "./api/*": "./dist/api/*", "./operations": "./dist/operations/index.js", + "./ext-src/*": "./dist/ext-src/*.js", "./operations/*": "./dist/operations/*", "./universal/url": "./dist/universal/url.js", "./universal/types": "./dist/universal/types.js", "./universal/validators": "./dist/universal/validators.js", "./server/dbClient": "./dist/server/dbClient.js", - "./server/utils": "./dist/server/utils.js" + "./server/utils": "./dist/server/utils.js", + "./server/actions": "./dist/server/actions/index.js", + "./server/queries": "./dist/server/queries/index.js" }, "license": "ISC", "include": [ diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/actions/index.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/actions/index.ts index 5f9800cf8d..9cafc77c30 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/actions/index.ts +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/actions/index.ts @@ -1,14 +1,13 @@ -import prisma from 'wasp/server/dbClient.js' -import { - updateTask as updateTaskUser, - createTask as createTaskUser, - deleteTasks as deleteTasksUser, -} from 'wasp/ext-src/actions.js' +import prisma from 'wasp/server/dbClient' -export type UpdateTask = typeof updateTask +import { createTask as createTask_ext } from 'wasp/ext-src/task/actions' +import { updateTask as updateTask_ext } from 'wasp/ext-src/task/actions' +import { deleteTasks as deleteTasks_ext } from 'wasp/ext-src/task/actions' -export const updateTask = async (args, context) => { - return (updateTaskUser as any)(args, { +export type CreateTask = typeof createTask_ext + +export const createTask = async (args, context) => { + return (createTask_ext as any)(args, { ...context, entities: { Task: prisma.task, @@ -16,10 +15,10 @@ export const updateTask = async (args, context) => { }) } -export type CreateTask = typeof createTask +export type UpdateTask = typeof updateTask_ext -export const createTask = async (args, context) => { - return (createTaskUser as any)(args, { +export const updateTask = async (args, context) => { + return (updateTask_ext as any)(args, { ...context, entities: { Task: prisma.task, @@ -27,10 +26,10 @@ export const createTask = async (args, context) => { }) } -export type DeleteTasks = typeof deleteTasks +export type DeleteTasks = typeof deleteTasks_ext export const deleteTasks = async (args, context) => { - return (deleteTasksUser as any)(args, { + return (deleteTasks_ext as any)(args, { ...context, entities: { Task: prisma.task, diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/actions/types.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/actions/types.ts index 8e03d4963e..11f743b28d 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/actions/types.ts +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/actions/types.ts @@ -2,7 +2,7 @@ import { type _Task, type AuthenticatedAction, type Payload, -} from '../_types' +} from 'wasp/server/_types' export type CreateTask = AuthenticatedAction< @@ -31,4 +31,3 @@ export type DeleteTasks - diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/queries/index.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/queries/index.ts index 3c49adc9dc..7325cc4fea 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/queries/index.ts +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/queries/index.ts @@ -1,10 +1,11 @@ -import prisma from 'wasp/server/dbClient.js' -import { getTasks as getTasksUser } from 'wasp/ext-src/queries.js' +import prisma from 'wasp/server/dbClient' -export type GetTasks = typeof getTasksUser +import { getTasks as getTasks_ext } from 'wasp/ext-src/task/queries' + +export type GetTasks = typeof getTasks_ext export const getTasks = async (args, context) => { - return (getTasksUser as any)(args, { + return (getTasks_ext as any)(args, { ...context, entities: { Task: prisma.task, diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/queries/types.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/queries/types.ts index 0617ad4559..f7391d0da2 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/queries/types.ts +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/queries/types.ts @@ -1,6 +1,16 @@ -import { type _Task, type AuthenticatedQuery, type Payload } from "../_types"; -export type GetTasks< - Input extends Payload = never, - Output extends Payload = Payload -> = AuthenticatedQuery<[_Task], Input, Output>; +import { + type _Task, + type AuthenticatedQuery, + type Payload, +} from 'wasp/server/_types' + +export type GetTasks = + AuthenticatedQuery< + [ + _Task, + ], + Input, + Output + > + diff --git a/waspc/examples/todo-typescript/main.wasp b/waspc/examples/todo-typescript/main.wasp index a2dd70db1c..21d03e54dc 100644 --- a/waspc/examples/todo-typescript/main.wasp +++ b/waspc/examples/todo-typescript/main.wasp @@ -53,23 +53,23 @@ query getTasks { // We specify the JS implementation of our query (which is an async JS function) // Even if you use TS and have a queries.ts file, you will still need to import it using the .js extension. // see here for more info: https://wasp-lang.dev/docs/tutorials/todo-app/03-listing-tasks#wasp-declaration - fn: import { getTasks } from "@src/task/queries.js", + fn: import { getTasks } from "@src/task/queries", // We tell Wasp that this query is doing something with the `Task` entity. With that, Wasp will // automatically refresh the results of this query when tasks change. entities: [Task] } action createTask { - fn: import { createTask } from "@src/task/actions.js", + fn: import { createTask } from "@src/task/actions", entities: [Task] } action updateTask { - fn: import { updateTask } from "@src/task/actions.js", + fn: import { updateTask } from "@src/task/actions", entities: [Task] } action deleteTasks { - fn: import { deleteTasks } from "@src/task/actions.js", + fn: import { deleteTasks } from "@src/task/actions", entities: [Task], } diff --git a/waspc/src/Wasp/Generator/JsImport.hs b/waspc/src/Wasp/Generator/JsImport.hs index 25db2c7df3..9df2fea89f 100644 --- a/waspc/src/Wasp/Generator/JsImport.hs +++ b/waspc/src/Wasp/Generator/JsImport.hs @@ -1,6 +1,7 @@ module Wasp.Generator.JsImport ( extImportToJsImport, jsImportToImportJson, + extImportNameToJsImportName, ) where @@ -14,6 +15,7 @@ import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir) import Wasp.JsImport ( JsImport, JsImportName (JsImportField, JsImportModule), + JsImportPath (RelativeImportPath), getJsImportStmtAndIdentifier, makeJsImport, ) @@ -24,15 +26,15 @@ extImportToJsImport :: Path Posix (Rel importLocation) (Dir d) -> EI.ExtImport -> JsImport -extImportToJsImport pathFromSrcDirToExtCodeDir pathFromImportLocationToSrcDir extImport = makeJsImport importPath importName +extImportToJsImport pathFromSrcDirToExtCodeDir pathFromImportLocationToSrcDir extImport = makeJsImport (RelativeImportPath importPath) importName where userDefinedPathInExtSrcDir = SP.castRel $ EI.path extImport :: Path Posix (Rel GeneratedExternalCodeDir) File' importName = extImportNameToJsImportName $ EI.name extImport importPath = SP.castRel $ pathFromImportLocationToSrcDir pathFromSrcDirToExtCodeDir userDefinedPathInExtSrcDir - extImportNameToJsImportName :: EI.ExtImportName -> JsImportName - extImportNameToJsImportName (EI.ExtImportModule name) = JsImportModule name - extImportNameToJsImportName (EI.ExtImportField name) = JsImportField name +extImportNameToJsImportName :: EI.ExtImportName -> JsImportName +extImportNameToJsImportName (EI.ExtImportModule name) = JsImportModule name +extImportNameToJsImportName (EI.ExtImportField name) = JsImportField name jsImportToImportJson :: Maybe JsImport -> Aeson.Value jsImportToImportJson maybeJsImport = maybe notDefinedValue mkTmplData maybeJsImport diff --git a/waspc/src/Wasp/Generator/SdkGenerator.hs b/waspc/src/Wasp/Generator/SdkGenerator.hs index 6b373e11e2..8299187a1a 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator.hs @@ -4,7 +4,6 @@ module Wasp.Generator.SdkGenerator ( genSdk, installNpmDependencies, genExternalCodeDir, - sdkRootDirInProjectRootDir, buildSdk, ) where @@ -12,7 +11,6 @@ where import Control.Concurrent (newChan) import Control.Concurrent.Async (concurrently) import Data.Aeson (object) -import qualified Data.Aeson as Aeson import Data.Aeson.Types ((.=)) import Data.Maybe (fromMaybe, isJust, mapMaybe) import GHC.IO (unsafePerformIO) @@ -31,8 +29,7 @@ import Wasp.AppSpec.Valid (getLowestNodeVersionUserAllows, isAuthEnabled) import qualified Wasp.AppSpec.Valid as AS.Valid import Wasp.Generator.Common (ProjectRootDir, makeJsonWithEntityData, prismaVersion) import qualified Wasp.Generator.DbGenerator.Auth as DbAuth -import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir) -import Wasp.Generator.FileDraft (FileDraft, createCopyDirFileDraft, createTemplateFileDraft) +import Wasp.Generator.FileDraft (FileDraft, createCopyDirFileDraft) import qualified Wasp.Generator.FileDraft as FD import Wasp.Generator.FileDraft.CopyDirFileDraft (CopyDirFileDraftDstDirStrategy (RemoveExistingDstDir)) import qualified Wasp.Generator.Job as J @@ -40,16 +37,15 @@ import Wasp.Generator.Job.IO (readJobMessagesAndPrintThemPrefixed) import Wasp.Generator.Job.Process (runNodeCommandAsJob) import Wasp.Generator.Monad (Generator) import qualified Wasp.Generator.NpmDependencies as N -import Wasp.Generator.Templates (TemplatesDir, getTemplatesDirAbsPath) +import Wasp.Generator.SdkGenerator.Common (SdkTemplatesDir) +import qualified Wasp.Generator.SdkGenerator.Common as C +import Wasp.Generator.SdkGenerator.ServerOpsGenerator (genOperations) +import Wasp.Generator.Templates (getTemplatesDirAbsPath) import qualified Wasp.Node.Version as NodeVersion import Wasp.Project.Common (WaspProjectDir) import qualified Wasp.SemanticVersion as SV import Wasp.Util (toLowerFirst, (<++>)) -data SdkRootDir - -data SdkTemplatesDir - genSdk :: AppSpec -> Generator [FileDraft] genSdk spec = genSdkHardcoded @@ -66,7 +62,7 @@ buildSdk projectRootDir = do ExitSuccess -> return $ Right () ExitFailure code -> return $ Left $ "SDK build failed with exit code: " ++ show code where - dstDir = projectRootDir sdkRootDirInProjectRootDir + dstDir = projectRootDir C.sdkRootDirInProjectRootDir genSdkReal :: AppSpec -> Generator [FileDraft] genSdkReal spec = @@ -78,11 +74,12 @@ genSdkReal spec = genServerUtils spec, genPackageJson spec ] + <++> genOperations spec <++> genUniversalDir <++> genExternalCodeDir (AS.externalCodeFiles spec) <++> genTypesAndEntitiesDirs spec where - genFileCopy = return . mkTmplFd + genFileCopy = return . C.mkTmplFd genSdkHardcoded :: Generator [FileDraft] genSdkHardcoded = @@ -92,8 +89,6 @@ genSdkHardcoded = copyFolder [reldir|ext-src|], copyFolder [reldir|operations|], copyFolder [reldir|rpc|], - copyFolder [reldir|server/actions|], - copyFolder [reldir|server/queries|], copyFolder [reldir|types|] ] where @@ -103,9 +98,9 @@ genSdkHardcoded = RemoveExistingDstDir (dstFolder castRel modul) (srcFolder modul) - dstFolder = sdkRootDirInProjectRootDir + dstFolder = C.sdkRootDirInProjectRootDir srcFolder = absSdkTemplatesDir - absSdkTemplatesDir = unsafePerformIO getTemplatesDirAbsPath sdkTemplatesDirInTemplatesDir + absSdkTemplatesDir = unsafePerformIO getTemplatesDirAbsPath C.sdkTemplatesDirInTemplatesDir genTypesAndEntitiesDirs :: AppSpec -> Generator [FileDraft] genTypesAndEntitiesDirs spec = @@ -117,7 +112,7 @@ genTypesAndEntitiesDirs spec = ] where entitiesIndexFileDraft = - mkTmplFdWithDstAndData + C.mkTmplFdWithDstAndData [relfile|entities/index.ts|] [relfile|entities/index.ts|] ( Just $ @@ -129,15 +124,15 @@ genTypesAndEntitiesDirs spec = ] ) taggedEntitiesFileDraft = - mkTmplFdWithDstAndData + C.mkTmplFdWithDstAndData [relfile|server/_types/taggedEntities.ts|] [relfile|server/_types/taggedEntities.ts|] (Just $ object ["entities" .= allEntities]) serializationFileDraft = - mkTmplFd + C.mkTmplFd [relfile|server/_types/serialization.ts|] typesIndexFileDraft = - mkTmplFdWithDstAndData + C.mkTmplFdWithDstAndData [relfile|server/_types/index.ts|] [relfile|server/_types/index.ts|] ( Just $ @@ -159,7 +154,7 @@ genTypesAndEntitiesDirs spec = genPackageJson :: AppSpec -> Generator FileDraft genPackageJson spec = return $ - mkTmplFdWithDstAndData + C.mkTmplFdWithDstAndData [relfile|package.json|] [relfile|package.json|] ( Just $ @@ -200,7 +195,7 @@ genPackageJson spec = genTsConfigJson :: Generator FileDraft genTsConfigJson = do return $ - mkTmplFdWithDstAndData + C.mkTmplFdWithDstAndData [relfile|tsconfig.json|] [relfile|tsconfig.json|] ( Just $ @@ -215,40 +210,6 @@ depsRequiredForAuth spec = where versionRange = SV.Range [SV.backwardsCompatibleWith (SV.Version 1 2 8)] -mkTmplFdWithDstAndData :: - Path' (Rel SdkTemplatesDir) File' -> - Path' (Rel SdkRootDir) File' -> - Maybe Aeson.Value -> - FileDraft -mkTmplFdWithDstAndData relSrcPath relDstPath tmplData = - createTemplateFileDraft - (sdkRootDirInProjectRootDir relDstPath) - (sdkTemplatesDirInTemplatesDir relSrcPath) - tmplData - -mkTmplFdWithDst :: Path' (Rel SdkTemplatesDir) File' -> Path' (Rel SdkRootDir) File' -> FileDraft -mkTmplFdWithDst src dst = mkTmplFdWithDstAndData src dst Nothing - -mkTmplFdWithData :: - Path' (Rel SdkTemplatesDir) File' -> - Maybe Aeson.Value -> - FileDraft -mkTmplFdWithData relSrcPath tmplData = mkTmplFdWithDstAndData relSrcPath relDstPath tmplData - where - relDstPath = castRel relSrcPath - -mkTmplFd :: Path' (Rel SdkTemplatesDir) File' -> FileDraft -mkTmplFd path = mkTmplFdWithDst path (SP.castRel path) - -sdkRootDirInProjectRootDir :: Path' (Rel ProjectRootDir) (Dir SdkRootDir) -sdkRootDirInProjectRootDir = [reldir|sdk/wasp|] - -sdkTemplatesDirInTemplatesDir :: Path' (Rel TemplatesDir) (Dir SdkTemplatesDir) -sdkTemplatesDirInTemplatesDir = [reldir|sdk|] - -extSrcDirInSdkRootDir :: Path' (Rel SdkRootDir) (Dir GeneratedExternalCodeDir) -extSrcDirInSdkRootDir = [reldir|ext-src|] - -- todo(filip): figure out where this belongs -- also, fix imports for wasp project installNpmDependencies :: Path' Abs (Dir WaspProjectDir) -> J.Job @@ -275,7 +236,7 @@ genFile file genResourceFile :: EC.CodeFile -> Generator FileDraft genResourceFile file = return $ FD.createCopyFileDraft relDstPath absSrcPath where - relDstPath = sdkRootDirInProjectRootDir extSrcDirInSdkRootDir SP.castRel (EC._pathInExtCodeDir file) + relDstPath = C.sdkRootDirInProjectRootDir C.extSrcDirInSdkRootDir SP.castRel (EC._pathInExtCodeDir file) absSrcPath = EC.fileAbsPath file genSourceFile :: EC.CodeFile -> Generator FD.FileDraft @@ -283,17 +244,17 @@ genSourceFile file = return $ FD.createTextFileDraft relDstPath text where filePathInSrcExtCodeDir = EC.filePathInExtCodeDir file text = EC.fileText file - relDstPath = sdkRootDirInProjectRootDir extSrcDirInSdkRootDir SP.castRel filePathInSrcExtCodeDir + relDstPath = C.sdkRootDirInProjectRootDir C.extSrcDirInSdkRootDir SP.castRel filePathInSrcExtCodeDir genUniversalDir :: Generator [FileDraft] genUniversalDir = return - [ mkTmplFd [relfile|universal/url.ts|], - mkTmplFd [relfile|universal/types.ts|], - mkTmplFd [relfile|universal/validators.js|] + [ C.mkTmplFd [relfile|universal/url.ts|], + C.mkTmplFd [relfile|universal/types.ts|], + C.mkTmplFd [relfile|universal/validators.js|] ] genServerUtils :: AppSpec -> Generator FileDraft -genServerUtils spec = return $ mkTmplFdWithData [relfile|server/utils.ts|] (Just tmplData) +genServerUtils spec = return $ C.mkTmplFdWithData [relfile|server/utils.ts|] (Just tmplData) where tmplData = object ["isAuthEnabled" .= (isAuthEnabled spec :: Bool)] \ No newline at end of file diff --git a/waspc/src/Wasp/Generator/SdkGenerator/Common.hs b/waspc/src/Wasp/Generator/SdkGenerator/Common.hs new file mode 100644 index 0000000000..bf64691298 --- /dev/null +++ b/waspc/src/Wasp/Generator/SdkGenerator/Common.hs @@ -0,0 +1,59 @@ +module Wasp.Generator.SdkGenerator.Common where + +import qualified Data.Aeson as Aeson +import Data.Maybe (fromJust) +import StrongPath +import qualified StrongPath as SP +import Wasp.Generator.Common (ProjectRootDir) +import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir) +import Wasp.Generator.FileDraft (FileDraft, createTemplateFileDraft) +import Wasp.Generator.Templates (TemplatesDir) + +data SdkRootDir + +data SdkTemplatesDir + +mkTmplFdWithDstAndData :: + Path' (Rel SdkTemplatesDir) File' -> + Path' (Rel SdkRootDir) File' -> + Maybe Aeson.Value -> + FileDraft +mkTmplFdWithDstAndData relSrcPath relDstPath tmplData = + createTemplateFileDraft + (sdkRootDirInProjectRootDir relDstPath) + (sdkTemplatesDirInTemplatesDir relSrcPath) + tmplData + +mkTmplFdWithDst :: Path' (Rel SdkTemplatesDir) File' -> Path' (Rel SdkRootDir) File' -> FileDraft +mkTmplFdWithDst src dst = mkTmplFdWithDstAndData src dst Nothing + +mkTmplFdWithData :: + Path' (Rel SdkTemplatesDir) File' -> + Maybe Aeson.Value -> + FileDraft +mkTmplFdWithData relSrcPath tmplData = mkTmplFdWithDstAndData relSrcPath relDstPath tmplData + where + relDstPath = castRel relSrcPath + +mkTmplFd :: Path' (Rel SdkTemplatesDir) File' -> FileDraft +mkTmplFd path = mkTmplFdWithDst path (SP.castRel path) + +sdkRootDirInProjectRootDir :: Path' (Rel ProjectRootDir) (Dir SdkRootDir) +sdkRootDirInProjectRootDir = [reldir|sdk/wasp|] + +sdkTemplatesDirInTemplatesDir :: Path' (Rel TemplatesDir) (Dir SdkTemplatesDir) +sdkTemplatesDirInTemplatesDir = [reldir|sdk|] + +extSrcDirInSdkRootDir :: Path' (Rel SdkRootDir) (Dir GeneratedExternalCodeDir) +extSrcDirInSdkRootDir = [reldir|ext-src|] + +relDirToRelFileP :: Path Posix (Rel d) Dir' -> Path Posix (Rel d) File' +relDirToRelFileP path = fromJust $ SP.parseRelFileP $ removeTrailingSlash $ SP.fromRelDirP path + where + removeTrailingSlash = reverse . dropWhile (== '/') . reverse + +makeSdkImportPath :: Path Posix (Rel SdkRootDir) File' -> Path Posix (Rel s) File' +makeSdkImportPath path = [reldirP|wasp|] path + +extCodeDirInSdkRootDir :: Path' (Rel SdkRootDir) Dir' +extCodeDirInSdkRootDir = [reldir|ext-src|] diff --git a/waspc/src/Wasp/Generator/SdkGenerator/ServerOpsGenerator.hs b/waspc/src/Wasp/Generator/SdkGenerator/ServerOpsGenerator.hs new file mode 100644 index 0000000000..dad6b7289d --- /dev/null +++ b/waspc/src/Wasp/Generator/SdkGenerator/ServerOpsGenerator.hs @@ -0,0 +1,160 @@ +{-# LANGUAGE TypeApplications #-} + +module Wasp.Generator.SdkGenerator.ServerOpsGenerator where + +import Data.Aeson (object, (.=)) +import qualified Data.Aeson as Aeson +import Data.List (nub) +import Data.Maybe (fromJust, fromMaybe) +import StrongPath (Dir', File', Path', Rel, reldir, relfile, ()) +import qualified StrongPath as SP +import Wasp.AppSpec (AppSpec) +import qualified Wasp.AppSpec as AS +import qualified Wasp.AppSpec.Action as AS.Action +import qualified Wasp.AppSpec.ExtImport as EI +import Wasp.AppSpec.Operation (getName) +import qualified Wasp.AppSpec.Operation as AS.Operation +import qualified Wasp.AppSpec.Query as AS.Query +import Wasp.AppSpec.Valid (isAuthEnabled) +import Wasp.Generator.Common (makeJsonWithEntityData) +import Wasp.Generator.FileDraft (FileDraft) +import qualified Wasp.Generator.JsImport as GJI +import Wasp.Generator.Monad (Generator) +import Wasp.Generator.SdkGenerator.Common (mkTmplFdWithData) +import qualified Wasp.Generator.SdkGenerator.Common as C +import Wasp.JsImport (JsImport (..), JsImportPath (..)) +import qualified Wasp.JsImport as JI +import Wasp.Util (toUpperFirst) + +genOperations :: AppSpec -> Generator [FileDraft] +genOperations spec = + sequence + [ genQueryTypesFile spec, + genActionTypesFile spec, + genQueriesIndex spec, + genActionsIndex spec + ] + +genQueriesIndex :: AppSpec -> Generator FileDraft +genQueriesIndex spec = return $ mkTmplFdWithData relPath (Just tmplData) + where + relPath = [relfile|server/queries/index.ts|] + tmplData = + object + [ "operations" .= map getQueryData (AS.getQueries spec) + ] + +genActionsIndex :: AppSpec -> Generator FileDraft +genActionsIndex spec = return $ mkTmplFdWithData relPath (Just tmplData) + where + relPath = [relfile|server/actions/index.ts|] + tmplData = + object + [ "operations" .= map getActionData (AS.getActions spec) + ] + +genQueryTypesFile :: AppSpec -> Generator FileDraft +genQueryTypesFile spec = genOperationTypesFile tmplFile dstFile operations isAuthEnabledGlobally + where + tmplFile = [relfile|server/queries/types.ts|] + dstFile = [relfile|server/queries/types.ts|] + operations = map (uncurry AS.Operation.QueryOp) $ AS.getQueries spec + isAuthEnabledGlobally = isAuthEnabled spec + +genActionTypesFile :: AppSpec -> Generator FileDraft +genActionTypesFile spec = genOperationTypesFile tmplFile dstFile operations isAuthEnabledGlobally + where + tmplFile = [relfile|server/actions/types.ts|] + dstFile = [relfile|server/actions/types.ts|] + operations = map (uncurry AS.Operation.ActionOp) $ AS.getActions spec + isAuthEnabledGlobally = isAuthEnabled spec + +-- | Here we generate JS file that basically imports JS query function provided by user, +-- decorates it (mostly injects stuff into it) and exports. Idea is that the rest of the server, +-- and user also, should use this new JS function, and not the old one directly. +getQueryData :: (String, AS.Query.Query) -> Aeson.Value +getQueryData (queryName, query) = getOperationTmplData operation + where + operation = AS.Operation.QueryOp queryName query + +getActionData :: (String, AS.Action.Action) -> Aeson.Value +getActionData (actionName, action) = getOperationTmplData operation + where + operation = AS.Operation.ActionOp actionName action + +genOperationTypesFile :: + Path' (Rel C.SdkTemplatesDir) File' -> + Path' (Rel C.SdkRootDir) File' -> + [AS.Operation.Operation] -> + Bool -> + Generator FileDraft +genOperationTypesFile tmplFile dstFile operations isAuthEnabledGlobally = + return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) + where + tmplData = + object + [ "operations" .= map operationTypeData operations, + "shouldImportAuthenticatedOperation" .= any usesAuth operations, + "shouldImportNonAuthenticatedOperation" .= not (all usesAuth operations), + "allEntities" .= nub (concatMap getEntities operations) + ] + operationTypeData operation = + object + [ "typeName" .= toUpperFirst (getName operation), + "entities" .= getEntities operation, + "usesAuth" .= usesAuth operation + ] + getEntities = map makeJsonWithEntityData . maybe [] (map AS.refName) . AS.Operation.getEntities + usesAuth = fromMaybe isAuthEnabledGlobally . AS.Operation.getAuth + +operationsDirInSdkRootDir :: AS.Operation.Operation -> Path' (Rel C.SdkRootDir) Dir' +operationsDirInSdkRootDir (AS.Operation.QueryOp _ _) = [reldir|server/queries|] +operationsDirInSdkRootDir (AS.Operation.ActionOp _ _) = [reldir|server/actions|] + +getOperationTmplData :: AS.Operation.Operation -> Aeson.Value +getOperationTmplData operation = + object + [ "jsFn" .= extOperationImportToImportJson (AS.Operation.getFn operation), + "operationName" .= getName operation, + "operationTypeName" .= toUpperFirst (getName operation), + "entities" + .= maybe [] (map (makeJsonWithEntityData . AS.refName)) (AS.Operation.getEntities operation) + ] + +extOperationImportToImportJson :: EI.ExtImport -> Aeson.Value +extOperationImportToImportJson = + GJI.jsImportToImportJson + . Just + . applyExtImportAlias + . extImportToJsImport + +applyExtImportAlias :: JsImport -> JsImport +applyExtImportAlias jsImport = + jsImport {_importAlias = Just $ JI.getImportIdentifier jsImport ++ "_ext"} + +extImportToJsImport :: EI.ExtImport -> JsImport +extImportToJsImport extImport@(EI.ExtImport extImportName extImportPath) = + JsImport + { _path = ModuleImportPath importPath, + _name = importName, + _importAlias = Just $ EI.importIdentifier extImport ++ "_ext" + } + where + importPath = C.makeSdkImportPath $ extCodeDirP SP.castRel extImportPath + extCodeDirP = fromJust $ SP.relDirToPosix C.extCodeDirInSdkRootDir + importName = GJI.extImportNameToJsImportName extImportName + +-- extImportToImportJson :: EI.ExtImport -> Aeson.Value +-- extImportToImportJson extImport@(EI.ExtImport importName importPath) = +-- object +-- [ "isDefined" .= True, +-- "importStatement" .= Debug.trace jsImportStmt jsImportStmt, +-- "importIdentifier" .= importAlias +-- ] +-- where +-- jsImportStmt = case importName of +-- EI.ExtImportModule n -> "import " ++ n ++ " from '" ++ importPathStr ++ "'" +-- EI.ExtImportField n -> "import { " ++ n ++ " as " ++ importAlias ++ " } from '" ++ importPathStr ++ "'" +-- importPathStr = C.makeSdkImportPath $ extCodeDirP SP.castRel importPath +-- extCodeDirP = fromJust $ SP.relDirToPosix C.extCodeDirInSdkRootDir +-- importAlias = EI.importIdentifier extImport ++ "User" diff --git a/waspc/src/Wasp/Generator/ServerGenerator/CrudG.hs b/waspc/src/Wasp/Generator/ServerGenerator/CrudG.hs index 242ce4596f..0b5689d582 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/CrudG.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/CrudG.hs @@ -26,6 +26,7 @@ import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.Monad (Generator) import qualified Wasp.Generator.ServerGenerator.Common as C import Wasp.Generator.ServerGenerator.JsImport (extImportToImportJson) +import Wasp.JsImport (JsImportPath (RelativeImportPath)) import qualified Wasp.JsImport as JI import Wasp.Util ((<++>)) @@ -59,7 +60,7 @@ genCrudIndexRoute cruds = return $ C.mkTmplFdWithData tmplPath (Just tmplData) JI.getJsImportStmtAndIdentifier JI.JsImport { JI._name = JI.JsImportField name, - JI._path = fromJust . SP.relFileToPosix $ getCrudFilePath name "js", + JI._path = RelativeImportPath (fromJust . SP.relFileToPosix $ getCrudFilePath name "js"), JI._importAlias = Nothing } diff --git a/waspc/src/Wasp/Generator/ServerGenerator/OperationsRoutesG.hs b/waspc/src/Wasp/Generator/ServerGenerator/OperationsRoutesG.hs index 4da990b3a1..cab064810d 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/OperationsRoutesG.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/OperationsRoutesG.hs @@ -8,7 +8,7 @@ where import Data.Aeson (object, (.=)) import Data.Maybe (fromJust, fromMaybe, isJust) -import StrongPath (Dir, File', Path, Path', Posix, Rel, reldir, reldirP, relfile, ()) +import StrongPath (Dir, File', Path', Rel, reldir, relfile, ()) import qualified StrongPath as SP import Wasp.AppSpec (AppSpec) import qualified Wasp.AppSpec as AS @@ -19,9 +19,15 @@ import Wasp.AppSpec.Valid (isAuthEnabled) import Wasp.Generator.Common (ServerRootDir) import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.Monad (Generator, GeneratorError (GenericGeneratorError), logAndThrowGeneratorError) +import Wasp.Generator.SdkGenerator.Common (makeSdkImportPath, relDirToRelFileP) +import Wasp.Generator.SdkGenerator.ServerOpsGenerator (operationsDirInSdkRootDir) import qualified Wasp.Generator.ServerGenerator.Common as C -import Wasp.Generator.ServerGenerator.OperationsG (operationFileInSrcDir) -import Wasp.JsImport (JsImportName (..), getJsImportStmtAndIdentifier, makeJsImport) +import Wasp.JsImport + ( JsImportName (..), + JsImportPath (ModuleImportPath, RelativeImportPath), + getJsImportStmtAndIdentifier, + makeJsImport, + ) import qualified Wasp.Util as U genOperationsRoutes :: AppSpec -> Generator [FileDraft] @@ -58,21 +64,16 @@ genOperationRoute operation tmplFile = return $ C.mkTmplFdWithDstAndData tmplFil ] ] - pathToOperationFile = - relPosixPathFromOperationsRoutesDirToSrcDir - fromJust (SP.relFileToPosix $ operationFileInSrcDir operation) - - operationImportPath = - fromJust $ - SP.parseRelFileP $ - C.toESModulesImportPath $ - SP.fromRelFileP pathToOperationFile - operationName = AS.Operation.getName operation (operationImportStmt, operationImportIdentifier) = getJsImportStmtAndIdentifier $ - makeJsImport operationImportPath (JsImportModule operationName) + makeJsImport (ModuleImportPath sdkImportPath) (JsImportField operationName) + sdkImportPath = + makeSdkImportPath $ + relDirToRelFileP $ + fromJust $ + SP.relDirToPosix $ operationsDirInSdkRootDir operation data OperationsRoutesDir @@ -85,9 +86,6 @@ operationsRoutesDirInServerRootDir = C.serverSrcDirInServerRootDir operation operationRouteFileInOperationsRoutesDir :: AS.Operation.Operation -> Path' (Rel OperationsRoutesDir) File' operationRouteFileInOperationsRoutesDir operation = fromJust $ SP.parseRelFile $ AS.Operation.getName operation ++ ".js" -relPosixPathFromOperationsRoutesDirToSrcDir :: Path Posix (Rel OperationsRoutesDir) (Dir C.ServerSrcDir) -relPosixPathFromOperationsRoutesDirToSrcDir = [reldirP|../..|] - genOperationsRouter :: AppSpec -> Generator FileDraft genOperationsRouter spec -- TODO: Right now we are throwing error here, but we should instead perform this check in parsing/analyzer phase, as a semantic check, since we have all the info we need then already. @@ -107,7 +105,7 @@ genOperationsRouter spec makeOperationRoute operation = let operationName = AS.Operation.getName operation importPath = fromJust $ SP.relFileToPosix $ SP.castRel $ operationRouteFileInOperationsRoutesDir operation - (importStmt, importIdentifier) = getJsImportStmtAndIdentifier $ makeJsImport importPath (JsImportModule operationName) + (importStmt, importIdentifier) = getJsImportStmtAndIdentifier $ makeJsImport (RelativeImportPath importPath) (JsImportModule operationName) in object [ "importIdentifier" .= importIdentifier, "importStatement" .= importStmt, diff --git a/waspc/src/Wasp/Generator/WebAppGenerator.hs b/waspc/src/Wasp/Generator/WebAppGenerator.hs index b553df8da0..5ea41861e2 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator.hs @@ -54,6 +54,7 @@ import qualified Wasp.Generator.WebSocket as AS.WS import Wasp.JsImport ( JsImport, JsImportName (JsImportModule), + JsImportPath (RelativeImportPath), makeJsImport, ) import qualified Wasp.Node.Version as NodeVersion @@ -320,7 +321,7 @@ genViteConfig spec = return $ C.mkTmplFdWithData tmplFile tmplData ] makeCustomViteConfigJsImport :: Path' (Rel SourceExternalCodeDir) File' -> JsImport - makeCustomViteConfigJsImport pathToConfig = makeJsImport importPath importName + makeCustomViteConfigJsImport pathToConfig = makeJsImport (RelativeImportPath importPath) importName where importPath = C.toViteImportPath $ fromJust $ SP.relFileToPosix pathToConfigInSrc pathToConfigInSrc = SP.castRel $ C.webAppSrcDirInWebAppRootDir SP.castRel pathToConfig diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/OperationsGenerator.hs b/waspc/src/Wasp/Generator/WebAppGenerator/OperationsGenerator.hs index 438f1d60d5..abe4106dab 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/OperationsGenerator.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/OperationsGenerator.hs @@ -28,7 +28,7 @@ import qualified Wasp.Generator.ServerGenerator.OperationsRoutesG as ServerOpera import Wasp.Generator.WebAppGenerator.Common (serverRootDirFromWebAppRootDir, toViteImportPath) import qualified Wasp.Generator.WebAppGenerator.Common as C import qualified Wasp.Generator.WebAppGenerator.OperationsGenerator.ResourcesG as Resources -import Wasp.JsImport (JsImportName (JsImportField), getJsImportStmtAndIdentifier, makeJsImport) +import Wasp.JsImport (JsImportName (JsImportField), JsImportPath (RelativeImportPath), getJsImportStmtAndIdentifier, makeJsImport) import Wasp.Util (toUpperFirst, (<++>)) genOperations :: AppSpec -> Generator [FileDraft] @@ -106,7 +106,7 @@ operationTypeData operation = tmplData (operationTypeImportStmt, operationTypeImportIdentifier) = getJsImportStmtAndIdentifier $ - makeJsImport operationImportPath (JsImportField $ toUpperFirst operationName) + makeJsImport (RelativeImportPath operationImportPath) (JsImportField $ toUpperFirst operationName) operationName = AS.Operation.getName operation diff --git a/waspc/src/Wasp/JsImport.hs b/waspc/src/Wasp/JsImport.hs index 8ef9da26b4..51e4eabef8 100644 --- a/waspc/src/Wasp/JsImport.hs +++ b/waspc/src/Wasp/JsImport.hs @@ -3,12 +3,13 @@ module Wasp.JsImport ( JsImport (..), JsImportName (..), + JsImportPath (..), JsImportAlias, - JsImportPath, JsImportIdentifier, JsImportStatement, makeJsImport, applyJsImportAlias, + getImportIdentifier, getJsImportStmtAndIdentifier, getJsImportStmtAndIdentifierRaw, ) @@ -33,7 +34,10 @@ data JsImport = JsImport } deriving (Show, Eq, Data) -type JsImportPath = Path Posix (Rel Dir') File' +data JsImportPath + = RelativeImportPath (Path Posix (Rel Dir') File') + | ModuleImportPath (Path Posix (Rel Dir') File') + deriving (Show, Eq, Data) -- Note (filip): not a fan of so many aliases for regular types type JsImportAlias = String @@ -54,6 +58,11 @@ type JsImportClause = String -- | Represents the full import statement e.g. @import { Name } from "file.js"@ type JsImportStatement = String +getImportIdentifier :: JsImport -> JsImportIdentifier +getImportIdentifier JsImport {_name = name} = case name of + JsImportModule identifier -> identifier + JsImportField identifier -> identifier + makeJsImport :: JsImportPath -> JsImportName -> JsImport makeJsImport importPath importName = JsImport importPath importName Nothing @@ -62,12 +71,14 @@ applyJsImportAlias importAlias jsImport = jsImport {_importAlias = importAlias} getJsImportStmtAndIdentifier :: JsImport -> (JsImportStatement, JsImportIdentifier) getJsImportStmtAndIdentifier (JsImport importPath importName maybeImportAlias) = - getJsImportStmtAndIdentifierRaw normalizedPath importName maybeImportAlias + getJsImportStmtAndIdentifierRaw filePath importName maybeImportAlias where - filePath = SP.fromRelFileP importPath - normalizedPath = if ".." `isPrefixOf` filePath then filePath else "./" ++ filePath + filePath = case importPath of + RelativeImportPath relPath -> normalizePath $ SP.fromRelFileP relPath + ModuleImportPath pathString -> SP.fromRelFileP pathString + normalizePath path = if ".." `isPrefixOf` path then path else "./" ++ path --- filip: attempt to simplify how we generate imports. I wanted to generate a +-- todo(filip): attempt to simplify how we generate imports. I wanted to generate a -- module import (e.g., '@ext-src/something') and couldn't do it. This is one of -- the funtions I implemented while I was trying to pull it off. getJsImportStmtAndIdentifierRaw :: diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index 6cd6512bde..c082573810 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -291,6 +291,8 @@ library Wasp.Generator.NpmDependencies Wasp.Generator.NpmInstall Wasp.Generator.SdkGenerator + Wasp.Generator.SdkGenerator.Common + Wasp.Generator.SdkGenerator.ServerOpsGenerator Wasp.Generator.ServerGenerator Wasp.Generator.ServerGenerator.JsImport Wasp.Generator.ServerGenerator.ApiRoutesG From a51406711b1e810580b34892d9d239d19563920c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Sun, 21 Jan 2024 13:49:12 +0100 Subject: [PATCH 50/54] Merge filip-base-sdk into filip-sdk --- CONTRIBUTING.md | 5 +- examples/todo-typescript/main.wasp | 2 +- .../migrations/migration_lock.toml | 3 - examples/waspello/README.md | 4 +- examples/waspleau/README.md | 2 +- waspc/.hlint.yaml | 1 + waspc/README.md | 8 +- waspc/cli/exe/Main.hs | 8 +- .../src/Wasp/Cli/Command/BashCompletion.hs | 22 +- waspc/cli/src/Wasp/Cli/Command/Build.hs | 10 +- waspc/cli/src/Wasp/Cli/Command/Call.hs | 1 - waspc/cli/src/Wasp/Cli/Command/Clean.hs | 11 +- waspc/cli/src/Wasp/Cli/Command/Common.hs | 31 +- waspc/cli/src/Wasp/Cli/Command/Compile.hs | 6 +- .../src/Wasp/Cli/Command/CreateNewProject.hs | 26 +- .../Wasp/Cli/Command/CreateNewProject/AI.hs | 2 +- .../Cli/Command/CreateNewProject/Common.hs | 19 - .../CreateNewProject/StarterTemplates.hs | 233 +- .../StarterTemplates/GhRepo.hs | 27 + .../StarterTemplates/Local.hs | 10 +- .../StarterTemplates/Remote.hs | 31 - .../StarterTemplates/Remote/Github.hs | 36 - .../StarterTemplates/Templating.hs | 21 +- waspc/cli/src/Wasp/Cli/Command/Db/Migrate.hs | 6 +- waspc/cli/src/Wasp/Cli/Command/Db/Reset.hs | 4 +- waspc/cli/src/Wasp/Cli/Command/Db/Seed.hs | 4 +- waspc/cli/src/Wasp/Cli/Command/Db/Studio.hs | 4 +- waspc/cli/src/Wasp/Cli/Command/Require.hs | 11 +- waspc/cli/src/Wasp/Cli/Command/Reset.hs | 24 - waspc/cli/src/Wasp/Cli/Command/Start.hs | 4 +- waspc/cli/src/Wasp/Cli/Command/Start/Db.hs | 2 +- waspc/cli/src/Wasp/Cli/Command/Studio.hs | 6 +- waspc/cli/src/Wasp/Cli/Command/Test.hs | 4 +- waspc/cli/src/Wasp/Cli/Common.hs | 37 +- waspc/data/Cli/templates/basic/package.json | 12 +- .../templates/react-app/src/actions/index.ts | 6 +- .../react-app/src/auth/email/actions/login.ts | 2 +- .../forms/internal/common/LoginSignupForm.tsx | 6 - .../src/auth/forms/internal/email/useEmail.ts | 9 +- .../react-app/src/auth/helpers/user.ts | 6 +- .../templates/react-app/src/auth/login.ts | 2 +- .../templates/react-app/src/auth/logout.ts | 18 +- .../src/auth/pages/OAuthCodeExchange.jsx | 16 +- .../templates/react-app/src/auth/types.ts | 2 +- .../templates/react-app/src/auth/user.ts | 4 +- .../src/webSocket/WebSocketProvider.tsx | 8 +- .../templates/react-app/vite.config.ts | 5 +- .../Generator/templates/sdk/api/events.ts | 6 +- .../data/Generator/templates/sdk/api/index.ts | 47 +- .../templates/sdk/auth/helpers/user.ts | 6 +- .../data/Generator/templates/sdk/auth/jwt.ts | 12 + .../Generator/templates/sdk/auth/login.ts | 2 +- .../Generator/templates/sdk/auth/logout.ts | 18 +- .../Generator/templates/sdk/auth/lucia.ts | 55 + .../Generator/templates/sdk/auth/password.ts | 15 + .../templates/sdk/auth/providers/types.ts | 14 +- .../Generator/templates/sdk/auth/session.ts | 107 + .../Generator/templates/sdk/auth/types.ts | 2 +- .../data/Generator/templates/sdk/auth/user.ts | 4 +- .../Generator/templates/sdk/auth/utils.ts | 46 +- .../data/Generator/templates/sdk/core/auth.js | 150 +- .../templates/sdk/server/_types/index.ts | 6 +- .../templates/server/src/auth/index.ts | 2 +- .../templates/server/src/auth/jwt.ts | 12 + .../templates/server/src/auth/lucia.ts | 56 + .../templates/server/src/auth/password.ts | 15 + .../src/auth/providers/config/_oauth.ts | 16 +- .../server/src/auth/providers/config/email.ts | 20 +- .../src/auth/providers/config/username.ts | 15 +- .../server/src/auth/providers/email/login.ts | 25 +- .../server/src/auth/providers/email/signup.ts | 43 +- .../server/src/auth/providers/email/utils.ts | 2 +- .../src/auth/providers/oauth/createRouter.ts | 45 +- .../server/src/auth/providers/oauth/init.ts | 10 +- .../server/src/auth/providers/oauth/types.ts | 5 - .../server/src/auth/providers/types.ts | 14 +- .../src/auth/providers/username/login.ts | 12 +- .../src/auth/providers/username/signup.ts | 60 +- .../templates/server/src/auth/session.ts | 108 + .../templates/server/src/auth/user.ts | 4 +- .../templates/server/src/auth/utils.ts | 54 +- .../templates/server/src/core/auth.js | 146 +- .../templates/server/src/crud/_operations.ts | 2 +- .../templates/server/src/email/core/index.ts | 3 + .../server/src/email/core/providers/dummy.ts | 31 +- .../templates/server/src/email/core/types.ts | 6 +- .../templates/server/src/email/index.ts | 15 +- .../templates/server/src/polyfill.ts | 7 + .../templates/server/src/routes/auth/index.js | 2 + .../server/src/routes/auth/logout.ts | 12 + .../templates/server/src/routes/auth/me.js | 3 +- .../Generator/templates/server/src/server.ts | 2 + .../templates/server/src/types/index.ts | 4 - .../Generator/templates/server/src/utils.ts | 3 +- .../server/src/webSocket/initialization.ts | 9 +- waspc/e2e-test/ShellCommands.hs | 1 - .../waspBuild-golden/files.manifest | 1 + .../waspBuild/.wasp/build/.waspchecksums | 17 +- .../.wasp/build/server/src/polyfill.ts | 7 + .../.wasp/build/server/src/server.ts | 2 + .../.wasp/build/server/src/types/index.ts | 1 - .../.wasp/build/web-app/src/actions/index.ts | 6 +- .../waspBuild/.wasp/build/web-app/src/api.ts | 47 +- .../.wasp/build/web-app/src/api/events.ts | 6 +- .../waspCompile-golden/files.manifest | 1 + .../waspCompile/.wasp/out/.waspchecksums | 17 +- .../.wasp/out/server/src/polyfill.ts | 7 + .../.wasp/out/server/src/server.ts | 2 + .../.wasp/out/server/src/types/index.ts | 1 - .../.wasp/out/web-app/src/actions/index.ts | 6 +- .../waspCompile/.wasp/out/web-app/src/api.ts | 47 +- .../.wasp/out/web-app/src/api/events.ts | 6 +- .../waspComplexTest-golden/files.manifest | 7 +- .../waspComplexTest/.wasp/out/.waspchecksums | 103 +- .../.wasp/out/db/schema.prisma | 9 + .../db/schema.prisma.wasp-generate-checksum | 2 +- .../installedFullStackNpmDependencies.json | 2 +- .../.wasp/out/server/package.json | 2 + .../.wasp/out/server/src/_types/index.ts | 6 +- .../.wasp/out/server/src/auth/jwt.ts | 12 + .../.wasp/out/server/src/auth/lucia.ts | 55 + .../.wasp/out/server/src/auth/password.ts | 15 + .../src/auth/providers/config/google.ts | 4 +- .../src/auth/providers/oauth/createRouter.ts | 45 +- .../server/src/auth/providers/oauth/init.ts | 10 +- .../server/src/auth/providers/oauth/types.ts | 5 - .../out/server/src/auth/providers/types.ts | 14 +- .../.wasp/out/server/src/auth/session.ts | 107 + .../.wasp/out/server/src/auth/user.ts | 4 +- .../.wasp/out/server/src/auth/utils.ts | 46 +- .../.wasp/out/server/src/core/auth.js | 146 +- .../.wasp/out/server/src/crud/tasks.ts | 2 +- .../server/src/email/core/providers/dummy.ts | 24 - .../.wasp/out/server/src/email/core/types.ts | 6 +- .../.wasp/out/server/src/email/index.ts | 10 +- .../.wasp/out/server/src/polyfill.ts | 7 + .../.wasp/out/server/src/routes/auth/index.js | 2 + .../out/server/src/routes/auth/logout.ts | 12 + .../.wasp/out/server/src/routes/auth/me.js | 2 +- .../.wasp/out/server/src/server.ts | 2 + .../.wasp/out/server/src/types/index.ts | 2 - .../.wasp/out/server/src/utils.ts | 3 +- .../.wasp/out/web-app/src/actions/index.ts | 6 +- .../.wasp/out/web-app/src/api.ts | 47 +- .../.wasp/out/web-app/src/api/events.ts | 6 +- .../out/web-app/src/auth/helpers/user.ts | 6 +- .../.wasp/out/web-app/src/auth/logout.ts | 18 +- .../src/auth/pages/OAuthCodeExchange.jsx | 16 +- .../.wasp/out/web-app/src/auth/types.ts | 2 +- .../.wasp/out/web-app/src/auth/user.ts | 4 +- .../waspJob-golden/files.manifest | 1 + .../waspJob/.wasp/out/.waspchecksums | 17 +- .../waspJob/.wasp/out/server/src/polyfill.ts | 7 + .../waspJob/.wasp/out/server/src/server.ts | 2 + .../.wasp/out/server/src/types/index.ts | 1 - .../.wasp/out/web-app/src/actions/index.ts | 6 +- .../waspJob/.wasp/out/web-app/src/api.ts | 47 +- .../.wasp/out/web-app/src/api/events.ts | 6 +- .../waspMigrate-golden/files.manifest | 1 + .../waspMigrate/.wasp/out/.waspchecksums | 17 +- .../.wasp/out/server/src/polyfill.ts | 7 + .../.wasp/out/server/src/server.ts | 2 + .../.wasp/out/server/src/types/index.ts | 1 - .../.wasp/out/web-app/src/actions/index.ts | 6 +- .../waspMigrate/.wasp/out/web-app/src/api.ts | 47 +- .../.wasp/out/web-app/src/api/events.ts | 6 +- waspc/examples/crud-testing/main.wasp | 7 +- .../20231106110220_initial/migration.sql | 24 - .../20231124155039_add_auth/migration.sql | 38 - .../20231212120120_inital/migration.sql | 24 - .../20231212132054_random/migration.sql | 24 - .../20231212132224_thrid/migration.sql | 13 - .../20240110121223_initial/migration.sql | 64 + .../examples/crud-testing/src/server/auth.ts | 5 +- .../crud-testing/src/server/auth_simple.js | 4 +- waspc/examples/pg-vector-example/main.wasp | 2 +- .../.wasp/out/sdk/wasp/api/events.ts | 6 +- .../.wasp/out/sdk/wasp/api/index.ts | 47 +- .../.wasp/out/sdk/wasp/auth/helpers/user.ts | 6 +- .../.wasp/out/sdk/wasp/auth/login.ts | 2 +- .../.wasp/out/sdk/wasp/auth/logout.ts | 18 +- .../out/sdk/wasp/auth/providers/types.ts | 14 +- .../.wasp/out/sdk/wasp/auth/types.ts | 2 +- .../.wasp/out/sdk/wasp/auth/user.ts | 4 +- .../.wasp/out/sdk/wasp/auth/utils.ts | 46 +- .../.wasp/out/sdk/wasp/core/auth.js | 150 +- .../.wasp/out/sdk/wasp/package.json | 4 +- .../.wasp/out/sdk/wasp/server/_types/index.ts | 6 +- .../.wasp/out/sdk/wasp/server/utils.ts | 3 +- waspc/examples/todo-typescript/cleanstart | 2 +- .../20240110163721_init/migration.sql | 34 - .../20240121113923_init}/migration.sql | 14 + .../todo-typescript/package-lock.json | 383 +-- .../20240110132515_add_session/migration.sql | 17 + .../examples/todoApp/src/server/auth/email.ts | 13 + .../todoApp/src/server/auth/github.js | 12 +- .../todoApp/src/server/auth/google.js | 5 +- .../todoApp/src/server/auth/signup.ts | 4 +- waspc/examples/todoApp/src/server/dbSeeds.ts | 2 +- waspc/examples/todoApp/todoApp.wasp | 13 +- .../20240115130723_add_session/migration.sql | 17 + .../examples/todoApp/sample.env.server | 3 +- .../examples/todoApp/todoApp.wasp | 3 +- waspc/headless-test/tests/simple.spec.ts | 8 +- waspc/src/Wasp/AI/CodeAgent.hs | 5 +- waspc/src/Wasp/AppSpec/App/Auth.hs | 42 +- waspc/src/Wasp/AppSpec/App/Dependency.hs | 2 + waspc/src/Wasp/AppSpec/App/EmailSender.hs | 2 +- waspc/src/Wasp/AppSpec/PackageJson.hs | 24 +- waspc/src/Wasp/AppSpec/Valid.hs | 19 +- waspc/src/Wasp/Generator/DbGenerator.hs | 2 - waspc/src/Wasp/Generator/DbGenerator/Auth.hs | 41 +- .../src/Wasp/Generator/DbGenerator/Common.hs | 5 - waspc/src/Wasp/Generator/DbGenerator/Jobs.hs | 23 +- .../Wasp/Generator/DbGenerator/Operations.hs | 1 - .../Wasp/Generator/Job/IO/PrefixedWriter.hs | 51 +- waspc/src/Wasp/Generator/NpmDependencies.hs | 9 +- waspc/src/Wasp/Generator/NpmInstall.hs | 76 +- waspc/src/Wasp/Generator/SdkGenerator.hs | 14 +- waspc/src/Wasp/Generator/ServerGenerator.hs | 3 +- .../ServerGenerator/Auth/EmailAuthG.hs | 6 +- .../ServerGenerator/Auth/LocalAuthG.hs | 37 +- .../ServerGenerator/Auth/OAuthAuthG.hs | 4 +- .../Wasp/Generator/ServerGenerator/AuthG.hs | 66 +- .../ServerGenerator/EmailSender/Providers.hs | 17 +- .../Generator/ServerGenerator/EmailSenderG.hs | 6 +- waspc/src/Wasp/Generator/WebAppGenerator.hs | 2 +- .../WebAppGenerator/Auth/AuthFormsG.hs | 3 +- .../Wasp/Generator/WebAppGenerator/AuthG.hs | 4 +- waspc/src/Wasp/Project/Analyze.hs | 8 +- waspc/src/Wasp/Project/Common.hs | 75 +- waspc/src/Wasp/Util/Terminal.hs | 16 + .../PageComponentFileTest.hs | 2 +- waspc/test/AnalyzerTest.hs | 22 +- waspc/test/AppSpec/ValidTest.hs | 118 +- waspc/waspc.cabal | 5 +- waspc/waspls/src/Wasp/LSP/Diagnostic.hs | 2 +- waspc/waspls/src/Wasp/LSP/ServerMonads.hs | 3 +- web/README.md | 59 +- web/blog/2023-03-02-wasp-beta-update-feb.md | 2 +- ...-voting-app-websockets-react-typescript.md | 4 +- ...1-21-guide-windows-development-wasp-wsl.md | 189 ++ web/blog/2023-12-05-writing-rfcs.md | 190 ++ web/blog/authors.yml | 11 + web/docs/OldDocsNote.tsx | 25 - web/docs/advanced/apis.md | 10 +- .../_addExternalAuthEnvVarsReminder.md | 3 +- web/docs/advanced/deployment/cli.md | 2 +- web/docs/advanced/deployment/manually.md | 8 +- web/docs/advanced/deployment/overview.md | 2 +- .../advanced/email/_dummy-provider-note.md | 4 + web/docs/advanced/email/email.md | 401 +++ web/docs/advanced/jobs.md | 6 +- web/docs/advanced/links.md | 2 +- web/docs/advanced/middleware-config.md | 2 +- web/docs/advanced/web-sockets.md | 2 +- web/docs/auth/_multiple-identities-warning.md | 6 + .../auth/_read-more-about-auth-entities.md | 1 + web/docs/auth/_user-fields.md | 8 + .../auth/_user-signup-fields-explainer.md | 40 + web/docs/auth/email.md | 370 ++- web/docs/auth/entities/_get-email.md | 48 + web/docs/auth/entities/_get-username.md | 48 + web/docs/auth/entities/entities.md | 450 ++++ web/docs/auth/overview.md | 324 ++- .../auth/social-auth/_api-reference-intro.md | 4 +- .../auth/social-auth/_default-behaviour.md | 7 - .../auth/social-auth/_getuserfields-type.md | 2 +- .../social-auth/_override-example-intro.md | 4 +- web/docs/auth/social-auth/_override-intro.md | 6 +- web/docs/auth/social-auth/_using-auth-note.md | 2 +- .../social-auth/_wasp-file-structure-note.md | 1 - web/docs/auth/social-auth/github.md | 165 +- web/docs/auth/social-auth/google.md | 171 +- web/docs/auth/social-auth/overview.md | 184 +- web/docs/auth/ui.md | 2 +- web/docs/auth/username-and-pass.md | 126 +- web/docs/contributing.md | 4 +- web/docs/data-model/backends.md | 6 +- web/docs/data-model/crud.md | 16 +- web/docs/data-model/entities.md | 4 +- web/docs/data-model/operations/actions.md | 10 +- web/docs/data-model/operations/overview.md | 4 +- web/docs/data-model/operations/queries.md | 10 +- web/docs/examples.md | 31 - web/docs/general/cli.md | 6 +- web/docs/introduction/editor-setup.md | 2 +- web/docs/introduction/introduction.md | 204 ++ .../{getting-started.md => quick-start.md} | 16 +- web/docs/language/features.md | 2372 ----------------- web/docs/project/client-config.md | 2 +- web/docs/project/css-frameworks.md | 16 +- web/docs/project/customizing-app.md | 21 +- web/docs/project/dependencies.md | 23 +- web/docs/project/env-vars.md | 2 +- web/docs/project/server-config.md | 4 +- web/docs/project/testing.md | 4 +- web/docs/tutorial/01-create.md | 2 +- web/docs/tutorial/02-project-structure.md | 2 +- web/docs/tutorial/03-pages.md | 2 +- web/docs/tutorial/04-entities.md | 2 +- web/docs/tutorial/05-queries.md | 6 +- web/docs/tutorial/07-auth.md | 50 +- web/docs/typescript.md | 521 ---- web/docusaurus.config.js | 59 +- web/sidebars.js | 7 +- web/src/components/Required.tsx | 22 - web/src/components/Tag.tsx | 37 + web/src/css/custom.css | 27 + .../img/auth-entities/model-example.png | Bin 0 -> 175631 bytes web/static/img/auth-entities/model.png | Bin 0 -> 101759 bytes web/static/img/wasp_db_demonstration.gif | Bin 153836 -> 290248 bytes web/static/img/wasp_user_in_db.gif | Bin 0 -> 607018 bytes .../img/writing-rfcs/existing-solutions.png | Bin 0 -> 141401 bytes web/static/img/writing-rfcs/rfc-flowchart.png | Bin 0 -> 239300 bytes web/static/img/writing-rfcs/rfc-meme-when.png | Bin 0 -> 722964 bytes web/static/img/writing-rfcs/rfc-metadata.png | Bin 0 -> 164701 bytes web/static/img/writing-rfcs/rfc-overview.png | Bin 0 -> 989915 bytes web/static/img/writing-rfcs/rfc-problem.png | Bin 0 -> 1659187 bytes web/static/img/writing-rfcs/rfc-prophet.png | Bin 0 -> 159576 bytes .../rfc-reviewer-status-example.png | Bin 0 -> 60595 bytes .../img/wsl-guide/wsl-guide-banner.jpeg | Bin 0 -> 472349 bytes .../_sendingEmailsInDevelopment.md | 7 + .../version-0.11.8/advanced/apis.md | 343 +++ .../deployment/DeploymentOptionsGrid.css | 29 + .../deployment/DeploymentOptionsGrid.tsx | 50 + .../_addExternalAuthEnvVarsReminder.md | 4 + .../deployment/_building-the-web-client.md | 13 + .../version-0.11.8/advanced/deployment/cli.md | 249 ++ .../advanced/deployment/manually.md | 553 ++++ .../advanced/deployment/overview.md | 44 + .../version-0.11.8}/advanced/email.md | 2 +- .../version-0.11.8/advanced/jobs.md | 426 +++ .../version-0.11.8/advanced/links.md | 134 + .../advanced/middleware-config.md | 280 ++ .../version-0.11.8/advanced/web-sockets.md | 335 +++ .../version-0.11.8/auth/Pills.css | 15 + .../version-0.11.8/auth/Pills.jsx | 48 + .../version-0.11.8/auth/email.md | 914 +++++++ .../version-0.11.8/auth/overview.md | 1306 +++++++++ .../auth/social-auth/SocialAuthGrid.css | 29 + .../auth/social-auth/SocialAuthGrid.tsx | 53 + .../auth/social-auth/_api-reference-intro.md | 10 + .../auth/social-auth/_default-behaviour.md | 10 + .../auth/social-auth/_getuserfields-type.md | 3 + .../social-auth/_override-example-intro.md | 10 + .../auth/social-auth/_override-intro.md | 10 + .../_username-generate-explanation.md | 0 .../auth/social-auth/_using-auth-note.md | 3 + .../social-auth/_wasp-file-structure-note.md | 16 + .../version-0.11.8/auth/social-auth/github.md | 606 +++++ .../version-0.11.8/auth/social-auth/google.md | 651 +++++ .../auth/social-auth/overview.md | 497 ++++ web/versioned_docs/version-0.11.8/auth/ui.md | 616 +++++ .../version-0.11.8/auth/username-and-pass.md | 652 +++++ web/versioned_docs/version-0.11.8/contact.md | 5 + .../version-0.11.8/contributing.md | 19 + .../version-0.11.8/data-model/backends.md | 452 ++++ .../version-0.11.8/data-model/crud.md | 749 ++++++ .../version-0.11.8/data-model/entities.md | 105 + .../data-model/operations/_superjson-note.md | 14 + .../data-model/operations/actions.md | 875 ++++++ .../data-model/operations/overview.md | 12 + .../data-model/operations/queries.md | 656 +++++ .../version-0.11.8/general/cli.md | 161 ++ .../version-0.11.8/general/language.md | 91 + .../introduction/editor-setup.md | 23 + .../introduction/introduction.md} | 10 +- .../introduction/quick-start.md | 143 + .../version-0.11.8/project/_baseDirEnvNote.md | 6 + .../version-0.11.8/project/client-config.md | 449 ++++ .../version-0.11.8/project/css-frameworks.md | 111 + .../project/custom-vite-config.md | 122 + .../version-0.11.8/project/customizing-app.md | 140 + .../version-0.11.8/project/dependencies.md | 32 + .../version-0.11.8/project/env-vars.md | 134 + .../version-0.11.8/project/server-config.md | 250 ++ .../project/starter-templates.md | 73 + .../version-0.11.8/project/static-assets.md | 68 + .../version-0.11.8/project/testing.md | 387 +++ .../version-0.11.8/telemetry.md | 59 + .../version-0.11.8/tutorial/01-create.md | 65 + .../tutorial/02-project-structure.md | 117 + .../version-0.11.8/tutorial/03-pages.md | 226 ++ .../version-0.11.8/tutorial/04-entities.md | 48 + .../version-0.11.8/tutorial/05-queries.md | 251 ++ .../version-0.11.8/tutorial/06-actions.md | 414 +++ .../version-0.11.8/tutorial/07-auth.md | 527 ++++ web/versioned_docs/version-0.11.8/vision.md | 30 + .../version-0.11.8/writingguide.md | 157 ++ .../version-0.11.8-sidebars.json | 137 + web/versions.json | 3 + 392 files changed, 20182 insertions(+), 6094 deletions(-) delete mode 100644 examples/todo-typescript/migrations/migration_lock.toml create mode 100644 waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/GhRepo.hs delete mode 100644 waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Remote.hs delete mode 100644 waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Remote/Github.hs delete mode 100644 waspc/cli/src/Wasp/Cli/Command/Reset.hs create mode 100644 waspc/data/Generator/templates/sdk/auth/jwt.ts create mode 100644 waspc/data/Generator/templates/sdk/auth/lucia.ts create mode 100644 waspc/data/Generator/templates/sdk/auth/password.ts create mode 100644 waspc/data/Generator/templates/sdk/auth/session.ts create mode 100644 waspc/data/Generator/templates/server/src/auth/jwt.ts create mode 100644 waspc/data/Generator/templates/server/src/auth/lucia.ts create mode 100644 waspc/data/Generator/templates/server/src/auth/password.ts create mode 100644 waspc/data/Generator/templates/server/src/auth/session.ts create mode 100644 waspc/data/Generator/templates/server/src/polyfill.ts create mode 100644 waspc/data/Generator/templates/server/src/routes/auth/logout.ts create mode 100644 waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/polyfill.ts create mode 100644 waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/polyfill.ts create mode 100644 waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/jwt.ts create mode 100644 waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/lucia.ts create mode 100644 waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/password.ts create mode 100644 waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/session.ts delete mode 100644 waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/email/core/providers/dummy.ts create mode 100644 waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/polyfill.ts create mode 100644 waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/routes/auth/logout.ts create mode 100644 waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/polyfill.ts create mode 100644 waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/polyfill.ts delete mode 100644 waspc/examples/crud-testing/migrations/20231106110220_initial/migration.sql delete mode 100644 waspc/examples/crud-testing/migrations/20231124155039_add_auth/migration.sql delete mode 100644 waspc/examples/crud-testing/migrations/20231212120120_inital/migration.sql delete mode 100644 waspc/examples/crud-testing/migrations/20231212132054_random/migration.sql delete mode 100644 waspc/examples/crud-testing/migrations/20231212132224_thrid/migration.sql create mode 100644 waspc/examples/crud-testing/migrations/20240110121223_initial/migration.sql delete mode 100644 waspc/examples/todo-typescript/migrations/20240110163721_init/migration.sql rename {examples/todo-typescript/migrations/20231214130914_new_auth => waspc/examples/todo-typescript/migrations/20240121113923_init}/migration.sql (72%) create mode 100644 waspc/examples/todoApp/migrations/20240110132515_add_session/migration.sql create mode 100644 waspc/headless-test/examples/todoApp/migrations/20240115130723_add_session/migration.sql create mode 100644 web/blog/2023-11-21-guide-windows-development-wasp-wsl.md create mode 100644 web/blog/2023-12-05-writing-rfcs.md delete mode 100644 web/docs/OldDocsNote.tsx create mode 100644 web/docs/advanced/email/_dummy-provider-note.md create mode 100644 web/docs/advanced/email/email.md create mode 100644 web/docs/auth/_multiple-identities-warning.md create mode 100644 web/docs/auth/_read-more-about-auth-entities.md create mode 100644 web/docs/auth/_user-fields.md create mode 100644 web/docs/auth/_user-signup-fields-explainer.md create mode 100644 web/docs/auth/entities/_get-email.md create mode 100644 web/docs/auth/entities/_get-username.md create mode 100644 web/docs/auth/entities/entities.md delete mode 100644 web/docs/examples.md create mode 100644 web/docs/introduction/introduction.md rename web/docs/introduction/{getting-started.md => quick-start.md} (75%) delete mode 100644 web/docs/language/features.md delete mode 100644 web/docs/typescript.md delete mode 100644 web/src/components/Required.tsx create mode 100644 web/src/components/Tag.tsx create mode 100644 web/static/img/auth-entities/model-example.png create mode 100644 web/static/img/auth-entities/model.png create mode 100644 web/static/img/wasp_user_in_db.gif create mode 100644 web/static/img/writing-rfcs/existing-solutions.png create mode 100644 web/static/img/writing-rfcs/rfc-flowchart.png create mode 100644 web/static/img/writing-rfcs/rfc-meme-when.png create mode 100644 web/static/img/writing-rfcs/rfc-metadata.png create mode 100644 web/static/img/writing-rfcs/rfc-overview.png create mode 100644 web/static/img/writing-rfcs/rfc-problem.png create mode 100644 web/static/img/writing-rfcs/rfc-prophet.png create mode 100644 web/static/img/writing-rfcs/rfc-reviewer-status-example.png create mode 100644 web/static/img/wsl-guide/wsl-guide-banner.jpeg create mode 100644 web/versioned_docs/version-0.11.8/_sendingEmailsInDevelopment.md create mode 100644 web/versioned_docs/version-0.11.8/advanced/apis.md create mode 100644 web/versioned_docs/version-0.11.8/advanced/deployment/DeploymentOptionsGrid.css create mode 100644 web/versioned_docs/version-0.11.8/advanced/deployment/DeploymentOptionsGrid.tsx create mode 100644 web/versioned_docs/version-0.11.8/advanced/deployment/_addExternalAuthEnvVarsReminder.md create mode 100644 web/versioned_docs/version-0.11.8/advanced/deployment/_building-the-web-client.md create mode 100644 web/versioned_docs/version-0.11.8/advanced/deployment/cli.md create mode 100644 web/versioned_docs/version-0.11.8/advanced/deployment/manually.md create mode 100644 web/versioned_docs/version-0.11.8/advanced/deployment/overview.md rename web/{docs => versioned_docs/version-0.11.8}/advanced/email.md (99%) create mode 100644 web/versioned_docs/version-0.11.8/advanced/jobs.md create mode 100644 web/versioned_docs/version-0.11.8/advanced/links.md create mode 100644 web/versioned_docs/version-0.11.8/advanced/middleware-config.md create mode 100644 web/versioned_docs/version-0.11.8/advanced/web-sockets.md create mode 100644 web/versioned_docs/version-0.11.8/auth/Pills.css create mode 100644 web/versioned_docs/version-0.11.8/auth/Pills.jsx create mode 100644 web/versioned_docs/version-0.11.8/auth/email.md create mode 100644 web/versioned_docs/version-0.11.8/auth/overview.md create mode 100644 web/versioned_docs/version-0.11.8/auth/social-auth/SocialAuthGrid.css create mode 100644 web/versioned_docs/version-0.11.8/auth/social-auth/SocialAuthGrid.tsx create mode 100644 web/versioned_docs/version-0.11.8/auth/social-auth/_api-reference-intro.md create mode 100644 web/versioned_docs/version-0.11.8/auth/social-auth/_default-behaviour.md create mode 100644 web/versioned_docs/version-0.11.8/auth/social-auth/_getuserfields-type.md create mode 100644 web/versioned_docs/version-0.11.8/auth/social-auth/_override-example-intro.md create mode 100644 web/versioned_docs/version-0.11.8/auth/social-auth/_override-intro.md rename web/{docs => versioned_docs/version-0.11.8}/auth/social-auth/_username-generate-explanation.md (100%) create mode 100644 web/versioned_docs/version-0.11.8/auth/social-auth/_using-auth-note.md create mode 100644 web/versioned_docs/version-0.11.8/auth/social-auth/_wasp-file-structure-note.md create mode 100644 web/versioned_docs/version-0.11.8/auth/social-auth/github.md create mode 100644 web/versioned_docs/version-0.11.8/auth/social-auth/google.md create mode 100644 web/versioned_docs/version-0.11.8/auth/social-auth/overview.md create mode 100644 web/versioned_docs/version-0.11.8/auth/ui.md create mode 100644 web/versioned_docs/version-0.11.8/auth/username-and-pass.md create mode 100644 web/versioned_docs/version-0.11.8/contact.md create mode 100644 web/versioned_docs/version-0.11.8/contributing.md create mode 100644 web/versioned_docs/version-0.11.8/data-model/backends.md create mode 100644 web/versioned_docs/version-0.11.8/data-model/crud.md create mode 100644 web/versioned_docs/version-0.11.8/data-model/entities.md create mode 100644 web/versioned_docs/version-0.11.8/data-model/operations/_superjson-note.md create mode 100644 web/versioned_docs/version-0.11.8/data-model/operations/actions.md create mode 100644 web/versioned_docs/version-0.11.8/data-model/operations/overview.md create mode 100644 web/versioned_docs/version-0.11.8/data-model/operations/queries.md create mode 100644 web/versioned_docs/version-0.11.8/general/cli.md create mode 100644 web/versioned_docs/version-0.11.8/general/language.md create mode 100644 web/versioned_docs/version-0.11.8/introduction/editor-setup.md rename web/{docs/introduction/what-is-wasp.md => versioned_docs/version-0.11.8/introduction/introduction.md} (97%) create mode 100644 web/versioned_docs/version-0.11.8/introduction/quick-start.md create mode 100644 web/versioned_docs/version-0.11.8/project/_baseDirEnvNote.md create mode 100644 web/versioned_docs/version-0.11.8/project/client-config.md create mode 100644 web/versioned_docs/version-0.11.8/project/css-frameworks.md create mode 100644 web/versioned_docs/version-0.11.8/project/custom-vite-config.md create mode 100644 web/versioned_docs/version-0.11.8/project/customizing-app.md create mode 100644 web/versioned_docs/version-0.11.8/project/dependencies.md create mode 100644 web/versioned_docs/version-0.11.8/project/env-vars.md create mode 100644 web/versioned_docs/version-0.11.8/project/server-config.md create mode 100644 web/versioned_docs/version-0.11.8/project/starter-templates.md create mode 100644 web/versioned_docs/version-0.11.8/project/static-assets.md create mode 100644 web/versioned_docs/version-0.11.8/project/testing.md create mode 100644 web/versioned_docs/version-0.11.8/telemetry.md create mode 100644 web/versioned_docs/version-0.11.8/tutorial/01-create.md create mode 100644 web/versioned_docs/version-0.11.8/tutorial/02-project-structure.md create mode 100644 web/versioned_docs/version-0.11.8/tutorial/03-pages.md create mode 100644 web/versioned_docs/version-0.11.8/tutorial/04-entities.md create mode 100644 web/versioned_docs/version-0.11.8/tutorial/05-queries.md create mode 100644 web/versioned_docs/version-0.11.8/tutorial/06-actions.md create mode 100644 web/versioned_docs/version-0.11.8/tutorial/07-auth.md create mode 100644 web/versioned_docs/version-0.11.8/vision.md create mode 100644 web/versioned_docs/version-0.11.8/writingguide.md create mode 100644 web/versioned_sidebars/version-0.11.8-sidebars.json create mode 100644 web/versions.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2a415179dd..7d3efa1ac5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,11 +41,14 @@ All that's required is to create an app. And make a tutorial or a blog post to h Or you can re-build your existing pet project with Wasp. That would be cool! -## Documentation +## Documentation & Blog It may sound like the simplest one, but it's super valuable! If you've found an issue, a broken link or if something was unclear on our [website](https://wasp-lang.dev/) - please, feel free to fix it :) +Please make sure to **base your feature branches and PRs on the `release` branch** instead of `main`, since that's the one that is deployed to the website. + [**Documentation issues for beginners can be found here.**](https://github.com/wasp-lang/wasp/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22+label%3Adocumentation) +If you'd like to write a blog post about Wasp, please contact us via [Discord](https://discord.gg/zKFDFrsHa9) to discuss the topic and the details. Happy hacking! \ No newline at end of file diff --git a/examples/todo-typescript/main.wasp b/examples/todo-typescript/main.wasp index cd84cad132..9d4d180e24 100644 --- a/examples/todo-typescript/main.wasp +++ b/examples/todo-typescript/main.wasp @@ -1,6 +1,6 @@ app TodoTypescript { wasp: { - version: "^0.11.0" + version: "^0.12.0" }, title: "ToDo TypeScript", diff --git a/examples/todo-typescript/migrations/migration_lock.toml b/examples/todo-typescript/migrations/migration_lock.toml deleted file mode 100644 index e5e5c4705a..0000000000 --- a/examples/todo-typescript/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "sqlite" \ No newline at end of file diff --git a/examples/waspello/README.md b/examples/waspello/README.md index 395cd3903e..5fb8c11ea6 100644 --- a/examples/waspello/README.md +++ b/examples/waspello/README.md @@ -10,9 +10,9 @@ The backend is hosted on Fly.io at https://waspello.fly.dev. # Development ### Database -Wasp needs the Postgres database running. Check out the docs for details on [how to setup PostgreSQL](https://wasp-lang.dev/docs/language/features#postgresql) +Wasp needs the Postgres database running. -You can use `wasp start db` to start a PostgreSQL locally using Docker. +Easiest way to do this is to use `wasp start db` to start a PostgreSQL locally using Docker. ### Env variables Copy `env.server` to `.env.server` and fill in the values. diff --git a/examples/waspleau/README.md b/examples/waspleau/README.md index cd7cd2b0a6..8e9779267f 100644 --- a/examples/waspleau/README.md +++ b/examples/waspleau/README.md @@ -1,7 +1,7 @@ # Waspleau Welcome to the Waspleau example! This is a small Wasp project that tracks status of wasp-lang/wasp repo via a nice looking dashboard. -It pulls in data via [Jobs](https://wasp-lang.dev/docs/language/features#jobs) and stores them in the database. +It pulls in data via [Jobs](https://wasp-lang.dev/docs/advanced/jobs) and stores them in the database. This example project can serve as a good starting point for building your own dashboard with Wasp, that regularly pulls in external data by using Jobs Wasp feature. diff --git a/waspc/.hlint.yaml b/waspc/.hlint.yaml index 1b21470602..66a623d845 100644 --- a/waspc/.hlint.yaml +++ b/waspc/.hlint.yaml @@ -12,3 +12,4 @@ - ignore: {name: Use $>} # I find it makes code harder to read if enforced. - ignore: {name: Use list comprehension} # We can decide this on our own. - ignore: {name: Use ++} # I sometimes prefer concat over ++ due to the nicer formatting / extensibility. +- ignore: {name: Redundant lambda} # Sometimes it is nicer to create explicit lambda then function. diff --git a/waspc/README.md b/waspc/README.md index d4fd2cf423..b4baf4cde0 100644 --- a/waspc/README.md +++ b/waspc/README.md @@ -362,11 +362,17 @@ NOTE: If building of your commit is suddenly taking much longer time, it might b If it happens just once every so it is probably nothing to worry about. If it happens consistently, we should look into it. ### Typical Release Process -- Ensure that all starter templates in `starter` repo are working with the version of Wasp we are about to release and upgrade their version of Wasp to the new one. +- Starter templates + - Context: they are used by used by `wasp new`, you can find reference to them in `Wasp.Cli. ... .StarterTemplates`. + - In `StarterTemplates.hs` file, update git tag to new version of Wasp we are about to release (e.g. `wasp-v0.13.1-template`). + - Ensure that all starter templates are working with this new version of Wasp. + Update Wasp version in their main.wasp files. Finally, in their repos (for those templates that are on Github), + create new git tag that is the same as the new one in `StarterTemplates.hs` (e.g. `wasp-v0.13.1-template`). - ChangeLog.md and version in waspc.cabal should already be up to date, but double check that they are correct and update them if needed. Also consider enriching and polishing ChangeLog.md a bit even if all the data is already there. Also check that ChangeLog has correction version of wasp specified. - If you modified ChangeLog.md or waspc.cabal, create a PR, wait for approval and all the checks (CI) to pass, then squash and merge mentioned PR into `main`. - Update your local repository state to have all remote changes (`git fetch`). - Update `main` to contain changes from `release` by running `git merge release` while on the `main` branch. Resolve any conflicts. +- Take a versioned "snapshot" of the current docs by running `npm run docusaurus docs:version {version}` in the [web](/web) dir. Check the README in the `web` dir for more details. Commit this change to `main`. - Fast-forward `release` to this new, updated `main` by running `git merge main` while on the `release` branch. - Make sure you are on `release` and then run `./new-release 0.x.y.z`. - This will do some checks, tag it with new release version, and push it. diff --git a/waspc/cli/exe/Main.hs b/waspc/cli/exe/Main.hs index 1a54fa106c..24eddb03a4 100644 --- a/waspc/cli/exe/Main.hs +++ b/waspc/cli/exe/Main.hs @@ -19,14 +19,12 @@ import Wasp.Cli.Command.CreateNewProject (createNewProject) import qualified Wasp.Cli.Command.CreateNewProject.AI as Command.CreateNewProject.AI import Wasp.Cli.Command.Db (runDbCommand) import qualified Wasp.Cli.Command.Db.Migrate as Command.Db.Migrate -import qualified Wasp.Cli.Command.Db.Reset as Command.Db.Reset import qualified Wasp.Cli.Command.Db.Seed as Command.Db.Seed import qualified Wasp.Cli.Command.Db.Studio as Command.Db.Studio import Wasp.Cli.Command.Deploy (deploy) import Wasp.Cli.Command.Deps (deps) import Wasp.Cli.Command.Dockerfile (printDockerfile) import Wasp.Cli.Command.Info (info) -import Wasp.Cli.Command.Reset (reset) import Wasp.Cli.Command.Start (start) import qualified Wasp.Cli.Command.Start.Db as Command.Start.Db import Wasp.Cli.Command.Studio (studio) @@ -50,7 +48,6 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do ("new:ai" : newAiArgs) -> Command.Call.NewAi newAiArgs ["start"] -> Command.Call.Start ["start", "db"] -> Command.Call.StartDb - ["reset"] -> Command.Call.Reset ["clean"] -> Command.Call.Clean ["compile"] -> Command.Call.Compile ("db" : dbArgs) -> Command.Call.Db dbArgs @@ -101,7 +98,6 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do Command.Call.Start -> runCommand start Command.Call.StartDb -> runCommand Command.Start.Db.start Command.Call.Clean -> runCommand clean - Command.Call.Reset -> runCommand reset Command.Call.Compile -> runCommand compile Command.Call.Db dbArgs -> dbCli dbArgs Command.Call.Version -> printVersion @@ -161,8 +157,7 @@ printUsage = cmd " start Runs Wasp app in development mode, watching for file changes.", cmd " start db Starts managed development database for you.", cmd " db [args] Executes a database command. Run 'wasp db' for more info.", - cmd " clean Deletes all generated code and other cached artifacts.", - cmd " reset Deletes all generated code, all cached artifacts, and the node_modules directory.", + cmd " clean Deletes all generated code, all cached artifacts, and the node_modules dir.", " Wasp equivalent of 'have you tried closing and opening it again?'.", cmd " build Generates full web app code, ready for deployment. Use when deploying or ejecting.", cmd " deploy Deploys your Wasp app to cloud hosting providers.", @@ -204,7 +199,6 @@ dbCli :: [String] -> IO () dbCli args = case args of ["start"] -> runCommand Command.Start.Db.start "migrate-dev" : optionalMigrateArgs -> runDbCommand $ Command.Db.Migrate.migrateDev optionalMigrateArgs - ["reset"] -> runDbCommand Command.Db.Reset.reset ["seed"] -> runDbCommand $ Command.Db.Seed.seed Nothing ["seed", seedName] -> runDbCommand $ Command.Db.Seed.seed $ Just seedName ["studio"] -> runDbCommand Command.Db.Studio.studio diff --git a/waspc/cli/src/Wasp/Cli/Command/BashCompletion.hs b/waspc/cli/src/Wasp/Cli/Command/BashCompletion.hs index 3f297c99ac..0a4e24f085 100644 --- a/waspc/cli/src/Wasp/Cli/Command/BashCompletion.hs +++ b/waspc/cli/src/Wasp/Cli/Command/BashCompletion.hs @@ -26,8 +26,26 @@ bashCompletion = do ["db", cmdPrefix] -> listMatchingCommands cmdPrefix dbSubCommands _ -> liftIO . putStrLn $ "" where - -- todo(filip): remove duplication - commands = ["new", "version", "waspls", "start", "db", "clean", "reset", "uninstall", "build", "telemetry", "deps", "info", "completion", "completion:generate"] + commands = + [ "new", + "new:ai", + "version", + "waspls", + "completion", + "completion:generate", + "uninstall", + "start", + "db", + "clean", + "build", + "deploy", + "telemetry", + "deps", + "dockerfile", + "info", + "test", + "studio" + ] dbSubCommands = ["migrate-dev", "studio"] listMatchingCommands :: String -> [String] -> Command () listMatchingCommands cmdPrefix cmdList = listCommands $ filter (cmdPrefix `isPrefixOf`) cmdList diff --git a/waspc/cli/src/Wasp/Cli/Command/Build.hs b/waspc/cli/src/Wasp/Cli/Command/Build.hs index cd049fa91f..591fc1256e 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Build.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Build.hs @@ -16,13 +16,13 @@ import Wasp.Cli.Command (Command, CommandError (..)) import Wasp.Cli.Command.Compile (compileIOWithOptions, printCompilationResult) import Wasp.Cli.Command.Message (cliSendMessageC) import Wasp.Cli.Command.Require (InWaspProject (InWaspProject), require) -import qualified Wasp.Cli.Common as Common import Wasp.Cli.Message (cliSendMessage) import Wasp.CompileOptions (CompileOptions (..)) import qualified Wasp.Generator import Wasp.Generator.Monad (GeneratorWarning (GeneratorNeedsMigrationWarning)) import qualified Wasp.Message as Msg -import Wasp.Project (CompileError, CompileWarning) +import Wasp.Project (CompileError, CompileWarning, WaspProjectDir) +import Wasp.Project.Common (buildDirInDotWaspDir, dotWaspDirInWaspProjectDir) -- | Builds Wasp project that the current working directory is part of. -- Does all the steps, from analysis to generation, and at the end writes generated code @@ -35,8 +35,8 @@ build :: Command () build = do InWaspProject waspProjectDir <- require let buildDir = - waspProjectDir Common.dotWaspDirInWaspProjectDir - Common.buildDirInDotWaspDir + waspProjectDir dotWaspDirInWaspProjectDir + buildDirInDotWaspDir buildDirFilePath = SP.fromAbsDir buildDir doesBuildDirExist <- liftIO $ doesDirectoryExist buildDirFilePath @@ -58,7 +58,7 @@ build = do CommandError "Building of wasp project failed" $ show (length errors) ++ " errors found" buildIO :: - Path' Abs (Dir Common.WaspProjectDir) -> + Path' Abs (Dir WaspProjectDir) -> Path' Abs (Dir Wasp.Generator.ProjectRootDir) -> IO ([CompileWarning], [CompileError]) buildIO waspProjectDir buildDir = compileIOWithOptions options waspProjectDir buildDir diff --git a/waspc/cli/src/Wasp/Cli/Command/Call.hs b/waspc/cli/src/Wasp/Cli/Command/Call.hs index 7a434c68a5..beb665a5b6 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Call.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Call.hs @@ -6,7 +6,6 @@ data Call | Start | StartDb | Clean - | Reset | Uninstall | Compile | Db Arguments -- db args diff --git a/waspc/cli/src/Wasp/Cli/Command/Clean.hs b/waspc/cli/src/Wasp/Cli/Command/Clean.hs index 74fd92a369..e5dccab574 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Clean.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Clean.hs @@ -3,11 +3,18 @@ module Wasp.Cli.Command.Clean ) where +import qualified StrongPath as SP import Wasp.Cli.Command (Command) -import Wasp.Cli.Command.Common (deleteDotWaspDirIfExists) +import Wasp.Cli.Command.Common (deleteDirectoryIfExistsVerbosely) import Wasp.Cli.Command.Require (InWaspProject (InWaspProject), require) +import Wasp.Project.Common (dotWaspDirInWaspProjectDir, nodeModulesDirInWaspProjectDir) clean :: Command () clean = do InWaspProject waspProjectDir <- require - deleteDotWaspDirIfExists waspProjectDir + + let dotWaspDir = waspProjectDir SP. dotWaspDirInWaspProjectDir + let nodeModulesDir = waspProjectDir SP. nodeModulesDirInWaspProjectDir + + deleteDirectoryIfExistsVerbosely dotWaspDir + deleteDirectoryIfExistsVerbosely nodeModulesDir diff --git a/waspc/cli/src/Wasp/Cli/Command/Common.hs b/waspc/cli/src/Wasp/Cli/Command/Common.hs index bcee71ee83..df5c396114 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Common.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Common.hs @@ -1,8 +1,7 @@ module Wasp.Cli.Command.Common ( readWaspCompileInfo, throwIfExeIsNotAvailable, - deleteDotWaspDirIfExists, - deleteDirectoryIfExists, + deleteDirectoryIfExistsVerbosely, ) where @@ -16,9 +15,9 @@ import System.Directory ) import Wasp.Cli.Command (Command, CommandError (..)) import Wasp.Cli.Command.Message (cliSendMessageC) -import Wasp.Cli.Common (WaspProjectDir) -import qualified Wasp.Cli.Common as Cli.Common import qualified Wasp.Message as Msg +import Wasp.Project (WaspProjectDir) +import qualified Wasp.Project.Common as Project.Common import Wasp.Util (ifM) import qualified Wasp.Util.IO as IOUtil @@ -30,9 +29,9 @@ readWaspCompileInfo waspDir = (return "No compile information found") where dotWaspInfoFile = - waspDir Cli.Common.dotWaspDirInWaspProjectDir - Cli.Common.generatedCodeDirInDotWaspDir - Cli.Common.dotWaspInfoFileInGeneratedCodeDir + waspDir Project.Common.dotWaspDirInWaspProjectDir + Project.Common.generatedCodeDirInDotWaspDir + Project.Common.dotWaspInfoFileInGeneratedCodeDir throwIfExeIsNotAvailable :: String -> String -> Command () throwIfExeIsNotAvailable exeName explanationMsg = do @@ -42,21 +41,15 @@ throwIfExeIsNotAvailable exeName explanationMsg = do E.throwError $ CommandError ("Couldn't find `" <> exeName <> "` executable") explanationMsg -deleteDirectoryIfExists :: Path' Abs (Dir d) -> Command () -deleteDirectoryIfExists dir = do +deleteDirectoryIfExistsVerbosely :: Path' Abs (Dir d) -> Command () +deleteDirectoryIfExistsVerbosely dir = do cliSendMessageC $ Msg.Start $ "Deleting the " ++ dirName ++ " directory..." dirExist <- liftIO $ IOUtil.doesDirectoryExist dir if dirExist - then deleteDir - else -- todo(filip): do we need to report this? If not, we can simply call the function from IOUtil. - cliSendMessageC $ Msg.Success $ "Nothing to delete: The " ++ dirName ++ " directory does not exist." - where - dirName = SP.toFilePath $ basename dir - deleteDir = do + then do liftIO $ IOUtil.removeDirectory dir cliSendMessageC $ Msg.Success $ "Deleted the " ++ dirName ++ " directory." - -deleteDotWaspDirIfExists :: Path' Abs (Dir WaspProjectDir) -> Command () -deleteDotWaspDirIfExists waspProjectDir = deleteDirectoryIfExists dotWaspDir + else do + cliSendMessageC $ Msg.Success $ "Nothing to delete: The " ++ dirName ++ " directory does not exist." where - dotWaspDir = waspProjectDir SP. Cli.Common.dotWaspDirInWaspProjectDir + dirName = SP.toFilePath $ basename dir diff --git a/waspc/cli/src/Wasp/Cli/Command/Compile.hs b/waspc/cli/src/Wasp/Cli/Command/Compile.hs index 4f92593faf..04346fa930 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Compile.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Compile.hs @@ -20,13 +20,13 @@ import qualified Wasp.AppSpec as AS import Wasp.Cli.Command (Command, CommandError (..)) import Wasp.Cli.Command.Message (cliSendMessageC) import Wasp.Cli.Command.Require (InWaspProject (InWaspProject), require) -import qualified Wasp.Cli.Common as Common import Wasp.Cli.Message (cliSendMessage) import Wasp.CompileOptions (CompileOptions (..)) import qualified Wasp.Generator import qualified Wasp.Message as Msg import Wasp.Project (CompileError, CompileWarning, WaspProjectDir) import qualified Wasp.Project +import Wasp.Project.Common (dotWaspDirInWaspProjectDir, extClientCodeDirInWaspProjectDir, extServerCodeDirInWaspProjectDir, extSharedCodeDirInWaspProjectDir, generatedCodeDirInDotWaspDir) -- | Same like 'compileWithOptions', but with default compile options. compile :: Command [CompileWarning] @@ -47,8 +47,8 @@ compileWithOptions :: CompileOptions -> Command [CompileWarning] compileWithOptions options = do InWaspProject waspProjectDir <- require let outDir = - waspProjectDir Common.dotWaspDirInWaspProjectDir - Common.generatedCodeDirInDotWaspDir + waspProjectDir dotWaspDirInWaspProjectDir + generatedCodeDirInDotWaspDir cliSendMessageC $ Msg.Start "Compiling wasp project..." (warnings, errors) <- liftIO $ compileIOWithOptions options waspProjectDir outDir diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs index 920eb222c5..6dd78fd804 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs @@ -5,11 +5,12 @@ where import Control.Monad.IO.Class (liftIO) import Data.Function ((&)) +import qualified StrongPath as SP import Wasp.Cli.Command (Command) import Wasp.Cli.Command.Call (Arguments) import qualified Wasp.Cli.Command.CreateNewProject.AI as AI import Wasp.Cli.Command.CreateNewProject.ArgumentsParser (parseNewProjectArgs) -import Wasp.Cli.Command.CreateNewProject.Common (printGettingStartedInstructions, throwProjectCreationError) +import qualified Wasp.Cli.Command.CreateNewProject.Common as Common import Wasp.Cli.Command.CreateNewProject.ProjectDescription ( NewProjectDescription (..), obtainNewProjectDescription, @@ -18,22 +19,24 @@ import Wasp.Cli.Command.CreateNewProject.StarterTemplates ( DirBasedTemplateMetadata (_path), StarterTemplate (..), getStarterTemplates, + getTemplateStartingInstructions, ) +import Wasp.Cli.Command.CreateNewProject.StarterTemplates.GhRepo (createProjectOnDiskFromGhRepoTemplate) import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Local (createProjectOnDiskFromLocalTemplate) -import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Remote (createProjectOnDiskFromRemoteTemplate) import Wasp.Cli.Command.Message (cliSendMessageC) import qualified Wasp.Message as Msg +import qualified Wasp.Util.Terminal as Term -- | It receives all of the arguments that were passed to the `wasp new` command. createNewProject :: Arguments -> Command () createNewProject args = do - newProjectArgs <- parseNewProjectArgs args & either throwProjectCreationError return - starterTemplates <- liftIO getStarterTemplates + newProjectArgs <- parseNewProjectArgs args & either Common.throwProjectCreationError return + let starterTemplates = getStarterTemplates newProjectDescription <- obtainNewProjectDescription newProjectArgs starterTemplates createProjectOnDisk newProjectDescription - liftIO $ printGettingStartedInstructions $ _absWaspProjectDir newProjectDescription + liftIO $ printGettingStartedInstructionsForProject newProjectDescription createProjectOnDisk :: NewProjectDescription -> Command () createProjectOnDisk @@ -45,9 +48,18 @@ createProjectOnDisk } = do cliSendMessageC $ Msg.Start $ "Creating your project from the \"" ++ show template ++ "\" template..." case template of - RemoteStarterTemplate metadata -> - createProjectOnDiskFromRemoteTemplate absWaspProjectDir projectName appName $ _path metadata + GhRepoStarterTemplate ghRepoRef metadata -> + createProjectOnDiskFromGhRepoTemplate absWaspProjectDir projectName appName ghRepoRef $ _path metadata LocalStarterTemplate metadata -> liftIO $ createProjectOnDiskFromLocalTemplate absWaspProjectDir projectName appName $ _path metadata AiGeneratedStarterTemplate -> AI.createNewProjectInteractiveOnDisk absWaspProjectDir appName + +-- | This function assumes that the project dir was created inside the current working directory. +printGettingStartedInstructionsForProject :: NewProjectDescription -> IO () +printGettingStartedInstructionsForProject projectDescription = do + let projectDirName = init . SP.toFilePath . SP.basename $ _absWaspProjectDir projectDescription + let instructions = getTemplateStartingInstructions projectDirName $ _template projectDescription + putStrLn $ Term.applyStyles [Term.Green] $ "Created new Wasp app in ./" ++ projectDirName ++ " directory!" + putStrLn "" + putStrLn instructions diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/AI.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/AI.hs index f4527ab775..aec5371649 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/AI.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/AI.hs @@ -39,8 +39,8 @@ import Wasp.Cli.Command.CreateNewProject.ProjectDescription parseWaspProjectNameIntoAppName, ) import Wasp.Cli.Command.CreateNewProject.StarterTemplates (readWaspProjectSkeletonFiles) -import Wasp.Cli.Common (WaspProjectDir) import qualified Wasp.Cli.Interactive as Interactive +import Wasp.Project.Common (WaspProjectDir) import qualified Wasp.Util as U import qualified Wasp.Util.Aeson as Utils.Aeson import qualified Wasp.Util.Terminal as T diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/Common.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/Common.hs index acb0657436..5446040773 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/Common.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/Common.hs @@ -2,17 +2,12 @@ module Wasp.Cli.Command.CreateNewProject.Common ( throwProjectCreationError, throwInvalidTemplateNameUsedError, defaultWaspVersionBounds, - printGettingStartedInstructions, ) where import Control.Monad.Except (throwError) -import StrongPath (Abs, Dir, Path') -import qualified StrongPath as SP import Wasp.Cli.Command (Command, CommandError (..)) -import Wasp.Cli.Common (WaspProjectDir) import qualified Wasp.SemanticVersion as SV -import qualified Wasp.Util.Terminal as Term import qualified Wasp.Version as WV throwProjectCreationError :: String -> Command a @@ -26,17 +21,3 @@ throwInvalidTemplateNameUsedError = defaultWaspVersionBounds :: String defaultWaspVersionBounds = show (SV.backwardsCompatibleWith WV.waspVersion) - --- | This function assumes that the project dir is created inside the current working directory --- when it prints the instructions. -printGettingStartedInstructions :: Path' Abs (Dir WaspProjectDir) -> IO () -printGettingStartedInstructions absProjectDir = do - let projectFolder = init . SP.toFilePath . SP.basename $ absProjectDir -{- ORMOLU_DISABLE -} - putStrLn $ Term.applyStyles [Term.Green] $ "Created new Wasp app in ./" ++ projectFolder ++ " directory!" - putStrLn "To run it, do:" - putStrLn "" - putStrLn $ Term.applyStyles [Term.Bold] $ " cd " ++ projectFolder - putStrLn $ Term.applyStyles [Term.Bold] " wasp start" - putStrLn "" -{- ORMOLU_ENABLE -} diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates.hs index 2bfb45d4ba..b99046be55 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates.hs @@ -7,78 +7,227 @@ module Wasp.Cli.Command.CreateNewProject.StarterTemplates findTemplateByString, defaultStarterTemplate, readWaspProjectSkeletonFiles, + getTemplateStartingInstructions, ) where -import Data.Either (fromRight) import Data.Foldable (find) import Data.Text (Text) -import StrongPath (File', Path, Rel, System, reldir, ()) -import qualified Wasp.Cli.Command.CreateNewProject.StarterTemplates.Remote.Github as Github -import Wasp.Cli.Common (WaspProjectDir) +import StrongPath (Dir', File', Path, Path', Rel, Rel', System, reldir, ()) +import qualified System.FilePath as FP +import qualified Wasp.Cli.GithubRepo as GhRepo import qualified Wasp.Cli.Interactive as Interactive import qualified Wasp.Data as Data +import Wasp.Project.Common (WaspProjectDir) import Wasp.Util.IO (listDirectoryDeep, readFileStrict) +import qualified Wasp.Util.Terminal as Term data StarterTemplate - = RemoteStarterTemplate DirBasedTemplateMetadata - | LocalStarterTemplate DirBasedTemplateMetadata - | AiGeneratedStarterTemplate - deriving (Eq) + = -- | Template from a Github repo. + GhRepoStarterTemplate !GhRepo.GithubRepoRef !DirBasedTemplateMetadata + | -- | Template from a disk, that comes bundled with wasp CLI. + LocalStarterTemplate !DirBasedTemplateMetadata + | -- | Template that will be dynamically generated by Wasp AI based on user's input. + AiGeneratedStarterTemplate data DirBasedTemplateMetadata = DirBasedTemplateMetadata - { _name :: String, - _path :: String, -- Path to a directory containing template files. - _description :: String + { _name :: !String, + _path :: !(Path' Rel' Dir'), -- Path to a directory containing template files. + _description :: !String, + _buildStartingInstructions :: !StartingInstructionsBuilder } - deriving (Eq, Show) instance Show StarterTemplate where - show (RemoteStarterTemplate metadata) = _name metadata + show (GhRepoStarterTemplate _ metadata) = _name metadata show (LocalStarterTemplate metadata) = _name metadata show AiGeneratedStarterTemplate = "ai-generated" instance Interactive.IsOption StarterTemplate where showOption = show - showOptionDescription (RemoteStarterTemplate metadata) = Just $ _description metadata + + showOptionDescription (GhRepoStarterTemplate _ metadata) = Just $ _description metadata showOptionDescription (LocalStarterTemplate metadata) = Just $ _description metadata showOptionDescription AiGeneratedStarterTemplate = Just "🤖 Describe an app in a couple of sentences and have Wasp AI generate initial code for you. (experimental)" -getStarterTemplates :: IO [StarterTemplate] -getStarterTemplates = do - remoteTemplates <- fromRight [] <$> fetchRemoteStarterTemplates - return $ localTemplates ++ remoteTemplates ++ [AiGeneratedStarterTemplate] - -fetchRemoteStarterTemplates :: IO (Either String [StarterTemplate]) -fetchRemoteStarterTemplates = do - fmap extractTemplateNames <$> Github.fetchRemoteTemplatesGithubData - where - extractTemplateNames :: [Github.RemoteTemplateGithubData] -> [StarterTemplate] - -- Each folder in the repo is a template. - extractTemplateNames = - map - ( \metadata -> - RemoteStarterTemplate $ - DirBasedTemplateMetadata - { _name = Github._name metadata, - _path = Github._path metadata, - _description = Github._description metadata - } - ) - -localTemplates :: [StarterTemplate] -localTemplates = [defaultStarterTemplate] +type StartingInstructionsBuilder = String -> String + +{- HLINT ignore getTemplateStartingInstructions "Redundant $" -} + +-- | Returns instructions for running the newly created (from the template) Wasp project. +-- Instructions assume that user is positioned right next to the just created project directory, +-- whose name is provided via projectDirName. +getTemplateStartingInstructions :: String -> StarterTemplate -> String +getTemplateStartingInstructions projectDirName = \case + GhRepoStarterTemplate _ metadata -> _buildStartingInstructions metadata projectDirName + LocalStarterTemplate metadata -> _buildStartingInstructions metadata projectDirName + AiGeneratedStarterTemplate -> + unlines + [ styleText $ "To run your new app, do:", + styleCode $ " cd " <> projectDirName, + styleCode $ " wasp db migrate-dev", + styleCode $ " wasp start" + ] + +getStarterTemplates :: [StarterTemplate] +getStarterTemplates = + [ defaultStarterTemplate, + todoTsStarterTemplate, + openSaasStarterTemplate, + embeddingsStarterTemplate, + AiGeneratedStarterTemplate + ] defaultStarterTemplate :: StarterTemplate -defaultStarterTemplate = +defaultStarterTemplate = basicStarterTemplate + +{- HLINT ignore basicStarterTemplate "Redundant $" -} + +basicStarterTemplate :: StarterTemplate +basicStarterTemplate = LocalStarterTemplate $ DirBasedTemplateMetadata - { _name = "basic", - _path = "basic", - _description = "Simple starter template with a single page." + { _path = [reldir|basic|], + _name = "basic", + _description = "Simple starter template with a single page.", + _buildStartingInstructions = \projectDirName -> + unlines + [ styleText $ "To run your new app, do:", + styleCode $ " cd " <> projectDirName, + styleCode $ " wasp db start" + ] } +{- HLINT ignore openSaasStarterTemplate "Redundant $" -} + +openSaasStarterTemplate :: StarterTemplate +openSaasStarterTemplate = + simpleGhRepoTemplate + ("open-saas", [reldir|.|]) + ( "saas", + "Everything a SaaS needs! Comes with Auth, ChatGPT API, Tailwind, Stripe payments and more." + <> " Check out https://opensaas.sh/ for more details." + ) + ( \projectDirName -> + unlines + [ styleText $ "To run your new app, follow the instructions below:", + styleText $ "", + styleText $ " 1. Position into app's root directory:", + styleCode $ " cd " <> projectDirName FP. "app", + styleText $ "", + styleText $ " 2. Run the development database (and leave it running):", + styleCode $ " wasp db start", + styleText $ "", + styleText $ " 3. Open new terminal window (or tab) in that same dir and continue in it.", + styleText $ "", + styleText $ " 4. Apply initial database migrations:", + styleCode $ " wasp db migrate-dev", + styleText $ "", + styleText $ " 5. Create initial dot env file from the template:", + styleCode $ " cp .env.server.example .env.server", + styleText $ "", + styleText $ " 6. Last step: run the app!", + styleCode $ " wasp start", + styleText $ "", + styleText $ "Check the README for additional guidance and the link to docs!" + ] + ) + +{- HLINT ignore todoTsStarterTemplate "Redundant $" -} + +todoTsStarterTemplate :: StarterTemplate +todoTsStarterTemplate = + simpleGhRepoTemplate + ("starters", [reldir|todo-ts|]) + ( "todo-ts", + "Simple but well-rounded Wasp app implemented with Typescript & full-stack type safety." + ) + ( \projectDirName -> + unlines + [ styleText $ "To run your new app, do:", + styleCode $ " cd " ++ projectDirName, + styleCode $ " wasp db migrate-dev", + styleCode $ " wasp start", + styleText $ "", + styleText $ "Check the README for additional guidance!" + ] + ) + +{- Functions for styling instructions. Their names are on purpose of same length, for nicer code formatting. -} + +styleCode :: String -> String +styleCode = Term.applyStyles [Term.Bold] + +styleText :: String -> String +styleText = id + +{- -} + +{- HLINT ignore embeddingsStarterTemplate "Redundant $" -} + +embeddingsStarterTemplate :: StarterTemplate +embeddingsStarterTemplate = + simpleGhRepoTemplate + ("starters", [reldir|embeddings|]) + ( "embeddings", + "Comes with code for generating vector embeddings and performing vector similarity search." + ) + ( \projectDirName -> + unlines + [ styleText $ "To run your new app, follow the instructions below:", + styleText $ "", + styleText $ " 1. Position into app's root directory:", + styleCode $ " cd " <> projectDirName, + styleText $ "", + styleText $ " 2. Create initial dot env file from the template and fill in your API keys:", + styleCode $ " cp .env.server.example .env.server", + styleText $ " Fill in your API keys!", + styleText $ "", + styleText $ " 3. Run the development database (and leave it running):", + styleCode $ " wasp db start", + styleText $ "", + styleText $ " 4. Open new terminal window (or tab) in that same dir and continue in it.", + styleText $ "", + styleText $ " 5. Apply initial database migrations:", + styleCode $ " wasp db migrate-dev", + styleText $ "", + styleText $ " 6. Run wasp seed script that will generate embeddings from the text files in src/shared/docs:", + styleCode $ " wasp db seed", + styleText $ "", + styleText $ " 7. Last step: run the app!", + styleCode $ " wasp start", + styleText $ "", + styleText $ "Check the README for more detailed instructions and additional guidance!" + ] + ) + +simpleGhRepoTemplate :: (String, Path' Rel' Dir') -> (String, String) -> StartingInstructionsBuilder -> StarterTemplate +simpleGhRepoTemplate (repoName, tmplPathInRepo) (tmplDisplayName, tmplDescription) buildStartingInstructions = + GhRepoStarterTemplate + ( GhRepo.GithubRepoRef + { GhRepo._repoOwner = waspGhOrgName, + GhRepo._repoName = repoName, + GhRepo._repoReferenceName = waspVersionTemplateGitTag + } + ) + ( DirBasedTemplateMetadata + { _name = tmplDisplayName, + _description = tmplDescription, + _path = tmplPathInRepo, + _buildStartingInstructions = buildStartingInstructions + } + ) + +waspGhOrgName :: String +waspGhOrgName = "wasp-lang" + +-- NOTE: As version of Wasp CLI changes, so we should update this tag name here, +-- and also create it on gh repos of templates. +-- By tagging templates for each version of Wasp CLI, we ensure that each release of +-- Wasp CLI uses correct version of templates, that work with it. +waspVersionTemplateGitTag :: String +waspVersionTemplateGitTag = "wasp-v0.12-template" + findTemplateByString :: [StarterTemplate] -> String -> Maybe StarterTemplate findTemplateByString templates query = find ((== query) . show) templates diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/GhRepo.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/GhRepo.hs new file mode 100644 index 0000000000..e859f74fbe --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/GhRepo.hs @@ -0,0 +1,27 @@ +module Wasp.Cli.Command.CreateNewProject.StarterTemplates.GhRepo + ( createProjectOnDiskFromGhRepoTemplate, + ) +where + +import Control.Monad.IO.Class (liftIO) +import StrongPath (Abs, Dir, Dir', Path', Rel') +import Wasp.Cli.Command (Command) +import Wasp.Cli.Command.CreateNewProject.Common (throwProjectCreationError) +import Wasp.Cli.Command.CreateNewProject.ProjectDescription (NewProjectAppName, NewProjectName) +import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Templating (replaceTemplatePlaceholdersInWaspFile) +import Wasp.Cli.GithubRepo (GithubRepoRef, fetchFolderFromGithubRepoToDisk) +import Wasp.Project (WaspProjectDir) + +createProjectOnDiskFromGhRepoTemplate :: + Path' Abs (Dir WaspProjectDir) -> + NewProjectName -> + NewProjectAppName -> + GithubRepoRef -> + Path' Rel' Dir' -> + Command () +createProjectOnDiskFromGhRepoTemplate absWaspProjectDir projectName appName ghRepoRef templatePathInRepo = do + fetchTheTemplateFromGhToDisk >>= either throwProjectCreationError pure + liftIO $ replaceTemplatePlaceholdersInWaspFile appName projectName absWaspProjectDir + where + fetchTheTemplateFromGhToDisk = do + liftIO $ fetchFolderFromGithubRepoToDisk ghRepoRef templatePathInRepo absWaspProjectDir diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Local.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Local.hs index 655e88815e..ec3e85303f 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Local.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Local.hs @@ -3,10 +3,8 @@ module Wasp.Cli.Command.CreateNewProject.StarterTemplates.Local ) where -import Data.Maybe (fromJust) import Path.IO (copyDirRecur) -import StrongPath (Abs, Dir, Path', reldir, ()) -import qualified StrongPath as SP +import StrongPath (Abs, Dir, Dir', Path', Rel', reldir, ()) import StrongPath.Path (toPathAbsDir) import Wasp.Cli.Command.CreateNewProject.ProjectDescription (NewProjectAppName, NewProjectName) import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Templating (replaceTemplatePlaceholdersInWaspFile) @@ -14,16 +12,16 @@ import qualified Wasp.Data as Data import Wasp.Project (WaspProjectDir) createProjectOnDiskFromLocalTemplate :: - Path' Abs (Dir WaspProjectDir) -> NewProjectName -> NewProjectAppName -> String -> IO () + Path' Abs (Dir WaspProjectDir) -> NewProjectName -> NewProjectAppName -> Path' Rel' Dir' -> IO () createProjectOnDiskFromLocalTemplate absWaspProjectDir projectName appName templatePath = do copyLocalTemplateToNewProjectDir templatePath replaceTemplatePlaceholdersInWaspFile appName projectName absWaspProjectDir where - copyLocalTemplateToNewProjectDir :: String -> IO () + copyLocalTemplateToNewProjectDir :: Path' Rel' Dir' -> IO () copyLocalTemplateToNewProjectDir templateDir = do dataDir <- Data.getAbsDataDirPath let absLocalTemplateDir = - dataDir [reldir|Cli/templates|] (fromJust . SP.parseRelDir $ templateDir) + dataDir [reldir|Cli/templates|] templateDir let absSkeletonTemplateDir = dataDir [reldir|Cli/templates/skeleton|] -- First we copy skeleton files, which form the basis of any Wasp project, diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Remote.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Remote.hs deleted file mode 100644 index 7317a38866..0000000000 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Remote.hs +++ /dev/null @@ -1,31 +0,0 @@ -module Wasp.Cli.Command.CreateNewProject.StarterTemplates.Remote - ( createProjectOnDiskFromRemoteTemplate, - ) -where - -import Control.Monad.IO.Class (liftIO) -import Data.Maybe (fromJust) -import StrongPath (Abs, Dir, Path') -import qualified StrongPath as SP -import Wasp.Cli.Command (Command) -import Wasp.Cli.Command.CreateNewProject.Common (throwProjectCreationError) -import Wasp.Cli.Command.CreateNewProject.ProjectDescription (NewProjectAppName, NewProjectName) -import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Remote.Github (starterTemplateGithubRepo) -import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Templating (replaceTemplatePlaceholdersInWaspFile) -import Wasp.Cli.GithubRepo (fetchFolderFromGithubRepoToDisk) -import Wasp.Project (WaspProjectDir) - -createProjectOnDiskFromRemoteTemplate :: - Path' Abs (Dir WaspProjectDir) -> - NewProjectName -> - NewProjectAppName -> - String -> - Command () -createProjectOnDiskFromRemoteTemplate absWaspProjectDir projectName appName templatePath = do - fetchGithubTemplateToDisk absWaspProjectDir templatePath >>= either throwProjectCreationError pure - liftIO $ replaceTemplatePlaceholdersInWaspFile appName projectName absWaspProjectDir - where - fetchGithubTemplateToDisk :: Path' Abs (Dir WaspProjectDir) -> String -> Command (Either String ()) - fetchGithubTemplateToDisk projectDir templatePathInRepo = do - let templateFolderPath = fromJust . SP.parseRelDir $ templatePathInRepo - liftIO $ fetchFolderFromGithubRepoToDisk starterTemplateGithubRepo templateFolderPath projectDir diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Remote/Github.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Remote/Github.hs deleted file mode 100644 index db93fe6578..0000000000 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Remote/Github.hs +++ /dev/null @@ -1,36 +0,0 @@ -module Wasp.Cli.Command.CreateNewProject.StarterTemplates.Remote.Github where - -import Data.Aeson (FromJSON (parseJSON), withObject, (.:)) -import Wasp.Cli.GithubRepo (GithubRepoRef (..)) -import qualified Wasp.Cli.GithubRepo as GR - -starterTemplateGithubRepo :: GithubRepoRef -starterTemplateGithubRepo = - GithubRepoRef - { _repoOwner = "wasp-lang", - _repoName = "starters", - _repoReferenceName = "main" - } - -starterTemplatesDataGithubFilePath :: FilePath -starterTemplatesDataGithubFilePath = "templates.json" - -fetchRemoteTemplatesGithubData :: IO (Either String [RemoteTemplateGithubData]) -fetchRemoteTemplatesGithubData = GR.fetchRepoFileContents starterTemplateGithubRepo starterTemplatesDataGithubFilePath - -data RemoteTemplateGithubData = RemoteTemplateGithubData - { _name :: String, - _description :: String, - _path :: String - } - deriving (Show, Eq) - -instance FromJSON RemoteTemplateGithubData where - parseJSON = withObject "RemoteTemplateGithubData" $ \obj -> - RemoteTemplateGithubData - <$> obj - .: "name" - <*> obj - .: "description" - <*> obj - .: "path" diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Templating.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Templating.hs index 02eac1ba0b..8347151624 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Templating.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Templating.hs @@ -3,20 +3,26 @@ module Wasp.Cli.Command.CreateNewProject.StarterTemplates.Templating where import Data.List (foldl') import Data.Text (Text) import qualified Data.Text as T -import StrongPath (Abs, Dir, File, Path', relfile, ()) +import StrongPath (Abs, Dir, File, Path') import Wasp.Cli.Command.CreateNewProject.Common (defaultWaspVersionBounds) import Wasp.Cli.Command.CreateNewProject.ProjectDescription (NewProjectAppName, NewProjectName) +import Wasp.Project.Analyze (findWaspFile) import Wasp.Project.Common (WaspProjectDir) import qualified Wasp.Util.IO as IOUtil --- Template file for wasp file has placeholders in it that we want to replace +-- | Template file for wasp file has placeholders in it that we want to replace -- in the .wasp file we have written to the disk. -replaceTemplatePlaceholdersInWaspFile :: NewProjectAppName -> NewProjectName -> Path' Abs (Dir WaspProjectDir) -> IO () -replaceTemplatePlaceholdersInWaspFile appName projectName projectDir = - updateFileContent absMainWaspFile $ replacePlaceholders waspFileReplacements +-- If no .wasp file was found in the project, do nothing. +replaceTemplatePlaceholdersInWaspFile :: + NewProjectAppName -> NewProjectName -> Path' Abs (Dir WaspProjectDir) -> IO () +replaceTemplatePlaceholdersInWaspFile appName projectName projectDir = do + findWaspFile projectDir >>= \case + Nothing -> return () + Just absMainWaspFile -> + updateFileContentWith absMainWaspFile (replacePlaceholders waspFileReplacements) where - updateFileContent :: Path' Abs (File f) -> (Text -> Text) -> IO () - updateFileContent absFilePath updateFn = + updateFileContentWith :: Path' Abs (File f) -> (Text -> Text) -> IO () + updateFileContentWith absFilePath updateFn = IOUtil.readFileStrict absFilePath >>= IOUtil.writeFileFromText absFilePath . updateFn replacePlaceholders :: [(String, String)] -> Text -> Text @@ -24,7 +30,6 @@ replaceTemplatePlaceholdersInWaspFile appName projectName projectDir = where replacePlaceholder content' (placeholder, value) = T.replace (T.pack placeholder) (T.pack value) content' - absMainWaspFile = projectDir [relfile|main.wasp|] waspFileReplacements = [ ("__waspAppName__", show appName), ("__waspProjectName__", show projectName), diff --git a/waspc/cli/src/Wasp/Cli/Command/Db/Migrate.hs b/waspc/cli/src/Wasp/Cli/Command/Db/Migrate.hs index ee6bc64d78..5f4e21c7d5 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Db/Migrate.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Db/Migrate.hs @@ -10,11 +10,11 @@ import StrongPath (Abs, Dir, Path', ()) import Wasp.Cli.Command (Command, CommandError (..)) import Wasp.Cli.Command.Message (cliSendMessageC) import Wasp.Cli.Command.Require (InWaspProject (InWaspProject), require) -import qualified Wasp.Cli.Common as Cli.Common import Wasp.Generator.Common (ProjectRootDir) import Wasp.Generator.DbGenerator.Common (MigrateArgs (..), defaultMigrateArgs) import qualified Wasp.Generator.DbGenerator.Operations as DbOps import qualified Wasp.Message as Msg +import Wasp.Project.Common (dotWaspDirInWaspProjectDir, generatedCodeDirInDotWaspDir) import Wasp.Project.Db.Migrations (DbMigrationsDir, dbMigrationsDirInWaspProjectDir) -- | NOTE(shayne): Performs database schema migration (based on current schema) in the generated project. @@ -26,8 +26,8 @@ migrateDev optionalMigrateArgs = do let waspDbMigrationsDir = waspProjectDir dbMigrationsDirInWaspProjectDir let projectRootDir = waspProjectDir - Cli.Common.dotWaspDirInWaspProjectDir - Cli.Common.generatedCodeDirInDotWaspDir + dotWaspDirInWaspProjectDir + generatedCodeDirInDotWaspDir migrateDatabase optionalMigrateArgs projectRootDir waspDbMigrationsDir diff --git a/waspc/cli/src/Wasp/Cli/Command/Db/Reset.hs b/waspc/cli/src/Wasp/Cli/Command/Db/Reset.hs index 9220a671d8..a293636b5c 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Db/Reset.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Db/Reset.hs @@ -8,15 +8,15 @@ import StrongPath (()) import Wasp.Cli.Command (Command) import Wasp.Cli.Command.Message (cliSendMessageC) import Wasp.Cli.Command.Require (InWaspProject (InWaspProject), require) -import qualified Wasp.Cli.Common as Common import Wasp.Generator.DbGenerator.Operations (dbReset) import qualified Wasp.Message as Msg +import Wasp.Project.Common (dotWaspDirInWaspProjectDir, generatedCodeDirInDotWaspDir) reset :: Command () reset = do InWaspProject waspProjectDir <- require let genProjectDir = - waspProjectDir Common.dotWaspDirInWaspProjectDir Common.generatedCodeDirInDotWaspDir + waspProjectDir dotWaspDirInWaspProjectDir generatedCodeDirInDotWaspDir cliSendMessageC $ Msg.Start "Resetting the database..." diff --git a/waspc/cli/src/Wasp/Cli/Command/Db/Seed.hs b/waspc/cli/src/Wasp/Cli/Command/Db/Seed.hs index ee7dfb85d7..176962f097 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Db/Seed.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Db/Seed.hs @@ -19,15 +19,15 @@ import Wasp.Cli.Command (Command, CommandError (CommandError)) import Wasp.Cli.Command.Compile (analyze) import Wasp.Cli.Command.Message (cliSendMessageC) import Wasp.Cli.Command.Require (InWaspProject (InWaspProject), require) -import qualified Wasp.Cli.Common as Common import Wasp.Generator.DbGenerator.Operations (dbSeed) import qualified Wasp.Message as Msg +import Wasp.Project.Common (dotWaspDirInWaspProjectDir, generatedCodeDirInDotWaspDir) seed :: Maybe String -> Command () seed maybeUserProvidedSeedName = do InWaspProject waspProjectDir <- require let genProjectDir = - waspProjectDir Common.dotWaspDirInWaspProjectDir Common.generatedCodeDirInDotWaspDir + waspProjectDir dotWaspDirInWaspProjectDir generatedCodeDirInDotWaspDir appSpec <- analyze waspProjectDir diff --git a/waspc/cli/src/Wasp/Cli/Command/Db/Studio.hs b/waspc/cli/src/Wasp/Cli/Command/Db/Studio.hs index 708f2c31ff..09db8ca458 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Db/Studio.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Db/Studio.hs @@ -10,16 +10,16 @@ import StrongPath (()) import Wasp.Cli.Command (Command) import Wasp.Cli.Command.Message (cliSendMessageC) import Wasp.Cli.Command.Require (InWaspProject (InWaspProject), require) -import qualified Wasp.Cli.Common as Common import Wasp.Generator.DbGenerator.Jobs (runStudio) import Wasp.Generator.Job.IO (readJobMessagesAndPrintThemPrefixed) import qualified Wasp.Message as Msg +import Wasp.Project.Common (dotWaspDirInWaspProjectDir, generatedCodeDirInDotWaspDir) studio :: Command () studio = do InWaspProject waspProjectDir <- require let genProjectDir = - waspProjectDir Common.dotWaspDirInWaspProjectDir Common.generatedCodeDirInDotWaspDir + waspProjectDir dotWaspDirInWaspProjectDir generatedCodeDirInDotWaspDir cliSendMessageC $ Msg.Start "Running studio..." diff --git a/waspc/cli/src/Wasp/Cli/Command/Require.hs b/waspc/cli/src/Wasp/Cli/Command/Require.hs index a640c7d64b..f4d072893c 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Require.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Require.hs @@ -37,9 +37,9 @@ import qualified StrongPath as SP import System.Directory (doesFileExist, doesPathExist, getCurrentDirectory) import qualified System.FilePath as FP import Wasp.Cli.Command (CommandError (CommandError), Requirable (checkRequirement), require) -import Wasp.Cli.Common (WaspProjectDir) -import qualified Wasp.Cli.Common as Cli.Common import Wasp.Generator.DbGenerator.Operations (isDbConnectionPossible, testDbConnection) +import Wasp.Project.Common (WaspProjectDir) +import qualified Wasp.Project.Common as Project.Common data DbConnectionEstablished = DbConnectionEstablished deriving (Typeable) @@ -48,7 +48,10 @@ instance Requirable DbConnectionEstablished where -- NOTE: 'InWaspProject' does not depend on this requirement, so this -- call to 'require' will not result in an infinite loop. InWaspProject waspProjectDir <- require - let outDir = waspProjectDir SP. Cli.Common.dotWaspDirInWaspProjectDir SP. Cli.Common.generatedCodeDirInDotWaspDir + let outDir = + waspProjectDir + SP. Project.Common.dotWaspDirInWaspProjectDir + SP. Project.Common.generatedCodeDirInDotWaspDir dbIsRunning <- liftIO $ isDbConnectionPossible <$> testDbConnection outDir if dbIsRunning @@ -82,7 +85,7 @@ instance Requirable InWaspProject where let absCurrentDirFp = SP.fromAbsDir currentDir doesCurrentDirExist <- liftIO $ doesPathExist absCurrentDirFp unless doesCurrentDirExist (throwError notFoundError) - let dotWaspRootFilePath = absCurrentDirFp FP. SP.fromRelFile Cli.Common.dotWaspRootFileInWaspProjectDir + let dotWaspRootFilePath = absCurrentDirFp FP. SP.fromRelFile Project.Common.dotWaspRootFileInWaspProjectDir isCurrentDirRoot <- liftIO $ doesFileExist dotWaspRootFilePath if isCurrentDirRoot then return $ InWaspProject $ SP.castDir currentDir diff --git a/waspc/cli/src/Wasp/Cli/Command/Reset.hs b/waspc/cli/src/Wasp/Cli/Command/Reset.hs deleted file mode 100644 index 24612826bc..0000000000 --- a/waspc/cli/src/Wasp/Cli/Command/Reset.hs +++ /dev/null @@ -1,24 +0,0 @@ -module Wasp.Cli.Command.Reset - ( reset, - ) -where - -import StrongPath (Dir, Path') -import qualified StrongPath as SP -import StrongPath.Types (Abs) -import Wasp.Cli.Command (Command) -import Wasp.Cli.Command.Common (deleteDirectoryIfExists, deleteDotWaspDirIfExists) -import Wasp.Cli.Command.Require (InWaspProject (InWaspProject), require) -import Wasp.Cli.Common (WaspProjectDir) -import qualified Wasp.Cli.Common as Common - -reset :: Command () -reset = do - InWaspProject waspProjectDir <- require - deleteDotWaspDirIfExists waspProjectDir - deleteNodeModulesDirIfExists waspProjectDir - -deleteNodeModulesDirIfExists :: Path' Abs (Dir WaspProjectDir) -> Command () -deleteNodeModulesDirIfExists waspProjectDir = deleteDirectoryIfExists nodeModulesDir - where - nodeModulesDir = waspProjectDir SP. Common.nodeModulesDirInWaspProjectDir diff --git a/waspc/cli/src/Wasp/Cli/Command/Start.hs b/waspc/cli/src/Wasp/Cli/Command/Start.hs index b1cc33f385..eb0f99536a 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Start.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Start.hs @@ -14,17 +14,17 @@ import Wasp.Cli.Command.Compile (compile, printWarningsAndErrorsIfAny) import Wasp.Cli.Command.Message (cliSendMessageC) import Wasp.Cli.Command.Require (DbConnectionEstablished (DbConnectionEstablished), InWaspProject (InWaspProject), require) import Wasp.Cli.Command.Watch (watch) -import qualified Wasp.Cli.Common as Common import qualified Wasp.Generator import qualified Wasp.Message as Msg import Wasp.Project (CompileError, CompileWarning) +import Wasp.Project.Common (dotWaspDirInWaspProjectDir, generatedCodeDirInDotWaspDir) -- | Does initial compile of wasp code and then runs the generated project. -- It also listens for any file changes and recompiles and restarts generated project accordingly. start :: Command () start = do InWaspProject waspRoot <- require - let outDir = waspRoot Common.dotWaspDirInWaspProjectDir Common.generatedCodeDirInDotWaspDir + let outDir = waspRoot dotWaspDirInWaspProjectDir generatedCodeDirInDotWaspDir cliSendMessageC $ Msg.Start "Starting compilation and setup phase. Hold tight..." diff --git a/waspc/cli/src/Wasp/Cli/Command/Start/Db.hs b/waspc/cli/src/Wasp/Cli/Command/Start/Db.hs index eb2b3baaa5..7807418264 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Start/Db.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Start/Db.hs @@ -22,8 +22,8 @@ import Wasp.Cli.Command.Common (throwIfExeIsNotAvailable) import Wasp.Cli.Command.Compile (analyze) import Wasp.Cli.Command.Message (cliSendMessageC) import Wasp.Cli.Command.Require (InWaspProject (InWaspProject), require) -import Wasp.Cli.Common (WaspProjectDir) import qualified Wasp.Message as Msg +import Wasp.Project.Common (WaspProjectDir) import Wasp.Project.Db (databaseUrlEnvVarName) import Wasp.Project.Db.Dev (makeDevDbUniqueId) import qualified Wasp.Project.Db.Dev.Postgres as Dev.Postgres diff --git a/waspc/cli/src/Wasp/Cli/Command/Studio.hs b/waspc/cli/src/Wasp/Cli/Command/Studio.hs index 48ec59d4cd..f3929496bb 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Studio.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Studio.hs @@ -29,8 +29,8 @@ import Wasp.Cli.Command (Command, CommandError (CommandError)) import Wasp.Cli.Command.Compile (analyze) import Wasp.Cli.Command.Message (cliSendMessageC) import Wasp.Cli.Command.Require (InWaspProject (InWaspProject), require) -import qualified Wasp.Cli.Common as Common import qualified Wasp.Message as Msg +import Wasp.Project.Common (dotWaspDirInWaspProjectDir, generatedCodeDirInDotWaspDir) import qualified Wasp.Project.Studio studio :: Command () @@ -123,8 +123,8 @@ studio = do ] let generatedProjectDir = - waspDir Common.dotWaspDirInWaspProjectDir - Common.generatedCodeDirInDotWaspDir + waspDir dotWaspDirInWaspProjectDir + generatedCodeDirInDotWaspDir let waspStudioDataJsonFilePath = generatedProjectDir [relfile|.wasp-studio-data.json|] liftIO $ do diff --git a/waspc/cli/src/Wasp/Cli/Command/Test.hs b/waspc/cli/src/Wasp/Cli/Command/Test.hs index 2e8a761aa5..e9378bfbce 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Test.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Test.hs @@ -14,10 +14,10 @@ import Wasp.Cli.Command.Compile (compile) import Wasp.Cli.Command.Message (cliSendMessageC) import Wasp.Cli.Command.Require (InWaspProject (InWaspProject), require) import Wasp.Cli.Command.Watch (watch) -import qualified Wasp.Cli.Common as Common import qualified Wasp.Generator import Wasp.Generator.Common (ProjectRootDir) import qualified Wasp.Message as Msg +import Wasp.Project.Common (dotWaspDirInWaspProjectDir, generatedCodeDirInDotWaspDir) test :: [String] -> Command () test [] = throwError $ CommandError "Not enough arguments" "Expected: wasp test client " @@ -28,7 +28,7 @@ test _ = throwError $ CommandError "Invalid arguments" "Expected: wasp test clie watchAndTest :: (Path' Abs (Dir ProjectRootDir) -> IO (Either String ())) -> Command () watchAndTest testRunner = do InWaspProject waspRoot <- require - let outDir = waspRoot Common.dotWaspDirInWaspProjectDir Common.generatedCodeDirInDotWaspDir + let outDir = waspRoot dotWaspDirInWaspProjectDir generatedCodeDirInDotWaspDir cliSendMessageC $ Msg.Start "Starting compilation and setup phase. Hold tight..." diff --git a/waspc/cli/src/Wasp/Cli/Common.hs b/waspc/cli/src/Wasp/Cli/Common.hs index 0daa1155d3..2fe6841615 100644 --- a/waspc/cli/src/Wasp/Cli/Common.hs +++ b/waspc/cli/src/Wasp/Cli/Common.hs @@ -1,50 +1,15 @@ module Wasp.Cli.Common - ( WaspProjectDir, - DotWaspDir, - CliTemplatesDir, - dotWaspDirInWaspProjectDir, - dotWaspRootFileInWaspProjectDir, - dotWaspInfoFileInGeneratedCodeDir, - nodeModulesDirInWaspProjectDir, - generatedCodeDirInDotWaspDir, - buildDirInDotWaspDir, + ( CliTemplatesDir, waspSays, waspWarns, waspScreams, ) where -import StrongPath (Dir, File', Path', Rel, reldir, relfile) -import qualified Wasp.Generator.Common -import Wasp.Project (WaspProjectDir) import qualified Wasp.Util.Terminal as Term -data DotWaspDir -- Here we put everything that wasp generates. - -data NodeModulesDir - data CliTemplatesDir --- TODO: SHould this be renamed to include word "root"? -dotWaspDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir DotWaspDir) -dotWaspDirInWaspProjectDir = [reldir|.wasp|] - -nodeModulesDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir NodeModulesDir) -nodeModulesDirInWaspProjectDir = [reldir|node_modules|] - --- TODO: Hm this has different name than it has in Generator. -generatedCodeDirInDotWaspDir :: Path' (Rel DotWaspDir) (Dir Wasp.Generator.Common.ProjectRootDir) -generatedCodeDirInDotWaspDir = [reldir|out|] - -buildDirInDotWaspDir :: Path' (Rel DotWaspDir) (Dir Wasp.Generator.Common.ProjectRootDir) -buildDirInDotWaspDir = [reldir|build|] - -dotWaspRootFileInWaspProjectDir :: Path' (Rel WaspProjectDir) File' -dotWaspRootFileInWaspProjectDir = [relfile|.wasproot|] - -dotWaspInfoFileInGeneratedCodeDir :: Path' (Rel Wasp.Generator.Common.ProjectRootDir) File' -dotWaspInfoFileInGeneratedCodeDir = [relfile|.waspinfo|] - waspSays :: String -> IO () waspSays what = putStrLn $ Term.applyStyles [Term.Yellow] what diff --git a/waspc/data/Cli/templates/basic/package.json b/waspc/data/Cli/templates/basic/package.json index a3aceaf30c..d9b1ec1fd9 100644 --- a/waspc/data/Cli/templates/basic/package.json +++ b/waspc/data/Cli/templates/basic/package.json @@ -9,5 +9,15 @@ "vite": "^4.3.9", "@types/react": "^18.0.37", "prisma": "4.16.2" - } + }, + "//": [ + "COMMENTS:", + { + "devDependencies.prisma": [ + "We on purpose specify exact version for prisma.", + "Check this GH comment for the reasoning behind it:", + "https://github.com/wasp-lang/wasp/pull/634#issuecomment-1158802302 ." + ] + } + ] } diff --git a/waspc/data/Generator/templates/react-app/src/actions/index.ts b/waspc/data/Generator/templates/react-app/src/actions/index.ts index 5e4dfedd12..7fb2de2f9e 100644 --- a/waspc/data/Generator/templates/react-app/src/actions/index.ts +++ b/waspc/data/Generator/templates/react-app/src/actions/index.ts @@ -42,7 +42,7 @@ export type UpdateQuery = (item: ActionInput, oldData: /** * A public query specifier used for addressing Wasp queries. See our docs for details: - * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates */ export type QuerySpecifier = [Query, ...any[]] @@ -116,7 +116,7 @@ type InternalAction = Action & { * * @param publicOptimisticUpdateDefinition An optimistic update definition * object that's a part of the public API: - * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates * @returns An internally-used optimistic update definition object. */ function translateToInternalDefinition( @@ -260,7 +260,7 @@ function getOptimisticUpdateDefinitionForSpecificItem( * Translates a Wasp query specifier to a query cache key used by React Query. * * @param querySpecifier A query specifier that's a part of the public API: - * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates * @returns A cache key React Query internally uses for addressing queries. */ function getRqQueryKeyFromSpecifier(querySpecifier: QuerySpecifier): QueryKey { diff --git a/waspc/data/Generator/templates/react-app/src/auth/email/actions/login.ts b/waspc/data/Generator/templates/react-app/src/auth/email/actions/login.ts index c287486ef5..dafb5d9ac9 100644 --- a/waspc/data/Generator/templates/react-app/src/auth/email/actions/login.ts +++ b/waspc/data/Generator/templates/react-app/src/auth/email/actions/login.ts @@ -5,7 +5,7 @@ import { initSession } from '../../helpers/user'; export async function login(data: { email: string; password: string }): Promise { try { const response = await api.post('{= loginPath =}', data); - await initSession(response.data.token); + await initSession(response.data.sessionId); } catch (e) { handleApiError(e); } diff --git a/waspc/data/Generator/templates/react-app/src/auth/forms/internal/common/LoginSignupForm.tsx b/waspc/data/Generator/templates/react-app/src/auth/forms/internal/common/LoginSignupForm.tsx index 9ec80aa6f1..8fd6348c58 100644 --- a/waspc/data/Generator/templates/react-app/src/auth/forms/internal/common/LoginSignupForm.tsx +++ b/waspc/data/Generator/templates/react-app/src/auth/forms/internal/common/LoginSignupForm.tsx @@ -166,12 +166,6 @@ export const LoginSignupForm = ({ onLoginSuccess() { history.push('{= onAuthSucceededRedirectTo =}') }, - {=# isEmailVerificationRequired =} - isEmailVerificationRequired: true, - {=/ isEmailVerificationRequired =} - {=^ isEmailVerificationRequired =} - isEmailVerificationRequired: false, - {=/ isEmailVerificationRequired =} }); {=/ isEmailAuthEnabled =} {=# isAnyPasswordBasedAuthEnabled =} diff --git a/waspc/data/Generator/templates/react-app/src/auth/forms/internal/email/useEmail.ts b/waspc/data/Generator/templates/react-app/src/auth/forms/internal/email/useEmail.ts index f5f4e371c0..4d8b792ba0 100644 --- a/waspc/data/Generator/templates/react-app/src/auth/forms/internal/email/useEmail.ts +++ b/waspc/data/Generator/templates/react-app/src/auth/forms/internal/email/useEmail.ts @@ -4,7 +4,6 @@ import { login } from '../../../email/actions/login' export function useEmail({ onError, showEmailVerificationPending, - isEmailVerificationRequired, onLoginSuccess, isLogin, }: { @@ -12,7 +11,6 @@ export function useEmail({ showEmailVerificationPending: () => void onLoginSuccess: () => void isLogin: boolean - isEmailVerificationRequired: boolean }) { async function handleSubmit(data) { try { @@ -21,12 +19,7 @@ export function useEmail({ onLoginSuccess() } else { await signup(data) - if (isEmailVerificationRequired) { - showEmailVerificationPending() - } else { - await login(data) - onLoginSuccess() - } + showEmailVerificationPending() } } catch (err: unknown) { onError(err as Error) diff --git a/waspc/data/Generator/templates/react-app/src/auth/helpers/user.ts b/waspc/data/Generator/templates/react-app/src/auth/helpers/user.ts index 1c6fc500f4..a6b06299ce 100644 --- a/waspc/data/Generator/templates/react-app/src/auth/helpers/user.ts +++ b/waspc/data/Generator/templates/react-app/src/auth/helpers/user.ts @@ -1,8 +1,8 @@ -import { setAuthToken } from '../../api' +import { setSessionId } from '../../api' import { invalidateAndRemoveQueries } from '../../operations/resources' -export async function initSession(token: string): Promise { - setAuthToken(token) +export async function initSession(sessionId: string): Promise { + setSessionId(sessionId) // We need to invalidate queries after login in order to get the correct user // data in the React components (using `useAuth`). // Redirects after login won't work properly without this. diff --git a/waspc/data/Generator/templates/react-app/src/auth/login.ts b/waspc/data/Generator/templates/react-app/src/auth/login.ts index 9bc9ceef53..aa808309f3 100644 --- a/waspc/data/Generator/templates/react-app/src/auth/login.ts +++ b/waspc/data/Generator/templates/react-app/src/auth/login.ts @@ -7,7 +7,7 @@ export default async function login(username: string, password: string): Promise const args = { username, password } const response = await api.post('{= loginPath =}', args) - await initSession(response.data.token) + await initSession(response.data.sessionId) } catch (error) { handleApiError(error) } diff --git a/waspc/data/Generator/templates/react-app/src/auth/logout.ts b/waspc/data/Generator/templates/react-app/src/auth/logout.ts index 44b9e05c33..715f99a49b 100644 --- a/waspc/data/Generator/templates/react-app/src/auth/logout.ts +++ b/waspc/data/Generator/templates/react-app/src/auth/logout.ts @@ -1,9 +1,17 @@ -import { removeLocalUserData } from '../api' +import api, { removeLocalUserData } from '../api' import { invalidateAndRemoveQueries } from '../operations/resources' export default async function logout(): Promise { - removeLocalUserData() - // TODO(filip): We are currently invalidating and removing all the queries, but - // we should remove only the non-public, user-dependent ones. - await invalidateAndRemoveQueries() + try { + await api.post('/auth/logout') + } finally { + // Even if the logout request fails, we still want to remove the local user data + // in case the logout failed because of a network error and the user walked away + // from the computer. + removeLocalUserData() + + // TODO(filip): We are currently invalidating and removing all the queries, but + // we should remove only the non-public, user-dependent ones. + await invalidateAndRemoveQueries() + } } diff --git a/waspc/data/Generator/templates/react-app/src/auth/pages/OAuthCodeExchange.jsx b/waspc/data/Generator/templates/react-app/src/auth/pages/OAuthCodeExchange.jsx index fdc0fbbcc0..10b3ed4d44 100644 --- a/waspc/data/Generator/templates/react-app/src/auth/pages/OAuthCodeExchange.jsx +++ b/waspc/data/Generator/templates/react-app/src/auth/pages/OAuthCodeExchange.jsx @@ -29,7 +29,7 @@ export default function OAuthCodeExchange({ pathToApiServerRouteHandlingOauthRed // This helps us reuse one component for various methods (e.g., Google, Facebook, etc.). const apiServerUrlHandlingOauthRedirect = constructOauthRedirectApiServerUrl(pathToApiServerRouteHandlingOauthRedirect) - exchangeCodeForJwtAndRedirect(history, apiServerUrlHandlingOauthRedirect) + exchangeCodeForSessionIdAndRedirect(history, apiServerUrlHandlingOauthRedirect) return () => { firstRender.current = false } @@ -47,22 +47,22 @@ function constructOauthRedirectApiServerUrl(pathToApiServerRouteHandlingOauthRed return `${config.apiUrl}${pathToApiServerRouteHandlingOauthRedirect}${queryParams}` } -async function exchangeCodeForJwtAndRedirect(history, apiServerUrlHandlingOauthRedirect) { - const token = await exchangeCodeForJwt(apiServerUrlHandlingOauthRedirect) +async function exchangeCodeForSessionIdAndRedirect(history, apiServerUrlHandlingOauthRedirect) { + const sessionId = await exchangeCodeForSessionId(apiServerUrlHandlingOauthRedirect) - if (token !== null) { - await initSession(token) + if (sessionId !== null) { + await initSession(sessionId) history.push('{= onAuthSucceededRedirectTo =}') } else { - console.error('Error obtaining JWT token') + console.error('Error obtaining session ID') history.push('{= onAuthFailedRedirectTo =}') } } -async function exchangeCodeForJwt(url) { +async function exchangeCodeForSessionId(url) { try { const response = await api.get(url) - return response?.data?.token || null + return response?.data?.sessionId || null } catch (e) { console.error(e) return null diff --git a/waspc/data/Generator/templates/react-app/src/auth/types.ts b/waspc/data/Generator/templates/react-app/src/auth/types.ts index 4405410cc7..637a2e13d4 100644 --- a/waspc/data/Generator/templates/react-app/src/auth/types.ts +++ b/waspc/data/Generator/templates/react-app/src/auth/types.ts @@ -1,2 +1,2 @@ // todo(filip): turn into a proper import/path -export type { SanitizedUser as User, ProviderName, DeserializedAuthEntity } from '../../../server/src/_types/' +export type { SanitizedUser as User, ProviderName, DeserializedAuthIdentity } from '../../../server/src/_types/' diff --git a/waspc/data/Generator/templates/react-app/src/auth/user.ts b/waspc/data/Generator/templates/react-app/src/auth/user.ts index 5799c71ea7..aa0da24824 100644 --- a/waspc/data/Generator/templates/react-app/src/auth/user.ts +++ b/waspc/data/Generator/templates/react-app/src/auth/user.ts @@ -2,7 +2,7 @@ // We have them duplicated in this file and in data/Generator/templates/server/src/auth/user.ts // If you are changing the logic here, make sure to change it there as well. -import type { User, ProviderName, DeserializedAuthEntity } from './types' +import type { User, ProviderName, DeserializedAuthIdentity } from './types' export function getEmail(user: User): string | null { return findUserIdentity(user, "email")?.providerUserId ?? null; @@ -20,7 +20,7 @@ export function getFirstProviderUserId(user?: User): string | null { return user.auth.identities[0].providerUserId ?? null; } -export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthEntity | undefined { +export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthIdentity | undefined { return user.auth.identities.find( (identity) => identity.providerName === providerName ); diff --git a/waspc/data/Generator/templates/react-app/src/webSocket/WebSocketProvider.tsx b/waspc/data/Generator/templates/react-app/src/webSocket/WebSocketProvider.tsx index 10e4aa1e96..ef6e4b1e2a 100644 --- a/waspc/data/Generator/templates/react-app/src/webSocket/WebSocketProvider.tsx +++ b/waspc/data/Generator/templates/react-app/src/webSocket/WebSocketProvider.tsx @@ -2,7 +2,7 @@ import { createContext, useState, useEffect } from 'react' import { io, Socket } from 'socket.io-client' -import { getAuthToken } from '../api' +import { getSessionId } from '../api' import { apiEventsEmitter } from '../api/events' import config from '../config' @@ -16,7 +16,7 @@ function refreshAuthToken() { // NOTE: When we figure out how `auth: true` works for Operations, we should // mirror that behavior here for WebSockets. Ref: https://github.com/wasp-lang/wasp/issues/1133 socket.auth = { - token: getAuthToken() + sessionId: getSessionId() } if (socket.connected) { @@ -26,8 +26,8 @@ function refreshAuthToken() { } refreshAuthToken() -apiEventsEmitter.on('authToken.set', refreshAuthToken) -apiEventsEmitter.on('authToken.clear', refreshAuthToken) +apiEventsEmitter.on('sessionId.set', refreshAuthToken) +apiEventsEmitter.on('sessionId.clear', refreshAuthToken) export const WebSocketContext = createContext({ socket, diff --git a/waspc/data/Generator/templates/react-app/vite.config.ts b/waspc/data/Generator/templates/react-app/vite.config.ts index f7fd3d8720..8bc0157ed7 100644 --- a/waspc/data/Generator/templates/react-app/vite.config.ts +++ b/waspc/data/Generator/templates/react-app/vite.config.ts @@ -1,7 +1,7 @@ {{={= =}=}} /// import { mergeConfig } from "vite"; -import react from "@vitejs/plugin-react-swc"; +import react from "@vitejs/plugin-react"; {=# customViteConfig.isDefined =} {=& customViteConfig.importStatement =} @@ -30,6 +30,9 @@ const defaultViteConfig = { environment: "jsdom", setupFiles: ["./src/test/vitest/setup.ts"], }, + // resolve: { + // dedupe: ["react", "react-dom"], + // }, }; // https://vitejs.dev/config/ diff --git a/waspc/data/Generator/templates/sdk/api/events.ts b/waspc/data/Generator/templates/sdk/api/events.ts index 9a59b366d3..a72e48dda8 100644 --- a/waspc/data/Generator/templates/sdk/api/events.ts +++ b/waspc/data/Generator/templates/sdk/api/events.ts @@ -3,9 +3,9 @@ import mitt, { Emitter } from 'mitt'; type ApiEvents = { // key: Event name // type: Event payload type - 'authToken.set': void; - 'authToken.clear': void; + 'sessionId.set': void; + 'sessionId.clear': void; }; -// Used to allow API clients to register for auth token change events. +// Used to allow API clients to register for auth session ID change events. export const apiEventsEmitter: Emitter = mitt(); diff --git a/waspc/data/Generator/templates/sdk/api/index.ts b/waspc/data/Generator/templates/sdk/api/index.ts index 9aad1ead59..8b22dd7ebc 100644 --- a/waspc/data/Generator/templates/sdk/api/index.ts +++ b/waspc/data/Generator/templates/sdk/api/index.ts @@ -8,59 +8,60 @@ const api = axios.create({ baseURL: config.apiUrl, }) -const WASP_APP_AUTH_TOKEN_NAME = 'authToken' +const WASP_APP_AUTH_SESSION_ID_NAME = 'sessionId' -let authToken = storage.get(WASP_APP_AUTH_TOKEN_NAME) as string | undefined +let waspAppAuthSessionId = storage.get(WASP_APP_AUTH_SESSION_ID_NAME) as string | undefined -export function setAuthToken(token: string): void { - authToken = token - storage.set(WASP_APP_AUTH_TOKEN_NAME, token) - apiEventsEmitter.emit('authToken.set') +export function setSessionId(sessionId: string): void { + waspAppAuthSessionId = sessionId + storage.set(WASP_APP_AUTH_SESSION_ID_NAME, sessionId) + apiEventsEmitter.emit('sessionId.set') } -export function getAuthToken(): string | undefined { - return authToken +export function getSessionId(): string | undefined { + return waspAppAuthSessionId } -export function clearAuthToken(): void { - authToken = undefined - storage.remove(WASP_APP_AUTH_TOKEN_NAME) - apiEventsEmitter.emit('authToken.clear') +export function clearSessionId(): void { + waspAppAuthSessionId = undefined + storage.remove(WASP_APP_AUTH_SESSION_ID_NAME) + apiEventsEmitter.emit('sessionId.clear') } export function removeLocalUserData(): void { - authToken = undefined + waspAppAuthSessionId = undefined storage.clear() - apiEventsEmitter.emit('authToken.clear') + apiEventsEmitter.emit('sessionId.clear') } api.interceptors.request.use((request) => { - if (authToken) { - request.headers['Authorization'] = `Bearer ${authToken}` + const sessionId = getSessionId() + if (sessionId) { + request.headers['Authorization'] = `Bearer ${sessionId}` } return request }) api.interceptors.response.use(undefined, (error) => { if (error.response?.status === 401) { - clearAuthToken() + clearSessionId() } return Promise.reject(error) }) // This handler will run on other tabs (not the active one calling API functions), -// and will ensure they know about auth token changes. +// and will ensure they know about auth session ID changes. // Ref: https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event // "Note: This won't work on the same page that is making the changes — it is really a way // for other pages on the domain using the storage to sync any changes that are made." window.addEventListener('storage', (event) => { - if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_TOKEN_NAME)) { + if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_SESSION_ID_NAME)) { if (!!event.newValue) { - authToken = event.newValue - apiEventsEmitter.emit('authToken.set') + waspAppAuthSessionId = event.newValue + apiEventsEmitter.emit('sessionId.set') } else { - authToken = undefined - apiEventsEmitter.emit('authToken.clear') + waspAppAuthSessionId = undefined + apiEventsEmitter.emit('sessionId.clear') } } }) diff --git a/waspc/data/Generator/templates/sdk/auth/helpers/user.ts b/waspc/data/Generator/templates/sdk/auth/helpers/user.ts index c3e6a4072b..498f2588a8 100644 --- a/waspc/data/Generator/templates/sdk/auth/helpers/user.ts +++ b/waspc/data/Generator/templates/sdk/auth/helpers/user.ts @@ -1,8 +1,8 @@ -import { setAuthToken } from 'wasp/api' +import { setSessionId } from 'wasp/api' import { invalidateAndRemoveQueries } from 'wasp/operations/resources' -export async function initSession(token: string): Promise { - setAuthToken(token) +export async function initSession(sessionId: string): Promise { + setSessionId(sessionId) // We need to invalidate queries after login in order to get the correct user // data in the React components (using `useAuth`). // Redirects after login won't work properly without this. diff --git a/waspc/data/Generator/templates/sdk/auth/jwt.ts b/waspc/data/Generator/templates/sdk/auth/jwt.ts new file mode 100644 index 0000000000..06c0f10d3a --- /dev/null +++ b/waspc/data/Generator/templates/sdk/auth/jwt.ts @@ -0,0 +1,12 @@ +import jwt from 'jsonwebtoken' +import util from 'util' + +import config from 'wasp/core/config' + +const jwtSign = util.promisify(jwt.sign) +const jwtVerify = util.promisify(jwt.verify) + +const JWT_SECRET = config.auth.jwtSecret + +export const signData = (data, options) => jwtSign(data, JWT_SECRET, options) +export const verify = (token) => jwtVerify(token, JWT_SECRET) diff --git a/waspc/data/Generator/templates/sdk/auth/login.ts b/waspc/data/Generator/templates/sdk/auth/login.ts index 487b45b981..2b4ec4b9fe 100644 --- a/waspc/data/Generator/templates/sdk/auth/login.ts +++ b/waspc/data/Generator/templates/sdk/auth/login.ts @@ -6,7 +6,7 @@ export default async function login(username: string, password: string): Promise const args = { username, password } const response = await api.post('/auth/username/login', args) - await initSession(response.data.token) + await initSession(response.data.sessionId) } catch (error) { handleApiError(error) } diff --git a/waspc/data/Generator/templates/sdk/auth/logout.ts b/waspc/data/Generator/templates/sdk/auth/logout.ts index 340e9dec9c..cc41b6989c 100644 --- a/waspc/data/Generator/templates/sdk/auth/logout.ts +++ b/waspc/data/Generator/templates/sdk/auth/logout.ts @@ -1,9 +1,17 @@ -import { removeLocalUserData } from 'wasp/api' +import api, { removeLocalUserData } from 'wasp/api' import { invalidateAndRemoveQueries } from 'wasp/operations/resources' export default async function logout(): Promise { - removeLocalUserData() - // TODO(filip): We are currently invalidating and removing all the queries, but - // we should remove only the non-public, user-dependent ones. - await invalidateAndRemoveQueries() + try { + await api.post('/auth/logout') + } finally { + // Even if the logout request fails, we still want to remove the local user data + // in case the logout failed because of a network error and the user walked away + // from the computer. + removeLocalUserData() + + // TODO(filip): We are currently invalidating and removing all the queries, but + // we should remove only the non-public, user-dependent ones. + await invalidateAndRemoveQueries() + } } diff --git a/waspc/data/Generator/templates/sdk/auth/lucia.ts b/waspc/data/Generator/templates/sdk/auth/lucia.ts new file mode 100644 index 0000000000..690d090d44 --- /dev/null +++ b/waspc/data/Generator/templates/sdk/auth/lucia.ts @@ -0,0 +1,55 @@ +import { Lucia } from "lucia"; +import { PrismaAdapter } from "@lucia-auth/adapter-prisma"; +import prisma from '../server/dbClient.js' +import config from 'wasp/core/config' +import { type User } from "../entities/index.js" + +const prismaAdapter = new PrismaAdapter( + // Using `as any` here since Lucia's model types are not compatible with Prisma 4 + // model types. This is a temporary workaround until we migrate to Prisma 5. + // This **works** in runtime, but Typescript complains about it. + prisma.session as any, + prisma.auth as any +); + +/** + * We are using Lucia for session management. + * + * Some details: + * 1. We are using the Prisma adapter for Lucia. + * 2. We are not using cookies for session management. Instead, we are using + * the Authorization header to send the session token. + * 3. Our `Session` entity is connected to the `Auth` entity. + * 4. We are exposing the `userId` field from the `Auth` entity to + * make fetching the User easier. + */ +export const auth = new Lucia<{}, { + userId: User['id'] +}>(prismaAdapter, { + // Since we are not using cookies, we don't need to set any cookie options. + // But in the future, if we decide to use cookies, we can set them here. + + // sessionCookie: { + // name: "session", + // expires: true, + // attributes: { + // secure: !config.isDevelopment, + // sameSite: "lax", + // }, + // }, + getUserAttributes({ userId }) { + return { + userId, + }; + }, +}); + +declare module "lucia" { + interface Register { + Lucia: typeof auth; + DatabaseSessionAttributes: {}; + DatabaseUserAttributes: { + userId: User['id'] + }; + } +} diff --git a/waspc/data/Generator/templates/sdk/auth/password.ts b/waspc/data/Generator/templates/sdk/auth/password.ts new file mode 100644 index 0000000000..a359892b5e --- /dev/null +++ b/waspc/data/Generator/templates/sdk/auth/password.ts @@ -0,0 +1,15 @@ +import SecurePassword from 'secure-password' + +const SP = new SecurePassword() + +export const hashPassword = async (password: string): Promise => { + const hashedPwdBuffer = await SP.hash(Buffer.from(password)) + return hashedPwdBuffer.toString("base64") +} + +export const verifyPassword = async (hashedPassword: string, password: string): Promise => { + const result = await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64")) + if (result !== SecurePassword.VALID) { + throw new Error('Invalid password.') + } +} diff --git a/waspc/data/Generator/templates/sdk/auth/providers/types.ts b/waspc/data/Generator/templates/sdk/auth/providers/types.ts index 5bbc99ca83..76e1114850 100644 --- a/waspc/data/Generator/templates/sdk/auth/providers/types.ts +++ b/waspc/data/Generator/templates/sdk/auth/providers/types.ts @@ -23,16 +23,18 @@ export type InitData = { export type RequestWithWasp = Request & { wasp?: { [key: string]: any } } -export type PossibleAdditionalSignupFields = Expand> +export type PossibleUserFields = Expand> -export function defineAdditionalSignupFields(config: { - [key in keyof PossibleAdditionalSignupFields]: FieldGetter< - PossibleAdditionalSignupFields[key] +export type UserSignupFields = { + [key in keyof PossibleUserFields]: FieldGetter< + PossibleUserFields[key] > -}) { - return config } type FieldGetter = ( data: { [key: string]: unknown } ) => Promise | T | undefined + +export function defineUserSignupFields(fields: UserSignupFields) { + return fields +} diff --git a/waspc/data/Generator/templates/sdk/auth/session.ts b/waspc/data/Generator/templates/sdk/auth/session.ts new file mode 100644 index 0000000000..ed9154120b --- /dev/null +++ b/waspc/data/Generator/templates/sdk/auth/session.ts @@ -0,0 +1,107 @@ +import { Request as ExpressRequest } from "express"; + +import { type User } from "../entities/index.js" +import { type SanitizedUser } from '../server/_types/index.js' + +import { auth } from "./lucia.js"; +import type { Session } from "lucia"; +import { + throwInvalidCredentialsError, + deserializeAndSanitizeProviderData, +} from "./utils.js"; + +import prisma from '../server/dbClient.js' + +// Creates a new session for the `authId` in the database +export async function createSession(authId: string): Promise { + return auth.createSession(authId, {}); +} + +export async function getSessionAndUserFromBearerToken(req: ExpressRequest): Promise<{ + user: SanitizedUser | null, + session: Session | null, +}> { + const authorizationHeader = req.headers["authorization"]; + + if (typeof authorizationHeader !== "string") { + return { + user: null, + session: null, + }; + } + + const sessionId = auth.readBearerToken(authorizationHeader); + if (!sessionId) { + return { + user: null, + session: null, + }; + } + + return getSessionAndUserFromSessionId(sessionId); +} + +export async function getSessionAndUserFromSessionId(sessionId: string): Promise<{ + user: SanitizedUser | null, + session: Session | null, +}> { + const { session, user: authEntity } = await auth.validateSession(sessionId); + + if (!session || !authEntity) { + return { + user: null, + session: null, + }; + } + + return { + session, + user: await getUser(authEntity.userId) + } +} + +async function getUser(userId: User['id']): Promise { + const user = await prisma.user + .findUnique({ + where: { id: userId }, + include: { + auth: { + include: { + identities: true + } + } + } + }) + + if (!user) { + throwInvalidCredentialsError() + } + + // TODO: This logic must match the type in _types/index.ts (if we remove the + // password field from the object here, we must to do the same there). + // Ideally, these two things would live in the same place: + // https://github.com/wasp-lang/wasp/issues/965 + const deserializedIdentities = user.auth.identities.map((identity) => { + const deserializedProviderData = deserializeAndSanitizeProviderData( + identity.providerData, + { + shouldRemovePasswordField: true, + } + ) + return { + ...identity, + providerData: deserializedProviderData, + } + }) + return { + ...user, + auth: { + ...user.auth, + identities: deserializedIdentities, + }, + } +} + +export function invalidateSession(sessionId: string): Promise { + return auth.invalidateSession(sessionId); +} diff --git a/waspc/data/Generator/templates/sdk/auth/types.ts b/waspc/data/Generator/templates/sdk/auth/types.ts index 9240b4e4b0..f9f079a57a 100644 --- a/waspc/data/Generator/templates/sdk/auth/types.ts +++ b/waspc/data/Generator/templates/sdk/auth/types.ts @@ -1,2 +1,2 @@ // todo(filip): turn into a proper import/path -export type { SanitizedUser as User, ProviderName, DeserializedAuthEntity } from 'wasp/server/_types/' +export type { SanitizedUser as User, ProviderName, DeserializedAuthIdentity } from 'wasp/server/_types/' diff --git a/waspc/data/Generator/templates/sdk/auth/user.ts b/waspc/data/Generator/templates/sdk/auth/user.ts index 5799c71ea7..aa0da24824 100644 --- a/waspc/data/Generator/templates/sdk/auth/user.ts +++ b/waspc/data/Generator/templates/sdk/auth/user.ts @@ -2,7 +2,7 @@ // We have them duplicated in this file and in data/Generator/templates/server/src/auth/user.ts // If you are changing the logic here, make sure to change it there as well. -import type { User, ProviderName, DeserializedAuthEntity } from './types' +import type { User, ProviderName, DeserializedAuthIdentity } from './types' export function getEmail(user: User): string | null { return findUserIdentity(user, "email")?.providerUserId ?? null; @@ -20,7 +20,7 @@ export function getFirstProviderUserId(user?: User): string | null { return user.auth.identities[0].providerUserId ?? null; } -export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthEntity | undefined { +export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthIdentity | undefined { return user.auth.identities.find( (identity) => identity.providerName === providerName ); diff --git a/waspc/data/Generator/templates/sdk/auth/utils.ts b/waspc/data/Generator/templates/sdk/auth/utils.ts index 7a180abdc6..603e9a4b11 100644 --- a/waspc/data/Generator/templates/sdk/auth/utils.ts +++ b/waspc/data/Generator/templates/sdk/auth/utils.ts @@ -1,4 +1,5 @@ -import { hashPassword, sign, verify } from 'wasp/core/auth' +import { hashPassword } from './password.js' +import { verify } from './jwt.js' import AuthError from '../core/AuthError.js' import HttpError from '../core/HttpError.js' import prisma from '../server/dbClient.js' @@ -12,9 +13,7 @@ import { Prisma } from '@prisma/client'; import { throwValidationError } from './validation.js' - -import { defineAdditionalSignupFields, type PossibleAdditionalSignupFields } from './providers/types.js' -const _waspAdditionalSignupFieldsConfig = {} as ReturnType +import { type UserSignupFields, type PossibleUserFields } from './providers/types.js' export type EmailProviderData = { hashedPassword: string; @@ -127,8 +126,10 @@ export async function findAuthWithUserBy( export async function createUser( providerId: ProviderId, serializedProviderData?: string, - userFields?: PossibleAdditionalSignupFields, -): Promise { + userFields?: PossibleUserFields, +): Promise { return prisma.user.create({ data: { // Using any here to prevent type errors when userFields are not @@ -145,7 +146,12 @@ export async function createUser( }, } }, - } + }, + // We need to include the Auth entity here because we need `authId` + // to be able to create a session. + include: { + auth: true, + }, }) } @@ -155,12 +161,6 @@ export async function deleteUserByAuthId(authId: string): Promise<{ count: numbe } } }) } -export async function createAuthToken( - userId: User['id'] -): Promise { - return sign(userId); -} - export async function verifyToken(token: string): Promise { return verify(token); } @@ -224,15 +224,23 @@ export function rethrowPossibleAuthError(e: unknown): void { throw e } -export async function validateAndGetAdditionalFields(data: { - [key: string]: unknown -}): Promise> { +export async function validateAndGetUserFields( + data: { + [key: string]: unknown + }, + userSignupFields?: UserSignupFields, +): Promise> { const { password: _password, ...sanitizedData } = data; const result: Record = {}; - for (const [field, getFieldValue] of Object.entries(_waspAdditionalSignupFieldsConfig)) { + + if (!userSignupFields) { + return result; + } + + for (const [field, getFieldValue] of Object.entries(userSignupFields)) { try { const value = await getFieldValue(sanitizedData) result[field] = value @@ -288,3 +296,7 @@ function providerDataHasPasswordField( ): providerData is { hashedPassword: string } { return 'hashedPassword' in providerData; } + +export function throwInvalidCredentialsError(message?: string): void { + throw new HttpError(401, 'Invalid credentials', { message }) +} diff --git a/waspc/data/Generator/templates/sdk/core/auth.js b/waspc/data/Generator/templates/sdk/core/auth.js index 75e77a7fb9..6908bfb517 100644 --- a/waspc/data/Generator/templates/sdk/core/auth.js +++ b/waspc/data/Generator/templates/sdk/core/auth.js @@ -1,23 +1,22 @@ -import jwt from 'jsonwebtoken' -import SecurePassword from 'secure-password' -import util from 'util' import { randomInt } from 'node:crypto' -import prisma from '@server/dbClient.js' -import { handleRejection } from '../server/utils' -import HttpError from './HttpError.js' -import config from '../config.js' -import { deserializeAndSanitizeProviderData } from 'wasp/auth/utils' - -const jwtSign = util.promisify(jwt.sign) -const jwtVerify = util.promisify(jwt.verify) - -const JWT_SECRET = config.auth.jwtSecret - -export const signData = (data, options) => jwtSign(data, JWT_SECRET, options) -export const sign = (id, options) => signData({ id }, options) -export const verify = (token) => jwtVerify(token, JWT_SECRET) - +import prisma from '../server/dbClient.js' +import { handleRejection } from '../utils.js' +import { getSessionAndUserFromBearerToken } from 'wasp/auth/session' +import { throwInvalidCredentialsError } from 'wasp/auth/utils' + +/** + * Auth middleware + * + * If the request includes an `Authorization` header it will try to authenticate the request, + * otherwise it will let the request through. + * + * - If authentication succeeds it sets `req.sessionId` and `req.user` + * - `req.user` is the user that made the request and it's used in + * all Wasp features that need to know the user that made the request. + * - `req.sessionId` is the ID of the session that authenticated the request. + * - If the request is not authenticated, it throws an error. + */ const auth = handleRejection(async (req, res, next) => { const authHeader = req.get('Authorization') if (!authHeader) { @@ -27,119 +26,16 @@ const auth = handleRejection(async (req, res, next) => { return next() } - if (authHeader.startsWith('Bearer ')) { - const token = authHeader.substring(7, authHeader.length) - req.user = await getUserFromToken(token) - } else { - throwInvalidCredentialsError() - } + const { session, user } = await getSessionAndUserFromBearerToken(req); - next() -}) - -export async function getUserFromToken(token) { - let userIdFromToken - try { - userIdFromToken = (await verify(token)).id - } catch (error) { - if (['TokenExpiredError', 'JsonWebTokenError', 'NotBeforeError'].includes(error.name)) { - throwInvalidCredentialsError() - } else { - throw error - } - } - - const user = await prisma.user - .findUnique({ - where: { id: userIdFromToken }, - include: { - auth: { - include: { - identities: true - } - } - } - }) - if (!user) { + if (!session || !user) { throwInvalidCredentialsError() } - // TODO: This logic must match the type in types/index.ts (if we remove the - // password field from the object here, we must to do the same there). - // Ideally, these two things would live in the same place: - // https://github.com/wasp-lang/wasp/issues/965 - let sanitizedUser = { ...user } - sanitizedUser.auth.identities = sanitizedUser.auth.identities.map(identity => { - identity.providerData = deserializeAndSanitizeProviderData(identity.providerData, { shouldRemovePasswordField: true }) - return identity - }); - return sanitizedUser -} - -const SP = new SecurePassword() - -export const hashPassword = async (password) => { - const hashedPwdBuffer = await SP.hash(Buffer.from(password)) - return hashedPwdBuffer.toString("base64") -} - -export const verifyPassword = async (hashedPassword, password) => { - const result = await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64")) - if (result !== SecurePassword.VALID) { - throw new Error('Invalid password.') - } -} + req.sessionId = session.id + req.user = user -// Generates an unused username that looks similar to "quick-purple-sheep-91231". -// It generates several options and ensures it picks one that is not currently in use. -export function generateAvailableDictionaryUsername() { - const adjectives = ['fuzzy', 'tall', 'short', 'nice', 'happy', 'quick', 'slow', 'good', 'new', 'old', 'first', 'last', 'old', 'young'] - const colors = ['red', 'green', 'blue', 'white', 'black', 'brown', 'purple', 'orange', 'yellow'] - const nouns = ['wasp', 'cat', 'dog', 'lion', 'rabbit', 'duck', 'pig', 'bee', 'goat', 'crab', 'fish', 'chicken', 'horse', 'llama', 'camel', 'sheep'] - - const potentialUsernames = [] - for (let i = 0; i < 10; i++) { - const potentialUsername = `${adjectives[randomInt(adjectives.length)]}-${colors[randomInt(colors.length)]}-${nouns[randomInt(nouns.length)]}-${randomInt(100_000)}` - potentialUsernames.push(potentialUsername) - } - - return findAvailableUsername(potentialUsernames) -} - -// Generates an unused username based on an array of username segments and a separator. -// It generates several options and ensures it picks one that is not currently in use. -export function generateAvailableUsername(usernameSegments, config) { - const separator = config?.separator || '-' - const baseUsername = usernameSegments.join(separator) - - const potentialUsernames = [] - for (let i = 0; i < 10; i++) { - const potentialUsername = `${baseUsername}${separator}${randomInt(100_000)}` - potentialUsernames.push(potentialUsername) - } - - return findAvailableUsername(potentialUsernames) -} - -// Checks the database for an unused username from an array provided and returns first. -async function findAvailableUsername(potentialUsernames) { - const users = await prisma.user.findMany({ - where: { - username: { in: potentialUsernames }, - } - }) - const takenUsernames = users.map(user => user.username) - const availableUsernames = potentialUsernames.filter(username => !takenUsernames.includes(username)) - - if (availableUsernames.length === 0) { - throw new Error('Unable to generate a unique username. Please contact Wasp.') - } - - return availableUsernames[0] -} - -export function throwInvalidCredentialsError(message) { - throw new HttpError(401, 'Invalid credentials', { message }) -} + next() +}) export default auth diff --git a/waspc/data/Generator/templates/sdk/server/_types/index.ts b/waspc/data/Generator/templates/sdk/server/_types/index.ts index c8b1d08d83..7b64cec772 100644 --- a/waspc/data/Generator/templates/sdk/server/_types/index.ts +++ b/waspc/data/Generator/templates/sdk/server/_types/index.ts @@ -88,18 +88,18 @@ type Context = Expand<{ {=# isAuthEnabled =} type ContextWithUser = Expand & { user?: SanitizedUser }> -// TODO: This type must match the logic in core/auth.js (if we remove the +// TODO: This type must match the logic in core/session.js (if we remove the // password field from the object there, we must do the same here). Ideally, // these two things would live in the same place: // https://github.com/wasp-lang/wasp/issues/965 -export type DeserializedAuthEntity = Expand & { +export type DeserializedAuthIdentity = Expand & { providerData: Omit | Omit | OAuthProviderData }> export type SanitizedUser = {= userEntityName =} & { {= authFieldOnUserEntityName =}: {= authEntityName =} & { - {= identitiesFieldOnAuthEntityName =}: DeserializedAuthEntity[] + {= identitiesFieldOnAuthEntityName =}: DeserializedAuthIdentity[] } | null } diff --git a/waspc/data/Generator/templates/server/src/auth/index.ts b/waspc/data/Generator/templates/server/src/auth/index.ts index cf74b0773e..354fbe542a 100644 --- a/waspc/data/Generator/templates/server/src/auth/index.ts +++ b/waspc/data/Generator/templates/server/src/auth/index.ts @@ -1 +1 @@ -export { defineAdditionalSignupFields } from './providers/types.js'; +export { defineUserSignupFields } from './providers/types.js'; diff --git a/waspc/data/Generator/templates/server/src/auth/jwt.ts b/waspc/data/Generator/templates/server/src/auth/jwt.ts new file mode 100644 index 0000000000..5d2f4ae6fa --- /dev/null +++ b/waspc/data/Generator/templates/server/src/auth/jwt.ts @@ -0,0 +1,12 @@ +import jwt from 'jsonwebtoken' +import util from 'util' + +import config from '../config.js' + +const jwtSign = util.promisify(jwt.sign) +const jwtVerify = util.promisify(jwt.verify) + +const JWT_SECRET = config.auth.jwtSecret + +export const signData = (data, options) => jwtSign(data, JWT_SECRET, options) +export const verify = (token) => jwtVerify(token, JWT_SECRET) diff --git a/waspc/data/Generator/templates/server/src/auth/lucia.ts b/waspc/data/Generator/templates/server/src/auth/lucia.ts new file mode 100644 index 0000000000..12587d6494 --- /dev/null +++ b/waspc/data/Generator/templates/server/src/auth/lucia.ts @@ -0,0 +1,56 @@ +{{={= =}=}} +import { Lucia } from "lucia"; +import { PrismaAdapter } from "@lucia-auth/adapter-prisma"; +import prisma from '../dbClient.js' +import config from '../config.js' +import { type {= userEntityUpper =} } from "../entities/index.js" + +const prismaAdapter = new PrismaAdapter( + // Using `as any` here since Lucia's model types are not compatible with Prisma 4 + // model types. This is a temporary workaround until we migrate to Prisma 5. + // This **works** in runtime, but Typescript complains about it. + prisma.{= sessionEntityLower =} as any, + prisma.{= authEntityLower =} as any +); + +/** + * We are using Lucia for session management. + * + * Some details: + * 1. We are using the Prisma adapter for Lucia. + * 2. We are not using cookies for session management. Instead, we are using + * the Authorization header to send the session token. + * 3. Our `Session` entity is connected to the `Auth` entity. + * 4. We are exposing the `userId` field from the `Auth` entity to + * make fetching the User easier. + */ +export const auth = new Lucia<{}, { + userId: {= userEntityUpper =}['id'] +}>(prismaAdapter, { + // Since we are not using cookies, we don't need to set any cookie options. + // But in the future, if we decide to use cookies, we can set them here. + + // sessionCookie: { + // name: "session", + // expires: true, + // attributes: { + // secure: !config.isDevelopment, + // sameSite: "lax", + // }, + // }, + getUserAttributes({ userId }) { + return { + userId, + }; + }, +}); + +declare module "lucia" { + interface Register { + Lucia: typeof auth; + DatabaseSessionAttributes: {}; + DatabaseUserAttributes: { + userId: {= userEntityUpper =}['id'] + }; + } +} diff --git a/waspc/data/Generator/templates/server/src/auth/password.ts b/waspc/data/Generator/templates/server/src/auth/password.ts new file mode 100644 index 0000000000..a359892b5e --- /dev/null +++ b/waspc/data/Generator/templates/server/src/auth/password.ts @@ -0,0 +1,15 @@ +import SecurePassword from 'secure-password' + +const SP = new SecurePassword() + +export const hashPassword = async (password: string): Promise => { + const hashedPwdBuffer = await SP.hash(Buffer.from(password)) + return hashedPwdBuffer.toString("base64") +} + +export const verifyPassword = async (hashedPassword: string, password: string): Promise => { + const result = await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64")) + if (result !== SecurePassword.VALID) { + throw new Error('Invalid password.') + } +} diff --git a/waspc/data/Generator/templates/server/src/auth/providers/config/_oauth.ts b/waspc/data/Generator/templates/server/src/auth/providers/config/_oauth.ts index a397bfa4be..5d6a8c6c2d 100644 --- a/waspc/data/Generator/templates/server/src/auth/providers/config/_oauth.ts +++ b/waspc/data/Generator/templates/server/src/auth/providers/config/_oauth.ts @@ -6,13 +6,13 @@ import { makeOAuthInit } from "../oauth/init.js"; import type { ProviderConfig } from "../types.js"; import type { OAuthConfig } from "../oauth/types.js"; -{=# userFieldsFn.isDefined =} -{=& userFieldsFn.importStatement =} -const _waspGetUserFieldsFn = {= userFieldsFn.importIdentifier =} -{=/ userFieldsFn.isDefined =} -{=^ userFieldsFn.isDefined =} -const _waspGetUserFieldsFn = undefined -{=/ userFieldsFn.isDefined =} +{=# userSignupFields.isDefined =} +{=& userSignupFields.importStatement =} +const _waspUserSignupFields = {= userSignupFields.importIdentifier =} +{=/ userSignupFields.isDefined =} +{=^ userSignupFields.isDefined =} +const _waspUserSignupFields = undefined +{=/ userSignupFields.isDefined =} {=# configFn.isDefined =} {=& configFn.importStatement =} const _waspUserDefinedConfigFn = {= configFn.importIdentifier =} @@ -32,7 +32,7 @@ const _waspConfig: ProviderConfig = { displayName: "{= displayName =}", init: makeOAuthInit({ npmPackage: '{= npmPackage =}', - getUserFieldsFn: _waspGetUserFieldsFn, + userSignupFields: _waspUserSignupFields, userDefinedConfigFn: _waspUserDefinedConfigFn, oAuthConfig: _waspOAuthConfig, }), diff --git a/waspc/data/Generator/templates/server/src/auth/providers/config/email.ts b/waspc/data/Generator/templates/server/src/auth/providers/config/email.ts index ef327d20b5..979514bb24 100644 --- a/waspc/data/Generator/templates/server/src/auth/providers/config/email.ts +++ b/waspc/data/Generator/templates/server/src/auth/providers/config/email.ts @@ -12,6 +12,14 @@ import { verifyEmail } from "../email/verifyEmail.js"; import { GetVerificationEmailContentFn, GetPasswordResetEmailContentFn } from "../email/types.js"; import { handleRejection } from "../../../utils.js"; +{=# userSignupFields.isDefined =} +{=& userSignupFields.importStatement =} +const _waspUserSignupFields = {= userSignupFields.importIdentifier =} +{=/ userSignupFields.isDefined =} +{=^ userSignupFields.isDefined =} +const _waspUserSignupFields = undefined +{=/ userSignupFields.isDefined =} + {=# getVerificationEmailContent.isDefined =} {=& getVerificationEmailContent.importStatement =} const _waspGetVerificationEmailContent: GetVerificationEmailContentFn = {= getVerificationEmailContent.importIdentifier =}; @@ -53,16 +61,20 @@ const config: ProviderConfig = { createRouter() { const router = Router(); - const loginRoute = handleRejection(getLoginRoute({ - allowUnverifiedLogin: {=# allowUnverifiedLogin =}true{=/ allowUnverifiedLogin =}{=^ allowUnverifiedLogin =}false{=/ allowUnverifiedLogin =}, - })); + const loginRoute = handleRejection(getLoginRoute()); router.post('/login', loginRoute); const signupRoute = handleRejection(getSignupRoute({ + userSignupFields: _waspUserSignupFields, fromField, clientRoute: '{= emailVerificationClientRoute =}', getVerificationEmailContent: _waspGetVerificationEmailContent, - allowUnverifiedLogin: {=# allowUnverifiedLogin =}true{=/ allowUnverifiedLogin =}{=^ allowUnverifiedLogin =}false{=/ allowUnverifiedLogin =}, + {=# isDevelopment =} + isEmailAutoVerified: process.env.SKIP_EMAIL_VERIFICATION_IN_DEV === 'true', + {=/ isDevelopment =} + {=^ isDevelopment =} + isEmailAutoVerified: false, + {=/ isDevelopment =} })); router.post('/signup', signupRoute); diff --git a/waspc/data/Generator/templates/server/src/auth/providers/config/username.ts b/waspc/data/Generator/templates/server/src/auth/providers/config/username.ts index f7b78ed83b..c9846f5187 100644 --- a/waspc/data/Generator/templates/server/src/auth/providers/config/username.ts +++ b/waspc/data/Generator/templates/server/src/auth/providers/config/username.ts @@ -3,9 +3,17 @@ import { Router } from "express"; import login from "../username/login.js"; -import signup from "../username/signup.js"; +import { getSignupRoute } from "../username/signup.js"; import { ProviderConfig } from "../types.js"; +{=# userSignupFields.isDefined =} +{=& userSignupFields.importStatement =} +const _waspUserSignupFields = {= userSignupFields.importIdentifier =} +{=/ userSignupFields.isDefined =} +{=^ userSignupFields.isDefined =} +const _waspUserSignupFields = undefined +{=/ userSignupFields.isDefined =} + const config: ProviderConfig = { id: "{= providerId =}", displayName: "{= displayName =}", @@ -13,7 +21,10 @@ const config: ProviderConfig = { const router = Router(); router.post('/login', login); - router.post('/signup', signup); + const signupRoute = getSignupRoute({ + userSignupFields: _waspUserSignupFields, + }); + router.post('/signup', signupRoute); return router; }, diff --git a/waspc/data/Generator/templates/server/src/auth/providers/email/login.ts b/waspc/data/Generator/templates/server/src/auth/providers/email/login.ts index b50422e3a6..ee987ca193 100644 --- a/waspc/data/Generator/templates/server/src/auth/providers/email/login.ts +++ b/waspc/data/Generator/templates/server/src/auth/providers/email/login.ts @@ -1,23 +1,20 @@ import { Request, Response } from 'express'; -import { verifyPassword, throwInvalidCredentialsError } from "../../../core/auth.js"; +import { throwInvalidCredentialsError } from '../../utils.js' +import { verifyPassword } from '../../password.js' import { createProviderId, findAuthIdentity, findAuthWithUserBy, - createAuthToken, deserializeAndSanitizeProviderData, -} from "../../utils.js"; -import { ensureValidEmail, ensurePasswordIsPresent } from "../../validation.js"; +} from '../../utils.js' +import { createSession } from '../../session.js' +import { ensureValidEmail, ensurePasswordIsPresent } from '../../validation.js' -export function getLoginRoute({ - allowUnverifiedLogin, -}: { - allowUnverifiedLogin: boolean -}) { +export function getLoginRoute() { return async function login( req: Request<{ email: string; password: string; }>, res: Response, - ): Promise> { + ): Promise> { const fields = req.body ?? {} ensureValidArgs(fields) @@ -28,7 +25,7 @@ export function getLoginRoute({ throwInvalidCredentialsError() } const providerData = deserializeAndSanitizeProviderData<'email'>(authIdentity.providerData) - if (!providerData.isEmailVerified && !allowUnverifiedLogin) { + if (!providerData.isEmailVerified) { throwInvalidCredentialsError() } try { @@ -38,9 +35,11 @@ export function getLoginRoute({ } const auth = await findAuthWithUserBy({ id: authIdentity.authId }) - const token = await createAuthToken(auth.userId) + const session = await createSession(auth.id) - return res.json({ token }) + return res.json({ + sessionId: session.id, + }) }; } diff --git a/waspc/data/Generator/templates/server/src/auth/providers/email/signup.ts b/waspc/data/Generator/templates/server/src/auth/providers/email/signup.ts index e6755e2b47..05d5b0368a 100644 --- a/waspc/data/Generator/templates/server/src/auth/providers/email/signup.ts +++ b/waspc/data/Generator/templates/server/src/auth/providers/email/signup.ts @@ -17,19 +17,22 @@ import { } from "./utils.js"; import { ensureValidEmail, ensureValidPassword, ensurePasswordIsPresent } from "../../validation.js"; import { GetVerificationEmailContentFn } from './types.js'; -import { validateAndGetAdditionalFields } from '../../utils.js' +import { validateAndGetUserFields } from '../../utils.js' import HttpError from '../../../core/HttpError.js'; +import { type UserSignupFields } from '../types.js'; export function getSignupRoute({ + userSignupFields, fromField, clientRoute, getVerificationEmailContent, - allowUnverifiedLogin, + isEmailAutoVerified, }: { + userSignupFields?: UserSignupFields; fromField: EmailFromField; clientRoute: string; getVerificationEmailContent: GetVerificationEmailContentFn; - allowUnverifiedLogin: boolean; + isEmailAutoVerified: boolean; }) { return async function signup( req: Request<{ email: string; password: string; }>, @@ -65,20 +68,7 @@ export function getSignupRoute({ * else's email address and therefore permanently making that email * address unavailable for later account creation (by real owner). */ - if (existingAuthIdentity) { - if (allowUnverifiedLogin) { - /** - * This is the case where we allow unverified login. - * - * If we pretended that the user was created successfully that would bring - * us little value: the attacker would not be able to login and figure out - * if the user exists or not, anyway. - * - * So, we throw an error that says that the user already exists. - */ - throw new HttpError(422, "User with that email already exists.") - } - + if (existingAuthIdentity) { const providerData = deserializeAndSanitizeProviderData<'email'>(existingAuthIdentity.providerData); // TOOD: faking work makes sense if the time spent on faking the work matches the time @@ -103,11 +93,14 @@ export function getSignupRoute({ } } - const userFields = await validateAndGetAdditionalFields(fields); + const userFields = await validateAndGetUserFields( + fields, + userSignupFields, + ); const newUserProviderData = await sanitizeAndSerializeProviderData<'email'>({ hashedPassword: fields.password, - isEmailVerified: false, + isEmailVerified: isEmailAutoVerified ? true : false, emailVerificationSentAt: null, passwordResetSentAt: null, }); @@ -116,12 +109,20 @@ export function getSignupRoute({ await createUser( providerId, newUserProviderData, - userFields, + // Using any here because we want to avoid TypeScript errors and + // rely on Prisma to validate the data. + userFields as any ); } catch (e: unknown) { rethrowPossibleAuthError(e); } + // Wasp allows for auto-verification of emails in development mode to + // make writing e2e tests easier. + if (isEmailAutoVerified) { + return res.json({ success: true }); + } + const verificationLink = await createEmailVerificationLink(fields.email, clientRoute); try { await sendEmailVerificationEmail( @@ -135,7 +136,7 @@ export function getSignupRoute({ } catch (e: unknown) { console.error("Failed to send email verification email:", e); throw new HttpError(500, "Failed to send email verification email."); - } + } return res.json({ success: true }); }; diff --git a/waspc/data/Generator/templates/server/src/auth/providers/email/utils.ts b/waspc/data/Generator/templates/server/src/auth/providers/email/utils.ts index ba366e44b6..7aeb36b64a 100644 --- a/waspc/data/Generator/templates/server/src/auth/providers/email/utils.ts +++ b/waspc/data/Generator/templates/server/src/auth/providers/email/utils.ts @@ -1,5 +1,5 @@ {{={= =}=}} -import { signData } from '../../../core/auth.js' +import { signData } from '../../jwt.js' import { emailSender } from '../../../email/index.js'; import { Email } from '../../../email/core/types.js'; import { diff --git a/waspc/data/Generator/templates/server/src/auth/providers/oauth/createRouter.ts b/waspc/data/Generator/templates/server/src/auth/providers/oauth/createRouter.ts index ccae975623..ffc7441737 100644 --- a/waspc/data/Generator/templates/server/src/auth/providers/oauth/createRouter.ts +++ b/waspc/data/Generator/templates/server/src/auth/providers/oauth/createRouter.ts @@ -12,20 +12,22 @@ import { authConfig, contextWithUserEntity, createUser, - findAuthWithUserBy, - createAuthToken, rethrowPossibleAuthError, sanitizeAndSerializeProviderData, + validateAndGetUserFields, } from "../../utils.js" -import { type {= userEntityUpper =} } from "../../../entities/index.js" -import type { ProviderConfig, RequestWithWasp } from "../types.js" -import type { GetUserFieldsFn } from "./types.js" +import { createSession } from "../../session.js" +import { type {= authEntityUpper =} } from "../../../entities/index.js" +import type { ProviderConfig, RequestWithWasp, UserSignupFields } from "../types.js" import { handleRejection } from "../../../utils.js" // For oauth providers, we have an endpoint /login to get the auth URL, // and the /callback endpoint which is used to get the actual access_token and the user info. -export function createRouter(provider: ProviderConfig, initData: { passportStrategyName: string, getUserFieldsFn?: GetUserFieldsFn }) { - const { passportStrategyName, getUserFieldsFn } = initData; +export function createRouter(provider: ProviderConfig, initData: { + passportStrategyName: string, + userSignupFields?: UserSignupFields, +}) { + const { passportStrategyName, userSignupFields } = initData; const router = Router(); @@ -53,9 +55,11 @@ export function createRouter(provider: ProviderConfig, initData: { passportStrat const providerId = createProviderId(provider.id, providerProfile.id); try { - const userId = await getUserIdFromProviderDetails(providerId, providerProfile, getUserFieldsFn) - const token = await createAuthToken(userId) - res.json({ token }) + const authId = await getAuthIdFromProviderDetails(providerId, providerProfile, userSignupFields) + const session = await createSession(authId) + return res.json({ + sessionId: session.id, + }) } catch (e) { rethrowPossibleAuthError(e) } @@ -67,11 +71,11 @@ export function createRouter(provider: ProviderConfig, initData: { passportStrat // We need a user id to create the auth token, so we either find an existing user // or create a new one if none exists for this provider. -async function getUserIdFromProviderDetails( +async function getAuthIdFromProviderDetails( providerId: ProviderId, providerProfile: any, - getUserFieldsFn?: GetUserFieldsFn, -): Promise<{= userEntityUpper =}['id']> { + userSignupFields?: UserSignupFields, +): Promise<{= authEntityUpper =}['id']> { const existingAuthIdentity = await prisma.{= authIdentityEntityLower =}.findUnique({ where: { providerName_providerUserId: providerId, @@ -86,11 +90,12 @@ async function getUserIdFromProviderDetails( }) if (existingAuthIdentity) { - return existingAuthIdentity.{= authFieldOnAuthIdentityEntityName =}.{= userFieldOnAuthEntityName =}.id + return existingAuthIdentity.{= authFieldOnAuthIdentityEntityName =}.id } else { - const userFields = getUserFieldsFn - ? await getUserFieldsFn(contextWithUserEntity, { profile: providerProfile }) - : {}; + const userFields = await validateAndGetUserFields( + { profile: providerProfile }, + userSignupFields, + ); // For now, we don't have any extra data for the oauth providers, so we just pass an empty object. const providerData = await sanitizeAndSerializeProviderData({}) @@ -98,9 +103,11 @@ async function getUserIdFromProviderDetails( const user = await createUser( providerId, providerData, - userFields, + // Using any here because we want to avoid TypeScript errors and + // rely on Prisma to validate the data. + userFields as any, ) - return user.id + return user.auth.id } } diff --git a/waspc/data/Generator/templates/server/src/auth/providers/oauth/init.ts b/waspc/data/Generator/templates/server/src/auth/providers/oauth/init.ts index ac5a56dafe..1462f3a2f7 100644 --- a/waspc/data/Generator/templates/server/src/auth/providers/oauth/init.ts +++ b/waspc/data/Generator/templates/server/src/auth/providers/oauth/init.ts @@ -2,10 +2,10 @@ import passport from "passport"; import waspServerConfig from '../../../config.js'; -import type { InitData, ProviderConfig, RequestWithWasp } from "../types.js"; -import type { OAuthConfig, GetUserFieldsFn, UserDefinedConfigFn } from "./types.js"; +import type { InitData, ProviderConfig, RequestWithWasp, UserSignupFields } from "../types.js"; +import type { OAuthConfig, UserDefinedConfigFn } from "./types.js"; -export function makeOAuthInit({ userDefinedConfigFn, getUserFieldsFn, npmPackage, oAuthConfig }: OAuthImports) { +export function makeOAuthInit({ userDefinedConfigFn, userSignupFields, npmPackage, oAuthConfig }: OAuthImports) { return async function init(provider: ProviderConfig): Promise { const userDefinedConfig = userDefinedConfigFn ? userDefinedConfigFn() @@ -35,7 +35,7 @@ export function makeOAuthInit({ userDefinedConfigFn, getUserFieldsFn, npmPackage return { passportStrategyName, - getUserFieldsFn, + userSignupFields, }; } } @@ -72,5 +72,5 @@ export type OAuthImports = { npmPackage: string; userDefinedConfigFn?: UserDefinedConfigFn; oAuthConfig: OAuthConfig; - getUserFieldsFn?: GetUserFieldsFn; + userSignupFields?: UserSignupFields; }; diff --git a/waspc/data/Generator/templates/server/src/auth/providers/oauth/types.ts b/waspc/data/Generator/templates/server/src/auth/providers/oauth/types.ts index 390cd45923..d18295c676 100644 --- a/waspc/data/Generator/templates/server/src/auth/providers/oauth/types.ts +++ b/waspc/data/Generator/templates/server/src/auth/providers/oauth/types.ts @@ -12,8 +12,3 @@ export type OAuthConfig = { export type UserFieldsFromOAuthSignup = Prisma.{= userEntityName =}CreateInput export type UserDefinedConfigFn = () => { [key: string]: any } - -export type GetUserFieldsFn = ( - context: typeof contextWithUserEntity, - args: { profile: { [key: string]: any } }, -) => Promise diff --git a/waspc/data/Generator/templates/server/src/auth/providers/types.ts b/waspc/data/Generator/templates/server/src/auth/providers/types.ts index 96f1be6a5a..f040c1c490 100644 --- a/waspc/data/Generator/templates/server/src/auth/providers/types.ts +++ b/waspc/data/Generator/templates/server/src/auth/providers/types.ts @@ -24,16 +24,18 @@ export type InitData = { export type RequestWithWasp = Request & { wasp?: { [key: string]: any } } -export type PossibleAdditionalSignupFields = Expand> +export type PossibleUserFields = Expand> -export function defineAdditionalSignupFields(config: { - [key in keyof PossibleAdditionalSignupFields]: FieldGetter< - PossibleAdditionalSignupFields[key] +export type UserSignupFields = { + [key in keyof PossibleUserFields]: FieldGetter< + PossibleUserFields[key] > -}) { - return config } type FieldGetter = ( data: { [key: string]: unknown } ) => Promise | T | undefined + +export function defineUserSignupFields(fields: UserSignupFields) { + return fields +} diff --git a/waspc/data/Generator/templates/server/src/auth/providers/username/login.ts b/waspc/data/Generator/templates/server/src/auth/providers/username/login.ts index 26dd4cbad9..79dc83a5eb 100644 --- a/waspc/data/Generator/templates/server/src/auth/providers/username/login.ts +++ b/waspc/data/Generator/templates/server/src/auth/providers/username/login.ts @@ -1,14 +1,15 @@ {{={= =}=}} -import { verifyPassword, throwInvalidCredentialsError } from '../../../core/auth.js' +import { throwInvalidCredentialsError } from '../../utils.js' import { handleRejection } from 'wasp/server/utils' +import { verifyPassword } from '../../password.js' import { createProviderId, findAuthIdentity, findAuthWithUserBy, - createAuthToken, deserializeAndSanitizeProviderData, } from '../../utils.js' +import { createSession } from '../../session.js' import { ensureValidUsername, ensurePasswordIsPresent } from '../../validation.js' export default handleRejection(async (req, res) => { @@ -32,9 +33,12 @@ export default handleRejection(async (req, res) => { const auth = await findAuthWithUserBy({ id: authIdentity.authId }) - const token = await createAuthToken(auth.userId) - return res.json({ token }) + const session = await createSession(auth.id) + + return res.json({ + sessionId: session.id, + }) }) function ensureValidArgs(args: unknown): void { diff --git a/waspc/data/Generator/templates/server/src/auth/providers/username/signup.ts b/waspc/data/Generator/templates/server/src/auth/providers/username/signup.ts index bc15868451..53b1561fd5 100644 --- a/waspc/data/Generator/templates/server/src/auth/providers/username/signup.ts +++ b/waspc/data/Generator/templates/server/src/auth/providers/username/signup.ts @@ -11,33 +11,43 @@ import { ensurePasswordIsPresent, ensureValidPassword, } from '../../validation.js' -import { validateAndGetAdditionalFields } from '../../utils.js' +import { validateAndGetUserFields } from '../../utils.js' +import { type UserSignupFields } from '../types.js' -export default handleRejection(async (req, res) => { - const fields = req.body ?? {} - ensureValidArgs(fields) - - const userFields = await validateAndGetAdditionalFields(fields) - - const providerId = createProviderId('username', fields.username) - const providerData = await sanitizeAndSerializeProviderData<'username'>({ - hashedPassword: fields.password, +export function getSignupRoute({ + userSignupFields, +}: { + userSignupFields?: UserSignupFields; +}) { + return handleRejection(async function signup(req, res) { + const fields = req.body ?? {} + ensureValidArgs(fields) + + const userFields = await validateAndGetUserFields( + fields, + userSignupFields, + ); + + const providerId = createProviderId('username', fields.username) + const providerData = await sanitizeAndSerializeProviderData<'username'>({ + hashedPassword: fields.password, + }) + + try { + await createUser( + providerId, + providerData, + // Using any here because we want to avoid TypeScript errors and + // rely on Prisma to validate the data. + userFields as any + ) + } catch (e: unknown) { + rethrowPossibleAuthError(e) + } + + return res.json({ success: true }) }) - - try { - await createUser( - providerId, - providerData, - // Using any here because we want to avoid TypeScript errors and - // rely on Prisma to validate the data. - userFields as any - ) - } catch (e: unknown) { - rethrowPossibleAuthError(e) - } - - return res.json({ success: true }) -}) +} function ensureValidArgs(args: unknown): void { ensureValidUsername(args) diff --git a/waspc/data/Generator/templates/server/src/auth/session.ts b/waspc/data/Generator/templates/server/src/auth/session.ts new file mode 100644 index 0000000000..636c8f49f1 --- /dev/null +++ b/waspc/data/Generator/templates/server/src/auth/session.ts @@ -0,0 +1,108 @@ +{{={= =}=}} +import { Request as ExpressRequest } from "express"; + +import { type {= userEntityUpper =} } from "../entities/index.js" +import { type SanitizedUser } from '../_types/index.js' + +import { auth } from "./lucia.js"; +import type { Session } from "lucia"; +import { + throwInvalidCredentialsError, + deserializeAndSanitizeProviderData, +} from "./utils.js"; + +import prisma from '../dbClient.js'; + +// Creates a new session for the `authId` in the database +export async function createSession(authId: string): Promise { + return auth.createSession(authId, {}); +} + +export async function getSessionAndUserFromBearerToken(req: ExpressRequest): Promise<{ + user: SanitizedUser | null, + session: Session | null, +}> { + const authorizationHeader = req.headers["authorization"]; + + if (typeof authorizationHeader !== "string") { + return { + user: null, + session: null, + }; + } + + const sessionId = auth.readBearerToken(authorizationHeader); + if (!sessionId) { + return { + user: null, + session: null, + }; + } + + return getSessionAndUserFromSessionId(sessionId); +} + +export async function getSessionAndUserFromSessionId(sessionId: string): Promise<{ + user: SanitizedUser | null, + session: Session | null, +}> { + const { session, user: authEntity } = await auth.validateSession(sessionId); + + if (!session || !authEntity) { + return { + user: null, + session: null, + }; + } + + return { + session, + user: await getUser(authEntity.userId) + } +} + +async function getUser(userId: {= userEntityUpper =}['id']): Promise { + const user = await prisma.{= userEntityLower =} + .findUnique({ + where: { id: userId }, + include: { + {= authFieldOnUserEntityName =}: { + include: { + {= identitiesFieldOnAuthEntityName =}: true + } + } + } + }) + + if (!user) { + throwInvalidCredentialsError() + } + + // TODO: This logic must match the type in _types/index.ts (if we remove the + // password field from the object here, we must to do the same there). + // Ideally, these two things would live in the same place: + // https://github.com/wasp-lang/wasp/issues/965 + const deserializedIdentities = user.{= authFieldOnUserEntityName =}.{= identitiesFieldOnAuthEntityName =}.map((identity) => { + const deserializedProviderData = deserializeAndSanitizeProviderData( + identity.providerData, + { + shouldRemovePasswordField: true, + } + ) + return { + ...identity, + providerData: deserializedProviderData, + } + }) + return { + ...user, + auth: { + ...user.auth, + identities: deserializedIdentities, + }, + } +} + +export function invalidateSession(sessionId: string): Promise { + return auth.invalidateSession(sessionId); +} diff --git a/waspc/data/Generator/templates/server/src/auth/user.ts b/waspc/data/Generator/templates/server/src/auth/user.ts index 8440b3d48c..137cf1b4b0 100644 --- a/waspc/data/Generator/templates/server/src/auth/user.ts +++ b/waspc/data/Generator/templates/server/src/auth/user.ts @@ -2,7 +2,7 @@ // We have them duplicated in this file and in data/Generator/templates/react-app/src/auth/user.ts // If you are changing the logic here, make sure to change it there as well. -import type { SanitizedUser as User, ProviderName, DeserializedAuthEntity } from 'wasp/server/_types/index' +import type { SanitizedUser as User, ProviderName, DeserializedAuthIdentity } from 'wasp/server/_types/index' export function getEmail(user: User): string | null { return findUserIdentity(user, "email")?.providerUserId ?? null; @@ -20,7 +20,7 @@ export function getFirstProviderUserId(user?: User): string | null { return user.auth.identities[0].providerUserId ?? null; } -export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthEntity | undefined { +export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthIdentity | undefined { return user.auth.identities.find( (identity) => identity.providerName === providerName ); diff --git a/waspc/data/Generator/templates/server/src/auth/utils.ts b/waspc/data/Generator/templates/server/src/auth/utils.ts index 36facceed5..4037270b08 100644 --- a/waspc/data/Generator/templates/server/src/auth/utils.ts +++ b/waspc/data/Generator/templates/server/src/auth/utils.ts @@ -1,5 +1,6 @@ {{={= =}=}} -import { hashPassword, sign, verify } from '../core/auth.js' +import { hashPassword } from './password.js' +import { verify } from './jwt.js' import AuthError from '../core/AuthError.js' import HttpError from '../core/HttpError.js' import prisma from 'wasp/server/dbClient' @@ -13,17 +14,7 @@ import { Prisma } from '@prisma/client'; import { throwValidationError } from './validation.js' -{=# additionalSignupFields.isDefined =} -{=& additionalSignupFields.importStatement =} -{=/ additionalSignupFields.isDefined =} - -import { defineAdditionalSignupFields, type PossibleAdditionalSignupFields } from './providers/types.js' -{=# additionalSignupFields.isDefined =} -const _waspAdditionalSignupFieldsConfig = {= additionalSignupFields.importIdentifier =} -{=/ additionalSignupFields.isDefined =} -{=^ additionalSignupFields.isDefined =} -const _waspAdditionalSignupFieldsConfig = {} as ReturnType -{=/ additionalSignupFields.isDefined =} +import { type UserSignupFields, type PossibleUserFields } from './providers/types.js' export type EmailProviderData = { hashedPassword: string; @@ -136,8 +127,10 @@ export async function findAuthWithUserBy( export async function createUser( providerId: ProviderId, serializedProviderData?: string, - userFields?: PossibleAdditionalSignupFields, -): Promise<{= userEntityUpper =}> { + userFields?: PossibleUserFields, +): Promise<{= userEntityUpper =} & { + auth: {= authEntityUpper =} +}> { return prisma.{= userEntityLower =}.create({ data: { // Using any here to prevent type errors when userFields are not @@ -154,7 +147,12 @@ export async function createUser( }, } }, - } + }, + // We need to include the Auth entity here because we need `authId` + // to be able to create a session. + include: { + {= authFieldOnUserEntityName =}: true, + }, }) } @@ -164,12 +162,6 @@ export async function deleteUserByAuthId(authId: string): Promise<{ count: numbe } } }) } -export async function createAuthToken( - userId: {= userEntityUpper =}['id'] -): Promise { - return sign(userId); -} - export async function verifyToken(token: string): Promise { return verify(token); } @@ -233,15 +225,23 @@ export function rethrowPossibleAuthError(e: unknown): void { throw e } -export async function validateAndGetAdditionalFields(data: { - [key: string]: unknown -}): Promise> { +export async function validateAndGetUserFields( + data: { + [key: string]: unknown + }, + userSignupFields?: UserSignupFields, +): Promise> { const { password: _password, ...sanitizedData } = data; const result: Record = {}; - for (const [field, getFieldValue] of Object.entries(_waspAdditionalSignupFieldsConfig)) { + + if (!userSignupFields) { + return result; + } + + for (const [field, getFieldValue] of Object.entries(userSignupFields)) { try { const value = await getFieldValue(sanitizedData) result[field] = value @@ -297,3 +297,7 @@ function providerDataHasPasswordField( ): providerData is { hashedPassword: string } { return 'hashedPassword' in providerData; } + +export function throwInvalidCredentialsError(message?: string): void { + throw new HttpError(401, 'Invalid credentials', { message }) +} diff --git a/waspc/data/Generator/templates/server/src/core/auth.js b/waspc/data/Generator/templates/server/src/core/auth.js index c415d73457..e2ad8135a3 100644 --- a/waspc/data/Generator/templates/server/src/core/auth.js +++ b/waspc/data/Generator/templates/server/src/core/auth.js @@ -1,24 +1,23 @@ {{={= =}=}} -import jwt from 'jsonwebtoken' -import SecurePassword from 'secure-password' -import util from 'util' import { randomInt } from 'node:crypto' import prisma from 'wasp/server/dbClient' import { handleRejection } from 'wasp/server/utils' -import HttpError from '../core/HttpError.js' -import config from '../config.js' -import { deserializeAndSanitizeProviderData } from '../auth/utils.js' - -const jwtSign = util.promisify(jwt.sign) -const jwtVerify = util.promisify(jwt.verify) - -const JWT_SECRET = config.auth.jwtSecret - -export const signData = (data, options) => jwtSign(data, JWT_SECRET, options) -export const sign = (id, options) => signData({ id }, options) -export const verify = (token) => jwtVerify(token, JWT_SECRET) - +import { getSessionAndUserFromBearerToken } from '../auth/session.js' +import { throwInvalidCredentialsError } from '../auth/utils.js' + +/** + * Auth middleware + * + * If the request includes an `Authorization` header it will try to authenticate the request, + * otherwise it will let the request through. + * + * - If authentication succeeds it sets `req.sessionId` and `req.user` + * - `req.user` is the user that made the request and it's used in + * all Wasp features that need to know the user that made the request. + * - `req.sessionId` is the ID of the session that authenticated the request. + * - If the request is not authenticated, it throws an error. + */ const auth = handleRejection(async (req, res, next) => { const authHeader = req.get('Authorization') if (!authHeader) { @@ -28,119 +27,16 @@ const auth = handleRejection(async (req, res, next) => { return next() } - if (authHeader.startsWith('Bearer ')) { - const token = authHeader.substring(7, authHeader.length) - req.user = await getUserFromToken(token) - } else { - throwInvalidCredentialsError() - } + const { session, user } = await getSessionAndUserFromBearerToken(req); - next() -}) - -export async function getUserFromToken(token) { - let userIdFromToken - try { - userIdFromToken = (await verify(token)).id - } catch (error) { - if (['TokenExpiredError', 'JsonWebTokenError', 'NotBeforeError'].includes(error.name)) { - throwInvalidCredentialsError() - } else { - throw error - } - } - - const user = await prisma.{= userEntityLower =} - .findUnique({ - where: { id: userIdFromToken }, - include: { - {= authFieldOnUserEntityName =}: { - include: { - {= identitiesFieldOnAuthEntityName =}: true - } - } - } - }) - if (!user) { + if (!session || !user) { throwInvalidCredentialsError() } - // TODO: This logic must match the type in types/index.ts (if we remove the - // password field from the object here, we must to do the same there). - // Ideally, these two things would live in the same place: - // https://github.com/wasp-lang/wasp/issues/965 - let sanitizedUser = { ...user } - sanitizedUser.{= authFieldOnUserEntityName =}.{= identitiesFieldOnAuthEntityName =} = sanitizedUser.{= authFieldOnUserEntityName =}.{= identitiesFieldOnAuthEntityName =}.map(identity => { - identity.providerData = deserializeAndSanitizeProviderData(identity.providerData, { shouldRemovePasswordField: true }) - return identity - }); - return sanitizedUser -} - -const SP = new SecurePassword() - -export const hashPassword = async (password) => { - const hashedPwdBuffer = await SP.hash(Buffer.from(password)) - return hashedPwdBuffer.toString("base64") -} - -export const verifyPassword = async (hashedPassword, password) => { - const result = await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64")) - if (result !== SecurePassword.VALID) { - throw new Error('Invalid password.') - } -} + req.sessionId = session.id + req.user = user -// Generates an unused username that looks similar to "quick-purple-sheep-91231". -// It generates several options and ensures it picks one that is not currently in use. -export function generateAvailableDictionaryUsername() { - const adjectives = ['fuzzy', 'tall', 'short', 'nice', 'happy', 'quick', 'slow', 'good', 'new', 'old', 'first', 'last', 'old', 'young'] - const colors = ['red', 'green', 'blue', 'white', 'black', 'brown', 'purple', 'orange', 'yellow'] - const nouns = ['wasp', 'cat', 'dog', 'lion', 'rabbit', 'duck', 'pig', 'bee', 'goat', 'crab', 'fish', 'chicken', 'horse', 'llama', 'camel', 'sheep'] - - const potentialUsernames = [] - for (let i = 0; i < 10; i++) { - const potentialUsername = `${adjectives[randomInt(adjectives.length)]}-${colors[randomInt(colors.length)]}-${nouns[randomInt(nouns.length)]}-${randomInt(100_000)}` - potentialUsernames.push(potentialUsername) - } - - return findAvailableUsername(potentialUsernames) -} - -// Generates an unused username based on an array of username segments and a separator. -// It generates several options and ensures it picks one that is not currently in use. -export function generateAvailableUsername(usernameSegments, config) { - const separator = config?.separator || '-' - const baseUsername = usernameSegments.join(separator) - - const potentialUsernames = [] - for (let i = 0; i < 10; i++) { - const potentialUsername = `${baseUsername}${separator}${randomInt(100_000)}` - potentialUsernames.push(potentialUsername) - } - - return findAvailableUsername(potentialUsernames) -} - -// Checks the database for an unused username from an array provided and returns first. -async function findAvailableUsername(potentialUsernames) { - const users = await prisma.{= userEntityLower =}.findMany({ - where: { - username: { in: potentialUsernames }, - } - }) - const takenUsernames = users.map(user => user.username) - const availableUsernames = potentialUsernames.filter(username => !takenUsernames.includes(username)) - - if (availableUsernames.length === 0) { - throw new Error('Unable to generate a unique username. Please contact Wasp.') - } - - return availableUsernames[0] -} - -export function throwInvalidCredentialsError(message) { - throw new HttpError(401, 'Invalid credentials', { message }) -} + next() +}) export default auth diff --git a/waspc/data/Generator/templates/server/src/crud/_operations.ts b/waspc/data/Generator/templates/server/src/crud/_operations.ts index 11626b6599..0185a6cfd7 100644 --- a/waspc/data/Generator/templates/server/src/crud/_operations.ts +++ b/waspc/data/Generator/templates/server/src/crud/_operations.ts @@ -18,7 +18,7 @@ import type { {= crud.entityUpper =}, } from "../entities"; {=# isAuthEnabled =} -import { throwInvalidCredentialsError } from "../core/auth.js"; +import { throwInvalidCredentialsError } from '../auth/utils.js' {=/ isAuthEnabled =} {=# overrides.GetAll.isDefined =} {=& overrides.GetAll.importStatement =} diff --git a/waspc/data/Generator/templates/server/src/email/core/index.ts b/waspc/data/Generator/templates/server/src/email/core/index.ts index 9844ed3962..78430344b7 100644 --- a/waspc/data/Generator/templates/server/src/email/core/index.ts +++ b/waspc/data/Generator/templates/server/src/email/core/index.ts @@ -8,3 +8,6 @@ export { initSendGridEmailSender as initEmailSender } from "./providers/sendgrid {=# isMailgunProviderUsed =} export { initMailgunEmailSender as initEmailSender } from "./providers/mailgun.js"; {=/ isMailgunProviderUsed =} +{=# isDummyProviderUsed =} +export { initDummyEmailSender as initEmailSender } from "./providers/dummy.js"; +{=/ isDummyProviderUsed =} diff --git a/waspc/data/Generator/templates/server/src/email/core/providers/dummy.ts b/waspc/data/Generator/templates/server/src/email/core/providers/dummy.ts index b4b3ef0450..f7b244e873 100644 --- a/waspc/data/Generator/templates/server/src/email/core/providers/dummy.ts +++ b/waspc/data/Generator/templates/server/src/email/core/providers/dummy.ts @@ -1,21 +1,28 @@ -import { EmailSender } from "../types.js"; +import { DummyEmailProvider, EmailSender } from "../types.js"; import { getDefaultFromField } from "../helpers.js"; -export function initDummyEmailSender(): EmailSender { +const yellowColor = "\x1b[33m%s\x1b[0m"; + +export function initDummyEmailSender( + config?: DummyEmailProvider, +): EmailSender { const defaultFromField = getDefaultFromField(); return { send: async (email) => { const fromField = email.from || defaultFromField; - console.log('Test email (not sent):', { - from: { - email: fromField.email, - name: fromField.name, - }, - to: email.to, - subject: email.subject, - text: email.text, - html: email.html, - }); + + console.log(yellowColor, '╔═══════════════════════╗'); + console.log(yellowColor, '║ Dummy email sender ✉️ ║'); + console.log(yellowColor, '╚═══════════════════════╝'); + console.log(`From: ${fromField.name} <${fromField.email}>`); + console.log(`To: ${email.to}`); + console.log(`Subject: ${email.subject}`); + console.log(yellowColor, '═════════ Text ═════════'); + console.log(email.text); + console.log(yellowColor, '═════════ HTML ═════════'); + console.log(email.html); + console.log(yellowColor, '════════════════════════'); + return { success: true, }; diff --git a/waspc/data/Generator/templates/server/src/email/core/types.ts b/waspc/data/Generator/templates/server/src/email/core/types.ts index a86b26b3ba..8b2a984799 100644 --- a/waspc/data/Generator/templates/server/src/email/core/types.ts +++ b/waspc/data/Generator/templates/server/src/email/core/types.ts @@ -1,5 +1,5 @@ {{={= =}=}} -export type EmailProvider = SMTPEmailProvider | SendGridProvider | MailgunEmailProvider; +export type EmailProvider = SMTPEmailProvider | SendGridProvider | MailgunEmailProvider | DummyEmailProvider; export type SMTPEmailProvider = { type: "smtp"; @@ -20,6 +20,10 @@ export type MailgunEmailProvider = { domain: string; }; +export type DummyEmailProvider = { + type: "dummy"; +} + export type EmailSender = { send: (email: Email) => Promise; }; diff --git a/waspc/data/Generator/templates/server/src/email/index.ts b/waspc/data/Generator/templates/server/src/email/index.ts index a4467c43f2..3ed1f35b2b 100644 --- a/waspc/data/Generator/templates/server/src/email/index.ts +++ b/waspc/data/Generator/templates/server/src/email/index.ts @@ -1,9 +1,6 @@ {{={= =}=}} import { initEmailSender } from "./core/index.js"; -import waspServerConfig from '../config.js'; -import { initDummyEmailSender } from "./core/providers/dummy.js"; - {=# isSmtpProviderUsed =} const emailProvider = { type: "smtp", @@ -26,10 +23,10 @@ const emailProvider = { domain: process.env.MAILGUN_DOMAIN, } as const; {=/ isMailgunProviderUsed =} +{=# isDummyProviderUsed =} +const emailProvider = { + type: "dummy", +} as const; +{=/ isDummyProviderUsed =} -const areEmailsSentInDevelopment = process.env.SEND_EMAILS_IN_DEVELOPMENT === "true"; -const isDummyEmailSenderUsed = waspServerConfig.isDevelopment && !areEmailsSentInDevelopment; - -export const emailSender = isDummyEmailSenderUsed - ? initDummyEmailSender() - : initEmailSender(emailProvider); \ No newline at end of file +export const emailSender = initEmailSender(emailProvider); diff --git a/waspc/data/Generator/templates/server/src/polyfill.ts b/waspc/data/Generator/templates/server/src/polyfill.ts new file mode 100644 index 0000000000..a59302451a --- /dev/null +++ b/waspc/data/Generator/templates/server/src/polyfill.ts @@ -0,0 +1,7 @@ +// This is a polyfill for Node.js 18 webcrypto API so Lucia can use it +// for random number generation. + +import { webcrypto } from "node:crypto"; + +// @ts-ignore +globalThis.crypto = webcrypto as Crypto; diff --git a/waspc/data/Generator/templates/server/src/routes/auth/index.js b/waspc/data/Generator/templates/server/src/routes/auth/index.js index f0feee2cd2..b64c407651 100644 --- a/waspc/data/Generator/templates/server/src/routes/auth/index.js +++ b/waspc/data/Generator/templates/server/src/routes/auth/index.js @@ -3,12 +3,14 @@ import express from 'express' import auth from '../../core/auth.js' import me from './me.js' +import logout from './logout.js' import providersRouter from '../../auth/providers/index.js' const router = express.Router() router.get('/me', auth, me) +router.post('/logout', auth, logout) router.use('/', providersRouter) export default router diff --git a/waspc/data/Generator/templates/server/src/routes/auth/logout.ts b/waspc/data/Generator/templates/server/src/routes/auth/logout.ts new file mode 100644 index 0000000000..10a24a7bc5 --- /dev/null +++ b/waspc/data/Generator/templates/server/src/routes/auth/logout.ts @@ -0,0 +1,12 @@ +import { handleRejection } from '../../utils.js' +import { throwInvalidCredentialsError } from '../../auth/utils.js' +import { invalidateSession } from '../../auth/session.js' + +export default handleRejection(async (req, res) => { + if (req.sessionId) { + await invalidateSession(req.sessionId) + return res.json({ success: true }) + } else { + throwInvalidCredentialsError() + } +}) diff --git a/waspc/data/Generator/templates/server/src/routes/auth/me.js b/waspc/data/Generator/templates/server/src/routes/auth/me.js index e21057e319..63b1b13feb 100644 --- a/waspc/data/Generator/templates/server/src/routes/auth/me.js +++ b/waspc/data/Generator/templates/server/src/routes/auth/me.js @@ -1,7 +1,6 @@ -{{={= =}=}} import { serialize as superjsonSerialize } from 'superjson' import { handleRejection } from 'wasp/server/utils' -import { throwInvalidCredentialsError } from '../../core/auth.js' +import { throwInvalidCredentialsError } from '../../auth/utils.js' export default handleRejection(async (req, res) => { if (req.user) { diff --git a/waspc/data/Generator/templates/server/src/server.ts b/waspc/data/Generator/templates/server/src/server.ts index 92d166a728..cbc5738ea9 100644 --- a/waspc/data/Generator/templates/server/src/server.ts +++ b/waspc/data/Generator/templates/server/src/server.ts @@ -18,6 +18,8 @@ import './jobs/core/allJobs.js' import { init as initWebSocket } from './webSocket/initialization.js' {=/ userWebSocketFn.isDefined =} +import './polyfill.js' + const startServer = async () => { {=# isPgBossJobExecutorUsed =} await startPgBoss() diff --git a/waspc/data/Generator/templates/server/src/types/index.ts b/waspc/data/Generator/templates/server/src/types/index.ts index 93313219e1..205014216e 100644 --- a/waspc/data/Generator/templates/server/src/types/index.ts +++ b/waspc/data/Generator/templates/server/src/types/index.ts @@ -13,10 +13,6 @@ export type ServerSetupFnContext = { export type { Application } from 'express' export type { Server } from 'http' -{=# isExternalAuthEnabled =} -export type { GetUserFieldsFn } from '../auth/providers/oauth/types'; -{=/ isExternalAuthEnabled =} - {=# isEmailAuthEnabled =} export type { GetVerificationEmailContentFn, GetPasswordResetEmailContentFn } from '../auth/providers/email/types'; {=/ isEmailAuthEnabled =} diff --git a/waspc/data/Generator/templates/server/src/utils.ts b/waspc/data/Generator/templates/server/src/utils.ts index 44f05bfc7c..6ca262decd 100644 --- a/waspc/data/Generator/templates/server/src/utils.ts +++ b/waspc/data/Generator/templates/server/src/utils.ts @@ -12,7 +12,8 @@ import { type SanitizedUser } from 'wasp/server/_types/index.js' type RequestWithExtraFields = Request & { {=# isAuthEnabled =} - user?: SanitizedUser + user?: SanitizedUser; + sessionId?: string; {=/ isAuthEnabled =} } diff --git a/waspc/data/Generator/templates/server/src/webSocket/initialization.ts b/waspc/data/Generator/templates/server/src/webSocket/initialization.ts index 82789ef03e..5f6555c6e6 100644 --- a/waspc/data/Generator/templates/server/src/webSocket/initialization.ts +++ b/waspc/data/Generator/templates/server/src/webSocket/initialization.ts @@ -8,7 +8,7 @@ import config from '../config.js' import prisma from 'wasp/server/dbClient' {=# isAuthEnabled =} -import { getUserFromToken } from '../core/auth.js' +import { getSessionAndUserFromSessionId } from '../auth/session.js' {=/ isAuthEnabled =} {=& userWebSocketFn.importStatement =} @@ -40,10 +40,11 @@ export async function init(server: http.Server): Promise { {=# isAuthEnabled =} async function addUserToSocketDataIfAuthenticated(socket: Socket, next: (err?: Error) => void) { - const token = socket.handshake.auth.token - if (token) { + const sessionId = socket.handshake.auth.sessionId + if (sessionId) { try { - socket.data = { ...socket.data, user: await getUserFromToken(token) } + const { user } = await getSessionAndUserFromSessionId(sessionId) + socket.data = { ...socket.data, user } } catch (err) { } } next() diff --git a/waspc/e2e-test/ShellCommands.hs b/waspc/e2e-test/ShellCommands.hs index 6b529161e4..6700e540bb 100644 --- a/waspc/e2e-test/ShellCommands.hs +++ b/waspc/e2e-test/ShellCommands.hs @@ -1,5 +1,4 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE InstanceSigs #-} module ShellCommands ( ShellCommand, diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/files.manifest b/waspc/e2e-test/test-outputs/waspBuild-golden/files.manifest index 412fac0d7d..5080925b79 100644 --- a/waspc/e2e-test/test-outputs/waspBuild-golden/files.manifest +++ b/waspc/e2e-test/test-outputs/waspBuild-golden/files.manifest @@ -23,6 +23,7 @@ waspBuild/.wasp/build/server/src/entities/index.ts waspBuild/.wasp/build/server/src/middleware/globalMiddleware.ts waspBuild/.wasp/build/server/src/middleware/index.ts waspBuild/.wasp/build/server/src/middleware/operations.ts +waspBuild/.wasp/build/server/src/polyfill.ts waspBuild/.wasp/build/server/src/queries/types.ts waspBuild/.wasp/build/server/src/routes/index.js waspBuild/.wasp/build/server/src/routes/operations/index.js diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/.waspchecksums b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/.waspchecksums index 17b04a8a9d..6b56c32fc1 100644 --- a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/.waspchecksums +++ b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/.waspchecksums @@ -167,6 +167,13 @@ ], "864c7492c27f6da1e67645fbc358dc803a168852bfd24f2c4dd13fccf6917b07" ], + [ + [ + "file", + "server/src/polyfill.ts" + ], + "66d3dca514bdd01be402714d0dfe3836e76f612346dea57ee595ae4f3da915cf" + ], [ [ "file", @@ -193,14 +200,14 @@ "file", "server/src/server.ts" ], - "1c5af223cf0309b341e87cf8b6afd58b0cb21217e64cd9ee498048136a9da5be" + "d0666b659cdc75db181ea2bbb50c4e157f0a7fbe00c4ff8fda0933b1a13e5a0e" ], [ [ "file", "server/src/types/index.ts" ], - "1958cfc3e3b5f59490168797e4b8dcdc38f32346e734f90df3fb6baa264b36b5" + "f7621082fc7d8467a0967eb0bd82ff7956052b766e9e82d50584b8de88e0d28a" ], [ [ @@ -319,21 +326,21 @@ "file", "web-app/src/actions/index.ts" ], - "3afb54edb61cbc95a9b2133f9b3bdc460ca97580aca700adad988bf0515ab092" + "607c3311861456ae47c246a950c8e29593f9837a9f5c48923d99cd7fac1ce0bb" ], [ [ "file", "web-app/src/api.ts" ], - "93118387834981574ce1773d33275308e68ef8ca87408a35be8931c44a8889bf" + "850331885230117aa56317186c6d38f696fb1fbd0c56470ff7c6e4f3c1c43104" ], [ [ "file", "web-app/src/api/events.ts" ], - "7220e570cfb823028ad6c076cbcf033d217acfb88537bcac47020f1085757044" + "91ec1889f649b608ca81cab8f048538b9dcc70f49444430b1e5b572af2a4970a" ], [ [ diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/polyfill.ts b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/polyfill.ts new file mode 100644 index 0000000000..a59302451a --- /dev/null +++ b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/polyfill.ts @@ -0,0 +1,7 @@ +// This is a polyfill for Node.js 18 webcrypto API so Lucia can use it +// for random number generation. + +import { webcrypto } from "node:crypto"; + +// @ts-ignore +globalThis.crypto = webcrypto as Crypto; diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/server.ts b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/server.ts index 1df46663f5..fff57f199b 100644 --- a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/server.ts +++ b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/server.ts @@ -6,6 +6,8 @@ import config from './config.js' +import './polyfill.js' + const startServer = async () => { const port = normalizePort(config.port) diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/types/index.ts b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/types/index.ts index 9540b5dddc..b30c92f5ec 100644 --- a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/types/index.ts +++ b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/types/index.ts @@ -12,4 +12,3 @@ export type ServerSetupFnContext = { export type { Application } from 'express' export type { Server } from 'http' - diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/actions/index.ts b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/actions/index.ts index 5e4dfedd12..7fb2de2f9e 100644 --- a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/actions/index.ts +++ b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/actions/index.ts @@ -42,7 +42,7 @@ export type UpdateQuery = (item: ActionInput, oldData: /** * A public query specifier used for addressing Wasp queries. See our docs for details: - * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates */ export type QuerySpecifier = [Query, ...any[]] @@ -116,7 +116,7 @@ type InternalAction = Action & { * * @param publicOptimisticUpdateDefinition An optimistic update definition * object that's a part of the public API: - * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates * @returns An internally-used optimistic update definition object. */ function translateToInternalDefinition( @@ -260,7 +260,7 @@ function getOptimisticUpdateDefinitionForSpecificItem( * Translates a Wasp query specifier to a query cache key used by React Query. * * @param querySpecifier A query specifier that's a part of the public API: - * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates * @returns A cache key React Query internally uses for addressing queries. */ function getRqQueryKeyFromSpecifier(querySpecifier: QuerySpecifier): QueryKey { diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/api.ts b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/api.ts index d7532f65c6..17e36c1248 100644 --- a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/api.ts +++ b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/api.ts @@ -8,59 +8,60 @@ const api = axios.create({ baseURL: config.apiUrl, }) -const WASP_APP_AUTH_TOKEN_NAME = 'authToken' +const WASP_APP_AUTH_SESSION_ID_NAME = 'sessionId' -let authToken = storage.get(WASP_APP_AUTH_TOKEN_NAME) as string | undefined +let waspAppAuthSessionId = storage.get(WASP_APP_AUTH_SESSION_ID_NAME) as string | undefined -export function setAuthToken(token: string): void { - authToken = token - storage.set(WASP_APP_AUTH_TOKEN_NAME, token) - apiEventsEmitter.emit('authToken.set') +export function setSessionId(sessionId: string): void { + waspAppAuthSessionId = sessionId + storage.set(WASP_APP_AUTH_SESSION_ID_NAME, sessionId) + apiEventsEmitter.emit('sessionId.set') } -export function getAuthToken(): string | undefined { - return authToken +export function getSessionId(): string | undefined { + return waspAppAuthSessionId } -export function clearAuthToken(): void { - authToken = undefined - storage.remove(WASP_APP_AUTH_TOKEN_NAME) - apiEventsEmitter.emit('authToken.clear') +export function clearSessionId(): void { + waspAppAuthSessionId = undefined + storage.remove(WASP_APP_AUTH_SESSION_ID_NAME) + apiEventsEmitter.emit('sessionId.clear') } export function removeLocalUserData(): void { - authToken = undefined + waspAppAuthSessionId = undefined storage.clear() - apiEventsEmitter.emit('authToken.clear') + apiEventsEmitter.emit('sessionId.clear') } api.interceptors.request.use((request) => { - if (authToken) { - request.headers['Authorization'] = `Bearer ${authToken}` + const sessionId = getSessionId() + if (sessionId) { + request.headers['Authorization'] = `Bearer ${sessionId}` } return request }) api.interceptors.response.use(undefined, (error) => { if (error.response?.status === 401) { - clearAuthToken() + clearSessionId() } return Promise.reject(error) }) // This handler will run on other tabs (not the active one calling API functions), -// and will ensure they know about auth token changes. +// and will ensure they know about auth session ID changes. // Ref: https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event // "Note: This won't work on the same page that is making the changes — it is really a way // for other pages on the domain using the storage to sync any changes that are made." window.addEventListener('storage', (event) => { - if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_TOKEN_NAME)) { + if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_SESSION_ID_NAME)) { if (!!event.newValue) { - authToken = event.newValue - apiEventsEmitter.emit('authToken.set') + waspAppAuthSessionId = event.newValue + apiEventsEmitter.emit('sessionId.set') } else { - authToken = undefined - apiEventsEmitter.emit('authToken.clear') + waspAppAuthSessionId = undefined + apiEventsEmitter.emit('sessionId.clear') } } }) diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/api/events.ts b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/api/events.ts index 9a59b366d3..a72e48dda8 100644 --- a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/api/events.ts +++ b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/api/events.ts @@ -3,9 +3,9 @@ import mitt, { Emitter } from 'mitt'; type ApiEvents = { // key: Event name // type: Event payload type - 'authToken.set': void; - 'authToken.clear': void; + 'sessionId.set': void; + 'sessionId.clear': void; }; -// Used to allow API clients to register for auth token change events. +// Used to allow API clients to register for auth session ID change events. export const apiEventsEmitter: Emitter = mitt(); diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/files.manifest b/waspc/e2e-test/test-outputs/waspCompile-golden/files.manifest index 0658a077b2..3c7ea01ae8 100644 --- a/waspc/e2e-test/test-outputs/waspCompile-golden/files.manifest +++ b/waspc/e2e-test/test-outputs/waspCompile-golden/files.manifest @@ -24,6 +24,7 @@ waspCompile/.wasp/out/server/src/entities/index.ts waspCompile/.wasp/out/server/src/middleware/globalMiddleware.ts waspCompile/.wasp/out/server/src/middleware/index.ts waspCompile/.wasp/out/server/src/middleware/operations.ts +waspCompile/.wasp/out/server/src/polyfill.ts waspCompile/.wasp/out/server/src/queries/types.ts waspCompile/.wasp/out/server/src/routes/index.js waspCompile/.wasp/out/server/src/routes/operations/index.js diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/.waspchecksums b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/.waspchecksums index 6627b3d2d7..cb531d0c2e 100644 --- a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/.waspchecksums +++ b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/.waspchecksums @@ -174,6 +174,13 @@ ], "864c7492c27f6da1e67645fbc358dc803a168852bfd24f2c4dd13fccf6917b07" ], + [ + [ + "file", + "server/src/polyfill.ts" + ], + "66d3dca514bdd01be402714d0dfe3836e76f612346dea57ee595ae4f3da915cf" + ], [ [ "file", @@ -200,14 +207,14 @@ "file", "server/src/server.ts" ], - "1c5af223cf0309b341e87cf8b6afd58b0cb21217e64cd9ee498048136a9da5be" + "d0666b659cdc75db181ea2bbb50c4e157f0a7fbe00c4ff8fda0933b1a13e5a0e" ], [ [ "file", "server/src/types/index.ts" ], - "1958cfc3e3b5f59490168797e4b8dcdc38f32346e734f90df3fb6baa264b36b5" + "f7621082fc7d8467a0967eb0bd82ff7956052b766e9e82d50584b8de88e0d28a" ], [ [ @@ -333,21 +340,21 @@ "file", "web-app/src/actions/index.ts" ], - "3afb54edb61cbc95a9b2133f9b3bdc460ca97580aca700adad988bf0515ab092" + "607c3311861456ae47c246a950c8e29593f9837a9f5c48923d99cd7fac1ce0bb" ], [ [ "file", "web-app/src/api.ts" ], - "93118387834981574ce1773d33275308e68ef8ca87408a35be8931c44a8889bf" + "850331885230117aa56317186c6d38f696fb1fbd0c56470ff7c6e4f3c1c43104" ], [ [ "file", "web-app/src/api/events.ts" ], - "7220e570cfb823028ad6c076cbcf033d217acfb88537bcac47020f1085757044" + "91ec1889f649b608ca81cab8f048538b9dcc70f49444430b1e5b572af2a4970a" ], [ [ diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/polyfill.ts b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/polyfill.ts new file mode 100644 index 0000000000..a59302451a --- /dev/null +++ b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/polyfill.ts @@ -0,0 +1,7 @@ +// This is a polyfill for Node.js 18 webcrypto API so Lucia can use it +// for random number generation. + +import { webcrypto } from "node:crypto"; + +// @ts-ignore +globalThis.crypto = webcrypto as Crypto; diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/server.ts b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/server.ts index 1df46663f5..fff57f199b 100644 --- a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/server.ts +++ b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/server.ts @@ -6,6 +6,8 @@ import config from './config.js' +import './polyfill.js' + const startServer = async () => { const port = normalizePort(config.port) diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/types/index.ts b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/types/index.ts index 9540b5dddc..b30c92f5ec 100644 --- a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/types/index.ts +++ b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/types/index.ts @@ -12,4 +12,3 @@ export type ServerSetupFnContext = { export type { Application } from 'express' export type { Server } from 'http' - diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/actions/index.ts b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/actions/index.ts index 5e4dfedd12..7fb2de2f9e 100644 --- a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/actions/index.ts +++ b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/actions/index.ts @@ -42,7 +42,7 @@ export type UpdateQuery = (item: ActionInput, oldData: /** * A public query specifier used for addressing Wasp queries. See our docs for details: - * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates */ export type QuerySpecifier = [Query, ...any[]] @@ -116,7 +116,7 @@ type InternalAction = Action & { * * @param publicOptimisticUpdateDefinition An optimistic update definition * object that's a part of the public API: - * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates * @returns An internally-used optimistic update definition object. */ function translateToInternalDefinition( @@ -260,7 +260,7 @@ function getOptimisticUpdateDefinitionForSpecificItem( * Translates a Wasp query specifier to a query cache key used by React Query. * * @param querySpecifier A query specifier that's a part of the public API: - * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates * @returns A cache key React Query internally uses for addressing queries. */ function getRqQueryKeyFromSpecifier(querySpecifier: QuerySpecifier): QueryKey { diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/api.ts b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/api.ts index d7532f65c6..17e36c1248 100644 --- a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/api.ts +++ b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/api.ts @@ -8,59 +8,60 @@ const api = axios.create({ baseURL: config.apiUrl, }) -const WASP_APP_AUTH_TOKEN_NAME = 'authToken' +const WASP_APP_AUTH_SESSION_ID_NAME = 'sessionId' -let authToken = storage.get(WASP_APP_AUTH_TOKEN_NAME) as string | undefined +let waspAppAuthSessionId = storage.get(WASP_APP_AUTH_SESSION_ID_NAME) as string | undefined -export function setAuthToken(token: string): void { - authToken = token - storage.set(WASP_APP_AUTH_TOKEN_NAME, token) - apiEventsEmitter.emit('authToken.set') +export function setSessionId(sessionId: string): void { + waspAppAuthSessionId = sessionId + storage.set(WASP_APP_AUTH_SESSION_ID_NAME, sessionId) + apiEventsEmitter.emit('sessionId.set') } -export function getAuthToken(): string | undefined { - return authToken +export function getSessionId(): string | undefined { + return waspAppAuthSessionId } -export function clearAuthToken(): void { - authToken = undefined - storage.remove(WASP_APP_AUTH_TOKEN_NAME) - apiEventsEmitter.emit('authToken.clear') +export function clearSessionId(): void { + waspAppAuthSessionId = undefined + storage.remove(WASP_APP_AUTH_SESSION_ID_NAME) + apiEventsEmitter.emit('sessionId.clear') } export function removeLocalUserData(): void { - authToken = undefined + waspAppAuthSessionId = undefined storage.clear() - apiEventsEmitter.emit('authToken.clear') + apiEventsEmitter.emit('sessionId.clear') } api.interceptors.request.use((request) => { - if (authToken) { - request.headers['Authorization'] = `Bearer ${authToken}` + const sessionId = getSessionId() + if (sessionId) { + request.headers['Authorization'] = `Bearer ${sessionId}` } return request }) api.interceptors.response.use(undefined, (error) => { if (error.response?.status === 401) { - clearAuthToken() + clearSessionId() } return Promise.reject(error) }) // This handler will run on other tabs (not the active one calling API functions), -// and will ensure they know about auth token changes. +// and will ensure they know about auth session ID changes. // Ref: https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event // "Note: This won't work on the same page that is making the changes — it is really a way // for other pages on the domain using the storage to sync any changes that are made." window.addEventListener('storage', (event) => { - if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_TOKEN_NAME)) { + if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_SESSION_ID_NAME)) { if (!!event.newValue) { - authToken = event.newValue - apiEventsEmitter.emit('authToken.set') + waspAppAuthSessionId = event.newValue + apiEventsEmitter.emit('sessionId.set') } else { - authToken = undefined - apiEventsEmitter.emit('authToken.clear') + waspAppAuthSessionId = undefined + apiEventsEmitter.emit('sessionId.clear') } } }) diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/api/events.ts b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/api/events.ts index 9a59b366d3..a72e48dda8 100644 --- a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/api/events.ts +++ b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/api/events.ts @@ -3,9 +3,9 @@ import mitt, { Emitter } from 'mitt'; type ApiEvents = { // key: Event name // type: Event payload type - 'authToken.set': void; - 'authToken.clear': void; + 'sessionId.set': void; + 'sessionId.clear': void; }; -// Used to allow API clients to register for auth token change events. +// Used to allow API clients to register for auth session ID change events. export const apiEventsEmitter: Emitter = mitt(); diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/files.manifest b/waspc/e2e-test/test-outputs/waspComplexTest-golden/files.manifest index af31ebb1a2..ee6b2469e7 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/files.manifest +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/files.manifest @@ -21,12 +21,16 @@ waspComplexTest/.wasp/out/server/src/actions/MySpecialAction.ts waspComplexTest/.wasp/out/server/src/actions/types.ts waspComplexTest/.wasp/out/server/src/apis/types.ts waspComplexTest/.wasp/out/server/src/app.js +waspComplexTest/.wasp/out/server/src/auth/jwt.ts +waspComplexTest/.wasp/out/server/src/auth/lucia.ts +waspComplexTest/.wasp/out/server/src/auth/password.ts waspComplexTest/.wasp/out/server/src/auth/providers/config/google.ts waspComplexTest/.wasp/out/server/src/auth/providers/index.ts waspComplexTest/.wasp/out/server/src/auth/providers/oauth/createRouter.ts waspComplexTest/.wasp/out/server/src/auth/providers/oauth/init.ts waspComplexTest/.wasp/out/server/src/auth/providers/oauth/types.ts waspComplexTest/.wasp/out/server/src/auth/providers/types.ts +waspComplexTest/.wasp/out/server/src/auth/session.ts waspComplexTest/.wasp/out/server/src/auth/user.ts waspComplexTest/.wasp/out/server/src/auth/utils.ts waspComplexTest/.wasp/out/server/src/auth/validation.ts @@ -39,7 +43,6 @@ waspComplexTest/.wasp/out/server/src/dbClient.ts waspComplexTest/.wasp/out/server/src/dbSeed/types.ts waspComplexTest/.wasp/out/server/src/email/core/helpers.ts waspComplexTest/.wasp/out/server/src/email/core/index.ts -waspComplexTest/.wasp/out/server/src/email/core/providers/dummy.ts waspComplexTest/.wasp/out/server/src/email/core/providers/sendgrid.ts waspComplexTest/.wasp/out/server/src/email/core/types.ts waspComplexTest/.wasp/out/server/src/email/index.ts @@ -60,10 +63,12 @@ waspComplexTest/.wasp/out/server/src/jobs/core/pgBoss/pgBossJob.ts waspComplexTest/.wasp/out/server/src/middleware/globalMiddleware.ts waspComplexTest/.wasp/out/server/src/middleware/index.ts waspComplexTest/.wasp/out/server/src/middleware/operations.ts +waspComplexTest/.wasp/out/server/src/polyfill.ts waspComplexTest/.wasp/out/server/src/queries/MySpecialQuery.ts waspComplexTest/.wasp/out/server/src/queries/types.ts waspComplexTest/.wasp/out/server/src/routes/apis/index.ts waspComplexTest/.wasp/out/server/src/routes/auth/index.js +waspComplexTest/.wasp/out/server/src/routes/auth/logout.ts waspComplexTest/.wasp/out/server/src/routes/auth/me.js waspComplexTest/.wasp/out/server/src/routes/crud/index.ts waspComplexTest/.wasp/out/server/src/routes/crud/tasks.ts diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/.waspchecksums b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/.waspchecksums index b4ff5d3eac..cf657f8095 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/.waspchecksums +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/.waspchecksums @@ -18,7 +18,7 @@ "file", "db/schema.prisma" ], - "3e6bfc3dadfc9399a3fae3ea77cadb1aa3259efa0b8362ed77d6d5ee5013d393" + "5a41561cd6f853da2f4b9f64267f5d4e3b2865f94dfa92d9143acf2c1485519c" ], [ [ @@ -60,7 +60,7 @@ "file", "server/package.json" ], - "53aef3360681af1936a5bb2a3f7fa1c7cb74f58f398942bb8bd979a9b78e9b16" + "831f2505c29201cd8e5076fda768423448a6cbba92410b0692fd8ac630aeb76b" ], [ [ @@ -88,7 +88,7 @@ "file", "server/src/_types/index.ts" ], - "92027caebe484c7d412f97be9bc3c39257d10f9f2bc2e447e52c6855d5d30ffc" + "e7678be590a77799133e99d07a3326bbc6e627a5a219db8714a1b3d3b3304150" ], [ [ @@ -132,12 +132,33 @@ ], "86504ede1daeca35cc93f665ca8ac2fdf46ecaff02f6d3a7810a14d2bc71e16a" ], + [ + [ + "file", + "server/src/auth/jwt.ts" + ], + "be637e3c0ac601c1b6edb4a5cc6a9300edfb7fb1568220672254524a6b395b89" + ], + [ + [ + "file", + "server/src/auth/lucia.ts" + ], + "dc5e75c3e3da677c8e98d1456964f1f9bce9105bf90f3c351379e54d3433523b" + ], + [ + [ + "file", + "server/src/auth/password.ts" + ], + "f9003f11bf1396bff69a98440543e6e131f50d5bc1269a8aae55d72615922705" + ], [ [ "file", "server/src/auth/providers/config/google.ts" ], - "62e519ae90c87e1032e53d089a8d6106331a278ecd1293767c8e8e9cb4848f6a" + "3b6483ca304c77002f5cd4475ed8cb11617d210bfab169d85b82effc82836da8" ], [ [ @@ -151,42 +172,49 @@ "file", "server/src/auth/providers/oauth/createRouter.ts" ], - "da122a8a244ddbd9b84bba72a7da2f32ffa41c6093614c6c0d59e113244d2bbc" + "76e0bd6e17cdcea8c418c2577be8f39fa20db8d208eedafef679a1aee9cd1dae" ], [ [ "file", "server/src/auth/providers/oauth/init.ts" ], - "cef00c764f6c6923c0138f114eaf0484ad30c4e9dde7f6b44a143061909a8ba1" + "e844a8225699db6d44cc59fb842c1142f44cfd53ee431aa6738c9971001b412b" ], [ [ "file", "server/src/auth/providers/oauth/types.ts" ], - "00c951bd5dae77b7aedca90c0847f6e861e7f151e89b1906e794469981191b47" + "9f1561dae6c728a2e93938e49cc4c51f11d332b74f7cf3ad6634acaf91bbd634" ], [ [ "file", "server/src/auth/providers/types.ts" ], - "b647575a04eeb7824d95082a461d59763d034dc7d03a8fbcdd25143b6f8431b6" + "72cf71573f3ae1b5f49c7019001781a13f8617772d07926375ed6966164d4597" + ], + [ + [ + "file", + "server/src/auth/session.ts" + ], + "6edca533dbb38ddcba326fd0399dc6f5bc9b249327ed61c7c167d3a5a9d9a462" ], [ [ "file", "server/src/auth/user.ts" ], - "5787f3cdab4739781090f2950ba432dca812483ec23c6319ac3f876118324d15" + "74080241b9011ddce54ac2452b7c8a7064a4070ef1b04e420c730c62d787e077" ], [ [ "file", "server/src/auth/utils.ts" ], - "ba76300456ffdbd647923b27ff163df5e3efa016d7cd2a01af0b6a86dcd780a9" + "9022854d0aa165227abb25a1f8c0a2c3985ce2ce1d624ebe807749dbaef94091" ], [ [ @@ -221,14 +249,14 @@ "file", "server/src/core/auth.js" ], - "d708303af170e8159b93f0dda521b6f622c0f3add2d4f4f8f2fd88c0a4f7b79e" + "c6563934d31312dc92af13ebdac56d30a4672c7419be8a72f5666e8e4635f653" ], [ [ "file", "server/src/crud/tasks.ts" ], - "2c4e1f94939adf825df14624940019889394a0e56cdea2855686d67e0c08458a" + "3dbc7ee4341bc00af125c02d4b771bc7e7fb5bc7063191d659e7243217e2761f" ], [ [ @@ -258,13 +286,6 @@ ], "d524dd9ef27cd311340060411276df0e8ef22db503473f44281832338b954bb7" ], - [ - [ - "file", - "server/src/email/core/providers/dummy.ts" - ], - "e93a7a02f50c8466f3e8e89255b98bebde598b25f9969ec117b16f07691575ae" - ], [ [ "file", @@ -277,14 +298,14 @@ "file", "server/src/email/core/types.ts" ], - "c343f0d87b65d7563816159a88f410b65d78d897822c0bbcd723ca7752e00a20" + "0d7c19707f4e7c498a458015b1065b3f84c31e53ba73807707a05b7293473eb2" ], [ [ "file", "server/src/email/index.ts" ], - "c4864d5c83b96a61b1ddfaac7b52c0898f5cff04320c166c8f658b017952ee05" + "4443efa3da16d8d950bde84acd2ed6d9bc3a5d211ce3138248e3b5bad5079978" ], [ [ @@ -405,6 +426,13 @@ ], "64eeed927f46f6d6eba143023f25fb9ac4cd81d6b68c9a7067306ad28a3eda92" ], + [ + [ + "file", + "server/src/polyfill.ts" + ], + "66d3dca514bdd01be402714d0dfe3836e76f612346dea57ee595ae4f3da915cf" + ], [ [ "file", @@ -431,14 +459,21 @@ "file", "server/src/routes/auth/index.js" ], - "47fb3317c5707e0d7646bb1c0f6fa8d9c5c0f980ca2d643d226e63b49766cea3" + "9761e5af295928520748246d3be0ba4113655dbef3afb5c9123894627f3bee1a" + ], + [ + [ + "file", + "server/src/routes/auth/logout.ts" + ], + "1324b110888548ab535d78a4c7614c996152e948144a873d0262fa0aeb7ab4dc" ], [ [ "file", "server/src/routes/auth/me.js" ], - "705f77d8970a8367981c9a89601c6d5b12e998c23970ae1735b376dd0826ef10" + "9a9cb533bb94af63caf448f73a0d0fef8902c8f8d1af411bed2570a32da2fab9" ], [ [ @@ -487,14 +522,14 @@ "file", "server/src/server.ts" ], - "93c05fac0fb2e30eeda90dbb374bfa5c7fcb860b4605da8ae2c6b6f913f95963" + "7963a3e625deb86593258b01dd87c94cf4e2103dbda3a6ee82e672911dee09bc" ], [ [ "file", "server/src/types/index.ts" ], - "0044d2263b936cbc20a278afa4aa61cd9d85821325f490b3cee8bbfb068ed836" + "f7621082fc7d8467a0967eb0bd82ff7956052b766e9e82d50584b8de88e0d28a" ], [ [ @@ -515,7 +550,7 @@ "file", "server/src/utils.ts" ], - "f8834df362946064f32ef6a145769f83d10da712ad3daa226243fc590f89618f" + "7d29cb34de86e6a0689655e4165aed9fe5b0f82f54e4194f002f7d5823c7cb18" ], [ [ @@ -627,21 +662,21 @@ "file", "web-app/src/actions/index.ts" ], - "3afb54edb61cbc95a9b2133f9b3bdc460ca97580aca700adad988bf0515ab092" + "607c3311861456ae47c246a950c8e29593f9837a9f5c48923d99cd7fac1ce0bb" ], [ [ "file", "web-app/src/api.ts" ], - "93118387834981574ce1773d33275308e68ef8ca87408a35be8931c44a8889bf" + "850331885230117aa56317186c6d38f696fb1fbd0c56470ff7c6e4f3c1c43104" ], [ [ "file", "web-app/src/api/events.ts" ], - "7220e570cfb823028ad6c076cbcf033d217acfb88537bcac47020f1085757044" + "91ec1889f649b608ca81cab8f048538b9dcc70f49444430b1e5b572af2a4970a" ], [ [ @@ -718,21 +753,21 @@ "file", "web-app/src/auth/helpers/user.ts" ], - "e6bc091d8f8520db542f959846ecf528e8a070c5ce989151d00d2f45da4a58a6" + "e57cecd0a50b1515d6da8fcfb45f1e98ebefa4f58fe59d792100ffd40980fee9" ], [ [ "file", "web-app/src/auth/logout.ts" ], - "6717411aa38e54aa74a5034628510ee3ca0f2ea9d1692644ee4886c74d8657af" + "e04607e4676af958f580152432e873452a33a4276ecdb1a4c368efe2e3958a6c" ], [ [ "file", "web-app/src/auth/pages/OAuthCodeExchange.jsx" ], - "7dbcc288201aafbb50b5f5319a28283546c81d006fe61c2a8a3c5f55c6833fb2" + "1536a0abfd92944dca315e344758ba50f44a18e34c18a8c1c91046b1e4cc2a5e" ], [ [ @@ -746,7 +781,7 @@ "file", "web-app/src/auth/types.ts" ], - "5ce8d0493c362093b0b2fc7b9df78a86688d3f40264ea8f29530f1d8fa67c4c6" + "26b1d53d6ea48d56421ee7050486eb9086367195534f21ca3371cd0685307d7c" ], [ [ @@ -760,7 +795,7 @@ "file", "web-app/src/auth/user.ts" ], - "7113c286081f5597b822f5e576735d321cce38fcbd1a25db0d90e1163570068f" + "08cd2cf7dbb5aa5371efcfc1b16651c0c59d03d4ea36cd9f69bdaa9edc5ca68e" ], [ [ diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/db/schema.prisma b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/db/schema.prisma index 69c01a4535..ea120405ad 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/db/schema.prisma +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/db/schema.prisma @@ -38,6 +38,7 @@ model Auth { userId Int? @unique user User? @relation(fields: [userId], references: [id], onDelete: Cascade) identities AuthIdentity[] + sessions Session[] } model AuthIdentity { @@ -49,3 +50,11 @@ model AuthIdentity { @@id([providerName, providerUserId]) } +model Session { + id String @id @unique + expiresAt DateTime + userId String + auth Auth @relation(references: [id], fields: [userId], onDelete: Cascade) + @@index([userId]) + +} diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/db/schema.prisma.wasp-generate-checksum b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/db/schema.prisma.wasp-generate-checksum index b340294f3e..a654c68c7b 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/db/schema.prisma.wasp-generate-checksum +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/db/schema.prisma.wasp-generate-checksum @@ -1 +1 @@ -3e6bfc3dadfc9399a3fae3ea77cadb1aa3259efa0b8362ed77d6d5ee5013d393 \ No newline at end of file +5a41561cd6f853da2f4b9f64267f5d4e3b2865f94dfa92d9143acf2c1485519c \ No newline at end of file diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/installedFullStackNpmDependencies.json b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/installedFullStackNpmDependencies.json index 82f79b0197..24e5342120 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/installedFullStackNpmDependencies.json +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/installedFullStackNpmDependencies.json @@ -1 +1 @@ -{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.16.2"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"rate-limiter-flexible","version":"^2.4.1"},{"name":"superjson","version":"^1.12.2"},{"name":"passport","version":"0.6.0"},{"name":"passport-google-oauth20","version":"2.0.0"},{"name":"pg-boss","version":"^8.4.2"},{"name":"@sendgrid/mail","version":"^7.7.0"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.16.2"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.0.0"},{"name":"@tsconfig/node18","version":"latest"},{"name":"@types/uuid","version":"^9.0.0"},{"name":"@types/cors","version":"^2.8.5"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"^4.29.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.16.2"},{"name":"superjson","version":"^1.12.2"},{"name":"mitt","version":"3.0.0"},{"name":"react-hook-form","version":"^7.45.4"},{"name":"@stitches/react","version":"^1.2.8"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"vite","version":"^4.3.9"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^2.0.0"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.0.0"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}} \ No newline at end of file +{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.16.2"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"rate-limiter-flexible","version":"^2.4.1"},{"name":"superjson","version":"^1.12.2"},{"name":"lucia","version":"^3.0.0-beta.14"},{"name":"@lucia-auth/adapter-prisma","version":"^4.0.0-beta.9"},{"name":"passport","version":"0.6.0"},{"name":"passport-google-oauth20","version":"2.0.0"},{"name":"pg-boss","version":"^8.4.2"},{"name":"@sendgrid/mail","version":"^7.7.0"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.16.2"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.0.0"},{"name":"@tsconfig/node18","version":"latest"},{"name":"@types/uuid","version":"^9.0.0"},{"name":"@types/cors","version":"^2.8.5"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"^4.29.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.16.2"},{"name":"superjson","version":"^1.12.2"},{"name":"mitt","version":"3.0.0"},{"name":"react-hook-form","version":"^7.45.4"},{"name":"@stitches/react","version":"^1.2.8"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"vite","version":"^4.3.9"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^2.0.0"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.0.0"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}} \ No newline at end of file diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/package.json b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/package.json index 0aa97eb91a..d17c69c2d5 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/package.json +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/package.json @@ -1,5 +1,6 @@ { "dependencies": { + "@lucia-auth/adapter-prisma": "^4.0.0-beta.9", "@prisma/client": "4.16.2", "@sendgrid/mail": "^7.7.0", "cookie-parser": "~1.4.6", @@ -9,6 +10,7 @@ "helmet": "^6.0.0", "jsonwebtoken": "^8.5.1", "lodash.merge": "^4.6.2", + "lucia": "^3.0.0-beta.14", "morgan": "~1.10.0", "passport": "0.6.0", "passport-google-oauth20": "2.0.0", diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/_types/index.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/_types/index.ts index 23017df130..fc52fa392d 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/_types/index.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/_types/index.ts @@ -82,18 +82,18 @@ type Context = Expand<{ type ContextWithUser = Expand & { user?: SanitizedUser }> -// TODO: This type must match the logic in core/auth.js (if we remove the +// TODO: This type must match the logic in auth/session.js (if we remove the // password field from the object there, we must do the same here). Ideally, // these two things would live in the same place: // https://github.com/wasp-lang/wasp/issues/965 -export type DeserializedAuthEntity = Expand & { +export type DeserializedAuthIdentity = Expand & { providerData: Omit | Omit | OAuthProviderData }> export type SanitizedUser = User & { auth: Auth & { - identities: DeserializedAuthEntity[] + identities: DeserializedAuthIdentity[] } | null } diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/jwt.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/jwt.ts new file mode 100644 index 0000000000..5d2f4ae6fa --- /dev/null +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/jwt.ts @@ -0,0 +1,12 @@ +import jwt from 'jsonwebtoken' +import util from 'util' + +import config from '../config.js' + +const jwtSign = util.promisify(jwt.sign) +const jwtVerify = util.promisify(jwt.verify) + +const JWT_SECRET = config.auth.jwtSecret + +export const signData = (data, options) => jwtSign(data, JWT_SECRET, options) +export const verify = (token) => jwtVerify(token, JWT_SECRET) diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/lucia.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/lucia.ts new file mode 100644 index 0000000000..bb9135c243 --- /dev/null +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/lucia.ts @@ -0,0 +1,55 @@ +import { Lucia } from "lucia"; +import { PrismaAdapter } from "@lucia-auth/adapter-prisma"; +import prisma from '../dbClient.js' +import config from '../config.js' +import { type User } from "../entities/index.js" + +const prismaAdapter = new PrismaAdapter( + // Using `as any` here since Lucia's model types are not compatible with Prisma 4 + // model types. This is a temporary workaround until we migrate to Prisma 5. + // This **works** in runtime, but Typescript complains about it. + prisma.session as any, + prisma.auth as any +); + +/** + * We are using Lucia for session management. + * + * Some details: + * 1. We are using the Prisma adapter for Lucia. + * 2. We are not using cookies for session management. Instead, we are using + * the Authorization header to send the session token. + * 3. Our `Session` entity is connected to the `Auth` entity. + * 4. We are exposing the `userId` field from the `Auth` entity to + * make fetching the User easier. + */ +export const auth = new Lucia<{}, { + userId: User['id'] +}>(prismaAdapter, { + // Since we are not using cookies, we don't need to set any cookie options. + // But in the future, if we decide to use cookies, we can set them here. + + // sessionCookie: { + // name: "session", + // expires: true, + // attributes: { + // secure: !config.isDevelopment, + // sameSite: "lax", + // }, + // }, + getUserAttributes({ userId }) { + return { + userId, + }; + }, +}); + +declare module "lucia" { + interface Register { + Lucia: typeof auth; + DatabaseSessionAttributes: {}; + DatabaseUserAttributes: { + userId: User['id'] + }; + } +} diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/password.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/password.ts new file mode 100644 index 0000000000..a359892b5e --- /dev/null +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/password.ts @@ -0,0 +1,15 @@ +import SecurePassword from 'secure-password' + +const SP = new SecurePassword() + +export const hashPassword = async (password: string): Promise => { + const hashedPwdBuffer = await SP.hash(Buffer.from(password)) + return hashedPwdBuffer.toString("base64") +} + +export const verifyPassword = async (hashedPassword: string, password: string): Promise => { + const result = await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64")) + if (result !== SecurePassword.VALID) { + throw new Error('Invalid password.') + } +} diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/config/google.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/config/google.ts index 0525771050..bb1c95b470 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/config/google.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/config/google.ts @@ -5,7 +5,7 @@ import { makeOAuthInit } from "../oauth/init.js"; import type { ProviderConfig } from "../types.js"; import type { OAuthConfig } from "../oauth/types.js"; -const _waspGetUserFieldsFn = undefined +const _waspUserSignupFields = undefined const _waspUserDefinedConfigFn = undefined const _waspOAuthConfig: OAuthConfig = { @@ -19,7 +19,7 @@ const _waspConfig: ProviderConfig = { displayName: "Google", init: makeOAuthInit({ npmPackage: 'passport-google-oauth20', - getUserFieldsFn: _waspGetUserFieldsFn, + userSignupFields: _waspUserSignupFields, userDefinedConfigFn: _waspUserDefinedConfigFn, oAuthConfig: _waspOAuthConfig, }), diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/createRouter.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/createRouter.ts index a64162b8dd..3b34263c34 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/createRouter.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/createRouter.ts @@ -11,20 +11,22 @@ import { authConfig, contextWithUserEntity, createUser, - findAuthWithUserBy, - createAuthToken, rethrowPossibleAuthError, sanitizeAndSerializeProviderData, + validateAndGetUserFields, } from "../../utils.js" -import { type User } from "../../../entities/index.js" -import type { ProviderConfig, RequestWithWasp } from "../types.js" -import type { GetUserFieldsFn } from "./types.js" +import { createSession } from "../../session.js" +import { type Auth } from "../../../entities/index.js" +import type { ProviderConfig, RequestWithWasp, UserSignupFields } from "../types.js" import { handleRejection } from "../../../utils.js" // For oauth providers, we have an endpoint /login to get the auth URL, // and the /callback endpoint which is used to get the actual access_token and the user info. -export function createRouter(provider: ProviderConfig, initData: { passportStrategyName: string, getUserFieldsFn?: GetUserFieldsFn }) { - const { passportStrategyName, getUserFieldsFn } = initData; +export function createRouter(provider: ProviderConfig, initData: { + passportStrategyName: string, + userSignupFields?: UserSignupFields, +}) { + const { passportStrategyName, userSignupFields } = initData; const router = Router(); @@ -52,9 +54,11 @@ export function createRouter(provider: ProviderConfig, initData: { passportStrat const providerId = createProviderId(provider.id, providerProfile.id); try { - const userId = await getUserIdFromProviderDetails(providerId, providerProfile, getUserFieldsFn) - const token = await createAuthToken(userId) - res.json({ token }) + const authId = await getAuthIdFromProviderDetails(providerId, providerProfile, userSignupFields) + const session = await createSession(authId) + return res.json({ + sessionId: session.id, + }) } catch (e) { rethrowPossibleAuthError(e) } @@ -66,11 +70,11 @@ export function createRouter(provider: ProviderConfig, initData: { passportStrat // We need a user id to create the auth token, so we either find an existing user // or create a new one if none exists for this provider. -async function getUserIdFromProviderDetails( +async function getAuthIdFromProviderDetails( providerId: ProviderId, providerProfile: any, - getUserFieldsFn?: GetUserFieldsFn, -): Promise { + userSignupFields?: UserSignupFields, +): Promise { const existingAuthIdentity = await prisma.authIdentity.findUnique({ where: { providerName_providerUserId: providerId, @@ -85,11 +89,12 @@ async function getUserIdFromProviderDetails( }) if (existingAuthIdentity) { - return existingAuthIdentity.auth.user.id + return existingAuthIdentity.auth.id } else { - const userFields = getUserFieldsFn - ? await getUserFieldsFn(contextWithUserEntity, { profile: providerProfile }) - : {}; + const userFields = await validateAndGetUserFields( + { profile: providerProfile }, + userSignupFields, + ); // For now, we don't have any extra data for the oauth providers, so we just pass an empty object. const providerData = await sanitizeAndSerializeProviderData({}) @@ -97,9 +102,11 @@ async function getUserIdFromProviderDetails( const user = await createUser( providerId, providerData, - userFields, + // Using any here because we want to avoid TypeScript errors and + // rely on Prisma to validate the data. + userFields as any, ) - return user.id + return user.auth.id } } diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/init.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/init.ts index ac5a56dafe..1462f3a2f7 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/init.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/init.ts @@ -2,10 +2,10 @@ import passport from "passport"; import waspServerConfig from '../../../config.js'; -import type { InitData, ProviderConfig, RequestWithWasp } from "../types.js"; -import type { OAuthConfig, GetUserFieldsFn, UserDefinedConfigFn } from "./types.js"; +import type { InitData, ProviderConfig, RequestWithWasp, UserSignupFields } from "../types.js"; +import type { OAuthConfig, UserDefinedConfigFn } from "./types.js"; -export function makeOAuthInit({ userDefinedConfigFn, getUserFieldsFn, npmPackage, oAuthConfig }: OAuthImports) { +export function makeOAuthInit({ userDefinedConfigFn, userSignupFields, npmPackage, oAuthConfig }: OAuthImports) { return async function init(provider: ProviderConfig): Promise { const userDefinedConfig = userDefinedConfigFn ? userDefinedConfigFn() @@ -35,7 +35,7 @@ export function makeOAuthInit({ userDefinedConfigFn, getUserFieldsFn, npmPackage return { passportStrategyName, - getUserFieldsFn, + userSignupFields, }; } } @@ -72,5 +72,5 @@ export type OAuthImports = { npmPackage: string; userDefinedConfigFn?: UserDefinedConfigFn; oAuthConfig: OAuthConfig; - getUserFieldsFn?: GetUserFieldsFn; + userSignupFields?: UserSignupFields; }; diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/types.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/types.ts index ca1e7a3f50..3b673a4858 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/types.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/types.ts @@ -11,8 +11,3 @@ export type OAuthConfig = { export type UserFieldsFromOAuthSignup = Prisma.UserCreateInput export type UserDefinedConfigFn = () => { [key: string]: any } - -export type GetUserFieldsFn = ( - context: typeof contextWithUserEntity, - args: { profile: { [key: string]: any } }, -) => Promise diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/types.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/types.ts index 9defb94486..f19198ca86 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/types.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/types.ts @@ -23,16 +23,18 @@ export type InitData = { export type RequestWithWasp = Request & { wasp?: { [key: string]: any } } -export type PossibleAdditionalSignupFields = Expand> +export type PossibleUserFields = Expand> -export function defineAdditionalSignupFields(config: { - [key in keyof PossibleAdditionalSignupFields]: FieldGetter< - PossibleAdditionalSignupFields[key] +export type UserSignupFields = { + [key in keyof PossibleUserFields]: FieldGetter< + PossibleUserFields[key] > -}) { - return config } type FieldGetter = ( data: { [key: string]: unknown } ) => Promise | T | undefined + +export function defineUserSignupFields(fields: UserSignupFields) { + return fields +} diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/session.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/session.ts new file mode 100644 index 0000000000..ed9154120b --- /dev/null +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/session.ts @@ -0,0 +1,107 @@ +import { Request as ExpressRequest } from "express"; + +import { type User } from "../entities/index.js" +import { type SanitizedUser } from '../server/_types/index.js' + +import { auth } from "./lucia.js"; +import type { Session } from "lucia"; +import { + throwInvalidCredentialsError, + deserializeAndSanitizeProviderData, +} from "./utils.js"; + +import prisma from '../server/dbClient.js' + +// Creates a new session for the `authId` in the database +export async function createSession(authId: string): Promise { + return auth.createSession(authId, {}); +} + +export async function getSessionAndUserFromBearerToken(req: ExpressRequest): Promise<{ + user: SanitizedUser | null, + session: Session | null, +}> { + const authorizationHeader = req.headers["authorization"]; + + if (typeof authorizationHeader !== "string") { + return { + user: null, + session: null, + }; + } + + const sessionId = auth.readBearerToken(authorizationHeader); + if (!sessionId) { + return { + user: null, + session: null, + }; + } + + return getSessionAndUserFromSessionId(sessionId); +} + +export async function getSessionAndUserFromSessionId(sessionId: string): Promise<{ + user: SanitizedUser | null, + session: Session | null, +}> { + const { session, user: authEntity } = await auth.validateSession(sessionId); + + if (!session || !authEntity) { + return { + user: null, + session: null, + }; + } + + return { + session, + user: await getUser(authEntity.userId) + } +} + +async function getUser(userId: User['id']): Promise { + const user = await prisma.user + .findUnique({ + where: { id: userId }, + include: { + auth: { + include: { + identities: true + } + } + } + }) + + if (!user) { + throwInvalidCredentialsError() + } + + // TODO: This logic must match the type in _types/index.ts (if we remove the + // password field from the object here, we must to do the same there). + // Ideally, these two things would live in the same place: + // https://github.com/wasp-lang/wasp/issues/965 + const deserializedIdentities = user.auth.identities.map((identity) => { + const deserializedProviderData = deserializeAndSanitizeProviderData( + identity.providerData, + { + shouldRemovePasswordField: true, + } + ) + return { + ...identity, + providerData: deserializedProviderData, + } + }) + return { + ...user, + auth: { + ...user.auth, + identities: deserializedIdentities, + }, + } +} + +export function invalidateSession(sessionId: string): Promise { + return auth.invalidateSession(sessionId); +} diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/user.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/user.ts index a5d987fc4e..410e92058b 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/user.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/user.ts @@ -2,7 +2,7 @@ // We have them duplicated in this file and in data/Generator/templates/react-app/src/auth/user.ts // If you are changing the logic here, make sure to change it there as well. -import type { SanitizedUser as User, ProviderName, DeserializedAuthEntity } from '../_types/index' +import type { SanitizedUser as User, ProviderName, DeserializedAuthIdentity } from '../_types/index' export function getEmail(user: User): string | null { return findUserIdentity(user, "email")?.providerUserId ?? null; @@ -20,7 +20,7 @@ export function getFirstProviderUserId(user?: User): string | null { return user.auth.identities[0].providerUserId ?? null; } -export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthEntity | undefined { +export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthIdentity | undefined { return user.auth.identities.find( (identity) => identity.providerName === providerName ); diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/utils.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/utils.ts index ba1ad7074c..dfde8ac98b 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/utils.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/utils.ts @@ -1,4 +1,5 @@ -import { hashPassword, sign, verify } from '../core/auth.js' +import { hashPassword } from './password.js' +import { verify } from './jwt.js' import AuthError from '../core/AuthError.js' import HttpError from '../core/HttpError.js' import prisma from '../dbClient.js' @@ -12,9 +13,7 @@ import { Prisma } from '@prisma/client'; import { throwValidationError } from './validation.js' - -import { defineAdditionalSignupFields, type PossibleAdditionalSignupFields } from './providers/types.js' -const _waspAdditionalSignupFieldsConfig = {} as ReturnType +import { type UserSignupFields, type PossibleUserFields } from './providers/types.js' export type EmailProviderData = { hashedPassword: string; @@ -127,8 +126,10 @@ export async function findAuthWithUserBy( export async function createUser( providerId: ProviderId, serializedProviderData?: string, - userFields?: PossibleAdditionalSignupFields, -): Promise { + userFields?: PossibleUserFields, +): Promise { return prisma.user.create({ data: { // Using any here to prevent type errors when userFields are not @@ -145,7 +146,12 @@ export async function createUser( }, } }, - } + }, + // We need to include the Auth entity here because we need `authId` + // to be able to create a session. + include: { + auth: true, + }, }) } @@ -155,12 +161,6 @@ export async function deleteUserByAuthId(authId: string): Promise<{ count: numbe } } }) } -export async function createAuthToken( - userId: User['id'] -): Promise { - return sign(userId); -} - export async function verifyToken(token: string): Promise { return verify(token); } @@ -224,15 +224,23 @@ export function rethrowPossibleAuthError(e: unknown): void { throw e } -export async function validateAndGetAdditionalFields(data: { - [key: string]: unknown -}): Promise> { +export async function validateAndGetUserFields( + data: { + [key: string]: unknown + }, + userSignupFields?: UserSignupFields, +): Promise> { const { password: _password, ...sanitizedData } = data; const result: Record = {}; - for (const [field, getFieldValue] of Object.entries(_waspAdditionalSignupFieldsConfig)) { + + if (!userSignupFields) { + return result; + } + + for (const [field, getFieldValue] of Object.entries(userSignupFields)) { try { const value = await getFieldValue(sanitizedData) result[field] = value @@ -288,3 +296,7 @@ function providerDataHasPasswordField( ): providerData is { hashedPassword: string } { return 'hashedPassword' in providerData; } + +export function throwInvalidCredentialsError(message?: string): void { + throw new HttpError(401, 'Invalid credentials', { message }) +} diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/core/auth.js b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/core/auth.js index 9c8c03ce91..1d01dd6858 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/core/auth.js +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/core/auth.js @@ -1,23 +1,22 @@ -import jwt from 'jsonwebtoken' -import SecurePassword from 'secure-password' -import util from 'util' import { randomInt } from 'node:crypto' import prisma from '../dbClient.js' import { handleRejection } from '../utils.js' -import HttpError from '../core/HttpError.js' -import config from '../config.js' -import { deserializeAndSanitizeProviderData } from '../auth/utils.js' - -const jwtSign = util.promisify(jwt.sign) -const jwtVerify = util.promisify(jwt.verify) - -const JWT_SECRET = config.auth.jwtSecret - -export const signData = (data, options) => jwtSign(data, JWT_SECRET, options) -export const sign = (id, options) => signData({ id }, options) -export const verify = (token) => jwtVerify(token, JWT_SECRET) - +import { getSessionAndUserFromBearerToken } from '../auth/session.js' +import { throwInvalidCredentialsError } from '../auth/utils.js' + +/** + * Auth middleware + * + * If the request includes an `Authorization` header it will try to authenticate the request, + * otherwise it will let the request through. + * + * - If authentication succeeds it sets `req.sessionId` and `req.user` + * - `req.user` is the user that made the request and it's used in + * all Wasp features that need to know the user that made the request. + * - `req.sessionId` is the ID of the session that authenticated the request. + * - If the request is not authenticated, it throws an error. + */ const auth = handleRejection(async (req, res, next) => { const authHeader = req.get('Authorization') if (!authHeader) { @@ -27,119 +26,16 @@ const auth = handleRejection(async (req, res, next) => { return next() } - if (authHeader.startsWith('Bearer ')) { - const token = authHeader.substring(7, authHeader.length) - req.user = await getUserFromToken(token) - } else { - throwInvalidCredentialsError() - } + const { session, user } = await getSessionAndUserFromBearerToken(req); - next() -}) - -export async function getUserFromToken(token) { - let userIdFromToken - try { - userIdFromToken = (await verify(token)).id - } catch (error) { - if (['TokenExpiredError', 'JsonWebTokenError', 'NotBeforeError'].includes(error.name)) { - throwInvalidCredentialsError() - } else { - throw error - } - } - - const user = await prisma.user - .findUnique({ - where: { id: userIdFromToken }, - include: { - auth: { - include: { - identities: true - } - } - } - }) - if (!user) { + if (!session || !user) { throwInvalidCredentialsError() } - // TODO: This logic must match the type in types/index.ts (if we remove the - // password field from the object here, we must to do the same there). - // Ideally, these two things would live in the same place: - // https://github.com/wasp-lang/wasp/issues/965 - let sanitizedUser = { ...user } - sanitizedUser.auth.identities = sanitizedUser.auth.identities.map(identity => { - identity.providerData = deserializeAndSanitizeProviderData(identity.providerData, { shouldRemovePasswordField: true }) - return identity - }); - return sanitizedUser -} - -const SP = new SecurePassword() - -export const hashPassword = async (password) => { - const hashedPwdBuffer = await SP.hash(Buffer.from(password)) - return hashedPwdBuffer.toString("base64") -} - -export const verifyPassword = async (hashedPassword, password) => { - const result = await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64")) - if (result !== SecurePassword.VALID) { - throw new Error('Invalid password.') - } -} + req.sessionId = session.id + req.user = user -// Generates an unused username that looks similar to "quick-purple-sheep-91231". -// It generates several options and ensures it picks one that is not currently in use. -export function generateAvailableDictionaryUsername() { - const adjectives = ['fuzzy', 'tall', 'short', 'nice', 'happy', 'quick', 'slow', 'good', 'new', 'old', 'first', 'last', 'old', 'young'] - const colors = ['red', 'green', 'blue', 'white', 'black', 'brown', 'purple', 'orange', 'yellow'] - const nouns = ['wasp', 'cat', 'dog', 'lion', 'rabbit', 'duck', 'pig', 'bee', 'goat', 'crab', 'fish', 'chicken', 'horse', 'llama', 'camel', 'sheep'] - - const potentialUsernames = [] - for (let i = 0; i < 10; i++) { - const potentialUsername = `${adjectives[randomInt(adjectives.length)]}-${colors[randomInt(colors.length)]}-${nouns[randomInt(nouns.length)]}-${randomInt(100_000)}` - potentialUsernames.push(potentialUsername) - } - - return findAvailableUsername(potentialUsernames) -} - -// Generates an unused username based on an array of username segments and a separator. -// It generates several options and ensures it picks one that is not currently in use. -export function generateAvailableUsername(usernameSegments, config) { - const separator = config?.separator || '-' - const baseUsername = usernameSegments.join(separator) - - const potentialUsernames = [] - for (let i = 0; i < 10; i++) { - const potentialUsername = `${baseUsername}${separator}${randomInt(100_000)}` - potentialUsernames.push(potentialUsername) - } - - return findAvailableUsername(potentialUsernames) -} - -// Checks the database for an unused username from an array provided and returns first. -async function findAvailableUsername(potentialUsernames) { - const users = await prisma.user.findMany({ - where: { - username: { in: potentialUsernames }, - } - }) - const takenUsernames = users.map(user => user.username) - const availableUsernames = potentialUsernames.filter(username => !takenUsernames.includes(username)) - - if (availableUsernames.length === 0) { - throw new Error('Unable to generate a unique username. Please contact Wasp.') - } - - return availableUsernames[0] -} - -export function throwInvalidCredentialsError(message) { - throw new HttpError(401, 'Invalid credentials', { message }) -} + next() +}) export default auth diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/crud/tasks.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/crud/tasks.ts index e2de0a4a3a..f862fe505b 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/crud/tasks.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/crud/tasks.ts @@ -10,7 +10,7 @@ import { Payload } from "../_types/serialization.js"; import type { Task, } from "../entities"; -import { throwInvalidCredentialsError } from "../core/auth.js"; +import { throwInvalidCredentialsError } from '../auth/utils.js' type _WaspEntityTagged = _Task type _WaspEntity = Task diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/email/core/providers/dummy.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/email/core/providers/dummy.ts deleted file mode 100644 index b4b3ef0450..0000000000 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/email/core/providers/dummy.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { EmailSender } from "../types.js"; -import { getDefaultFromField } from "../helpers.js"; - -export function initDummyEmailSender(): EmailSender { - const defaultFromField = getDefaultFromField(); - return { - send: async (email) => { - const fromField = email.from || defaultFromField; - console.log('Test email (not sent):', { - from: { - email: fromField.email, - name: fromField.name, - }, - to: email.to, - subject: email.subject, - text: email.text, - html: email.html, - }); - return { - success: true, - }; - } - } -} diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/email/core/types.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/email/core/types.ts index 9a2440038a..f5274ac498 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/email/core/types.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/email/core/types.ts @@ -1,4 +1,4 @@ -export type EmailProvider = SMTPEmailProvider | SendGridProvider | MailgunEmailProvider; +export type EmailProvider = SMTPEmailProvider | SendGridProvider | MailgunEmailProvider | DummyEmailProvider; export type SMTPEmailProvider = { type: "smtp"; @@ -19,6 +19,10 @@ export type MailgunEmailProvider = { domain: string; }; +export type DummyEmailProvider = { + type: "dummy"; +} + export type EmailSender = { send: (email: Email) => Promise; }; diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/email/index.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/email/index.ts index 47f4e8e649..4deb2c8c68 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/email/index.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/email/index.ts @@ -1,16 +1,8 @@ import { initEmailSender } from "./core/index.js"; -import waspServerConfig from '../config.js'; -import { initDummyEmailSender } from "./core/providers/dummy.js"; - const emailProvider = { type: "sendgrid", apiKey: process.env.SENDGRID_API_KEY, } as const; -const areEmailsSentInDevelopment = process.env.SEND_EMAILS_IN_DEVELOPMENT === "true"; -const isDummyEmailSenderUsed = waspServerConfig.isDevelopment && !areEmailsSentInDevelopment; - -export const emailSender = isDummyEmailSenderUsed - ? initDummyEmailSender() - : initEmailSender(emailProvider); \ No newline at end of file +export const emailSender = initEmailSender(emailProvider); diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/polyfill.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/polyfill.ts new file mode 100644 index 0000000000..a59302451a --- /dev/null +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/polyfill.ts @@ -0,0 +1,7 @@ +// This is a polyfill for Node.js 18 webcrypto API so Lucia can use it +// for random number generation. + +import { webcrypto } from "node:crypto"; + +// @ts-ignore +globalThis.crypto = webcrypto as Crypto; diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/routes/auth/index.js b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/routes/auth/index.js index 0fe478cd80..b8041270b2 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/routes/auth/index.js +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/routes/auth/index.js @@ -2,12 +2,14 @@ import express from 'express' import auth from '../../core/auth.js' import me from './me.js' +import logout from './logout.js' import providersRouter from '../../auth/providers/index.js' const router = express.Router() router.get('/me', auth, me) +router.post('/logout', auth, logout) router.use('/', providersRouter) export default router diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/routes/auth/logout.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/routes/auth/logout.ts new file mode 100644 index 0000000000..10a24a7bc5 --- /dev/null +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/routes/auth/logout.ts @@ -0,0 +1,12 @@ +import { handleRejection } from '../../utils.js' +import { throwInvalidCredentialsError } from '../../auth/utils.js' +import { invalidateSession } from '../../auth/session.js' + +export default handleRejection(async (req, res) => { + if (req.sessionId) { + await invalidateSession(req.sessionId) + return res.json({ success: true }) + } else { + throwInvalidCredentialsError() + } +}) diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/routes/auth/me.js b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/routes/auth/me.js index b1f2cae496..e84aff221a 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/routes/auth/me.js +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/routes/auth/me.js @@ -1,6 +1,6 @@ import { serialize as superjsonSerialize } from 'superjson' import { handleRejection } from '../../utils.js' -import { throwInvalidCredentialsError } from '../../core/auth.js' +import { throwInvalidCredentialsError } from '../../auth/utils.js' export default handleRejection(async (req, res) => { if (req.user) { diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/server.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/server.ts index 48b5caa83d..b34339f7c4 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/server.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/server.ts @@ -10,6 +10,8 @@ import { startPgBoss } from './jobs/core/pgBoss/pgBoss.js' import './jobs/core/allJobs.js' +import './polyfill.js' + const startServer = async () => { await startPgBoss() diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/types/index.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/types/index.ts index a7f36194fb..b30c92f5ec 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/types/index.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/types/index.ts @@ -12,5 +12,3 @@ export type ServerSetupFnContext = { export type { Application } from 'express' export type { Server } from 'http' -export type { GetUserFieldsFn } from '../auth/providers/oauth/types'; - diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/utils.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/utils.ts index a930149d08..b0744f3129 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/utils.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/utils.ts @@ -8,7 +8,8 @@ import { fileURLToPath } from 'url' import { type SanitizedUser } from './_types/index.js' type RequestWithExtraFields = Request & { - user?: SanitizedUser + user?: SanitizedUser; + sessionId?: string; } /** diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/actions/index.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/actions/index.ts index 5e4dfedd12..7fb2de2f9e 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/actions/index.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/actions/index.ts @@ -42,7 +42,7 @@ export type UpdateQuery = (item: ActionInput, oldData: /** * A public query specifier used for addressing Wasp queries. See our docs for details: - * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates */ export type QuerySpecifier = [Query, ...any[]] @@ -116,7 +116,7 @@ type InternalAction = Action & { * * @param publicOptimisticUpdateDefinition An optimistic update definition * object that's a part of the public API: - * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates * @returns An internally-used optimistic update definition object. */ function translateToInternalDefinition( @@ -260,7 +260,7 @@ function getOptimisticUpdateDefinitionForSpecificItem( * Translates a Wasp query specifier to a query cache key used by React Query. * * @param querySpecifier A query specifier that's a part of the public API: - * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates * @returns A cache key React Query internally uses for addressing queries. */ function getRqQueryKeyFromSpecifier(querySpecifier: QuerySpecifier): QueryKey { diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/api.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/api.ts index d7532f65c6..17e36c1248 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/api.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/api.ts @@ -8,59 +8,60 @@ const api = axios.create({ baseURL: config.apiUrl, }) -const WASP_APP_AUTH_TOKEN_NAME = 'authToken' +const WASP_APP_AUTH_SESSION_ID_NAME = 'sessionId' -let authToken = storage.get(WASP_APP_AUTH_TOKEN_NAME) as string | undefined +let waspAppAuthSessionId = storage.get(WASP_APP_AUTH_SESSION_ID_NAME) as string | undefined -export function setAuthToken(token: string): void { - authToken = token - storage.set(WASP_APP_AUTH_TOKEN_NAME, token) - apiEventsEmitter.emit('authToken.set') +export function setSessionId(sessionId: string): void { + waspAppAuthSessionId = sessionId + storage.set(WASP_APP_AUTH_SESSION_ID_NAME, sessionId) + apiEventsEmitter.emit('sessionId.set') } -export function getAuthToken(): string | undefined { - return authToken +export function getSessionId(): string | undefined { + return waspAppAuthSessionId } -export function clearAuthToken(): void { - authToken = undefined - storage.remove(WASP_APP_AUTH_TOKEN_NAME) - apiEventsEmitter.emit('authToken.clear') +export function clearSessionId(): void { + waspAppAuthSessionId = undefined + storage.remove(WASP_APP_AUTH_SESSION_ID_NAME) + apiEventsEmitter.emit('sessionId.clear') } export function removeLocalUserData(): void { - authToken = undefined + waspAppAuthSessionId = undefined storage.clear() - apiEventsEmitter.emit('authToken.clear') + apiEventsEmitter.emit('sessionId.clear') } api.interceptors.request.use((request) => { - if (authToken) { - request.headers['Authorization'] = `Bearer ${authToken}` + const sessionId = getSessionId() + if (sessionId) { + request.headers['Authorization'] = `Bearer ${sessionId}` } return request }) api.interceptors.response.use(undefined, (error) => { if (error.response?.status === 401) { - clearAuthToken() + clearSessionId() } return Promise.reject(error) }) // This handler will run on other tabs (not the active one calling API functions), -// and will ensure they know about auth token changes. +// and will ensure they know about auth session ID changes. // Ref: https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event // "Note: This won't work on the same page that is making the changes — it is really a way // for other pages on the domain using the storage to sync any changes that are made." window.addEventListener('storage', (event) => { - if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_TOKEN_NAME)) { + if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_SESSION_ID_NAME)) { if (!!event.newValue) { - authToken = event.newValue - apiEventsEmitter.emit('authToken.set') + waspAppAuthSessionId = event.newValue + apiEventsEmitter.emit('sessionId.set') } else { - authToken = undefined - apiEventsEmitter.emit('authToken.clear') + waspAppAuthSessionId = undefined + apiEventsEmitter.emit('sessionId.clear') } } }) diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/api/events.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/api/events.ts index 9a59b366d3..a72e48dda8 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/api/events.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/api/events.ts @@ -3,9 +3,9 @@ import mitt, { Emitter } from 'mitt'; type ApiEvents = { // key: Event name // type: Event payload type - 'authToken.set': void; - 'authToken.clear': void; + 'sessionId.set': void; + 'sessionId.clear': void; }; -// Used to allow API clients to register for auth token change events. +// Used to allow API clients to register for auth session ID change events. export const apiEventsEmitter: Emitter = mitt(); diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/helpers/user.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/helpers/user.ts index 1c6fc500f4..a6b06299ce 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/helpers/user.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/helpers/user.ts @@ -1,8 +1,8 @@ -import { setAuthToken } from '../../api' +import { setSessionId } from '../../api' import { invalidateAndRemoveQueries } from '../../operations/resources' -export async function initSession(token: string): Promise { - setAuthToken(token) +export async function initSession(sessionId: string): Promise { + setSessionId(sessionId) // We need to invalidate queries after login in order to get the correct user // data in the React components (using `useAuth`). // Redirects after login won't work properly without this. diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/logout.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/logout.ts index 44b9e05c33..715f99a49b 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/logout.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/logout.ts @@ -1,9 +1,17 @@ -import { removeLocalUserData } from '../api' +import api, { removeLocalUserData } from '../api' import { invalidateAndRemoveQueries } from '../operations/resources' export default async function logout(): Promise { - removeLocalUserData() - // TODO(filip): We are currently invalidating and removing all the queries, but - // we should remove only the non-public, user-dependent ones. - await invalidateAndRemoveQueries() + try { + await api.post('/auth/logout') + } finally { + // Even if the logout request fails, we still want to remove the local user data + // in case the logout failed because of a network error and the user walked away + // from the computer. + removeLocalUserData() + + // TODO(filip): We are currently invalidating and removing all the queries, but + // we should remove only the non-public, user-dependent ones. + await invalidateAndRemoveQueries() + } } diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/pages/OAuthCodeExchange.jsx b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/pages/OAuthCodeExchange.jsx index 83ffc0ee93..490f76d0ad 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/pages/OAuthCodeExchange.jsx +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/pages/OAuthCodeExchange.jsx @@ -28,7 +28,7 @@ export default function OAuthCodeExchange({ pathToApiServerRouteHandlingOauthRed // This helps us reuse one component for various methods (e.g., Google, Facebook, etc.). const apiServerUrlHandlingOauthRedirect = constructOauthRedirectApiServerUrl(pathToApiServerRouteHandlingOauthRedirect) - exchangeCodeForJwtAndRedirect(history, apiServerUrlHandlingOauthRedirect) + exchangeCodeForSessionIdAndRedirect(history, apiServerUrlHandlingOauthRedirect) return () => { firstRender.current = false } @@ -46,22 +46,22 @@ function constructOauthRedirectApiServerUrl(pathToApiServerRouteHandlingOauthRed return `${config.apiUrl}${pathToApiServerRouteHandlingOauthRedirect}${queryParams}` } -async function exchangeCodeForJwtAndRedirect(history, apiServerUrlHandlingOauthRedirect) { - const token = await exchangeCodeForJwt(apiServerUrlHandlingOauthRedirect) +async function exchangeCodeForSessionIdAndRedirect(history, apiServerUrlHandlingOauthRedirect) { + const sessionId = await exchangeCodeForSessionId(apiServerUrlHandlingOauthRedirect) - if (token !== null) { - await initSession(token) + if (sessionId !== null) { + await initSession(sessionId) history.push('/') } else { - console.error('Error obtaining JWT token') + console.error('Error obtaining session ID') history.push('/login') } } -async function exchangeCodeForJwt(url) { +async function exchangeCodeForSessionId(url) { try { const response = await api.get(url) - return response?.data?.token || null + return response?.data?.sessionId || null } catch (e) { console.error(e) return null diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/types.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/types.ts index 4405410cc7..637a2e13d4 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/types.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/types.ts @@ -1,2 +1,2 @@ // todo(filip): turn into a proper import/path -export type { SanitizedUser as User, ProviderName, DeserializedAuthEntity } from '../../../server/src/_types/' +export type { SanitizedUser as User, ProviderName, DeserializedAuthIdentity } from '../../../server/src/_types/' diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/user.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/user.ts index 5799c71ea7..aa0da24824 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/user.ts +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/user.ts @@ -2,7 +2,7 @@ // We have them duplicated in this file and in data/Generator/templates/server/src/auth/user.ts // If you are changing the logic here, make sure to change it there as well. -import type { User, ProviderName, DeserializedAuthEntity } from './types' +import type { User, ProviderName, DeserializedAuthIdentity } from './types' export function getEmail(user: User): string | null { return findUserIdentity(user, "email")?.providerUserId ?? null; @@ -20,7 +20,7 @@ export function getFirstProviderUserId(user?: User): string | null { return user.auth.identities[0].providerUserId ?? null; } -export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthEntity | undefined { +export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthIdentity | undefined { return user.auth.identities.find( (identity) => identity.providerName === providerName ); diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/files.manifest b/waspc/e2e-test/test-outputs/waspJob-golden/files.manifest index 356ddfd991..91b05f149a 100644 --- a/waspc/e2e-test/test-outputs/waspJob-golden/files.manifest +++ b/waspc/e2e-test/test-outputs/waspJob-golden/files.manifest @@ -31,6 +31,7 @@ waspJob/.wasp/out/server/src/jobs/core/pgBoss/pgBossJob.ts waspJob/.wasp/out/server/src/middleware/globalMiddleware.ts waspJob/.wasp/out/server/src/middleware/index.ts waspJob/.wasp/out/server/src/middleware/operations.ts +waspJob/.wasp/out/server/src/polyfill.ts waspJob/.wasp/out/server/src/queries/types.ts waspJob/.wasp/out/server/src/routes/index.js waspJob/.wasp/out/server/src/routes/operations/index.js diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/.waspchecksums b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/.waspchecksums index 753356375d..f9f7edfd5e 100644 --- a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/.waspchecksums +++ b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/.waspchecksums @@ -216,6 +216,13 @@ ], "864c7492c27f6da1e67645fbc358dc803a168852bfd24f2c4dd13fccf6917b07" ], + [ + [ + "file", + "server/src/polyfill.ts" + ], + "66d3dca514bdd01be402714d0dfe3836e76f612346dea57ee595ae4f3da915cf" + ], [ [ "file", @@ -242,14 +249,14 @@ "file", "server/src/server.ts" ], - "e28a2e72f8a0cedbfba8c6acfbc36b9ae35db9005aa26484d88ef7e7688efa5b" + "d26cf3913b82c3525fe2214e0d94606e36ccb306ca9b036979e003f1e1d44f4b" ], [ [ "file", "server/src/types/index.ts" ], - "1958cfc3e3b5f59490168797e4b8dcdc38f32346e734f90df3fb6baa264b36b5" + "f7621082fc7d8467a0967eb0bd82ff7956052b766e9e82d50584b8de88e0d28a" ], [ [ @@ -375,21 +382,21 @@ "file", "web-app/src/actions/index.ts" ], - "3afb54edb61cbc95a9b2133f9b3bdc460ca97580aca700adad988bf0515ab092" + "607c3311861456ae47c246a950c8e29593f9837a9f5c48923d99cd7fac1ce0bb" ], [ [ "file", "web-app/src/api.ts" ], - "93118387834981574ce1773d33275308e68ef8ca87408a35be8931c44a8889bf" + "850331885230117aa56317186c6d38f696fb1fbd0c56470ff7c6e4f3c1c43104" ], [ [ "file", "web-app/src/api/events.ts" ], - "7220e570cfb823028ad6c076cbcf033d217acfb88537bcac47020f1085757044" + "91ec1889f649b608ca81cab8f048538b9dcc70f49444430b1e5b572af2a4970a" ], [ [ diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/polyfill.ts b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/polyfill.ts new file mode 100644 index 0000000000..a59302451a --- /dev/null +++ b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/polyfill.ts @@ -0,0 +1,7 @@ +// This is a polyfill for Node.js 18 webcrypto API so Lucia can use it +// for random number generation. + +import { webcrypto } from "node:crypto"; + +// @ts-ignore +globalThis.crypto = webcrypto as Crypto; diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/server.ts b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/server.ts index 8c59523a89..01edef6b1b 100644 --- a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/server.ts +++ b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/server.ts @@ -8,6 +8,8 @@ import { startPgBoss } from './jobs/core/pgBoss/pgBoss.js' import './jobs/core/allJobs.js' +import './polyfill.js' + const startServer = async () => { await startPgBoss() diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/types/index.ts b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/types/index.ts index 9540b5dddc..b30c92f5ec 100644 --- a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/types/index.ts +++ b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/types/index.ts @@ -12,4 +12,3 @@ export type ServerSetupFnContext = { export type { Application } from 'express' export type { Server } from 'http' - diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/actions/index.ts b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/actions/index.ts index 5e4dfedd12..7fb2de2f9e 100644 --- a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/actions/index.ts +++ b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/actions/index.ts @@ -42,7 +42,7 @@ export type UpdateQuery = (item: ActionInput, oldData: /** * A public query specifier used for addressing Wasp queries. See our docs for details: - * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates */ export type QuerySpecifier = [Query, ...any[]] @@ -116,7 +116,7 @@ type InternalAction = Action & { * * @param publicOptimisticUpdateDefinition An optimistic update definition * object that's a part of the public API: - * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates * @returns An internally-used optimistic update definition object. */ function translateToInternalDefinition( @@ -260,7 +260,7 @@ function getOptimisticUpdateDefinitionForSpecificItem( * Translates a Wasp query specifier to a query cache key used by React Query. * * @param querySpecifier A query specifier that's a part of the public API: - * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates * @returns A cache key React Query internally uses for addressing queries. */ function getRqQueryKeyFromSpecifier(querySpecifier: QuerySpecifier): QueryKey { diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/api.ts b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/api.ts index d7532f65c6..17e36c1248 100644 --- a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/api.ts +++ b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/api.ts @@ -8,59 +8,60 @@ const api = axios.create({ baseURL: config.apiUrl, }) -const WASP_APP_AUTH_TOKEN_NAME = 'authToken' +const WASP_APP_AUTH_SESSION_ID_NAME = 'sessionId' -let authToken = storage.get(WASP_APP_AUTH_TOKEN_NAME) as string | undefined +let waspAppAuthSessionId = storage.get(WASP_APP_AUTH_SESSION_ID_NAME) as string | undefined -export function setAuthToken(token: string): void { - authToken = token - storage.set(WASP_APP_AUTH_TOKEN_NAME, token) - apiEventsEmitter.emit('authToken.set') +export function setSessionId(sessionId: string): void { + waspAppAuthSessionId = sessionId + storage.set(WASP_APP_AUTH_SESSION_ID_NAME, sessionId) + apiEventsEmitter.emit('sessionId.set') } -export function getAuthToken(): string | undefined { - return authToken +export function getSessionId(): string | undefined { + return waspAppAuthSessionId } -export function clearAuthToken(): void { - authToken = undefined - storage.remove(WASP_APP_AUTH_TOKEN_NAME) - apiEventsEmitter.emit('authToken.clear') +export function clearSessionId(): void { + waspAppAuthSessionId = undefined + storage.remove(WASP_APP_AUTH_SESSION_ID_NAME) + apiEventsEmitter.emit('sessionId.clear') } export function removeLocalUserData(): void { - authToken = undefined + waspAppAuthSessionId = undefined storage.clear() - apiEventsEmitter.emit('authToken.clear') + apiEventsEmitter.emit('sessionId.clear') } api.interceptors.request.use((request) => { - if (authToken) { - request.headers['Authorization'] = `Bearer ${authToken}` + const sessionId = getSessionId() + if (sessionId) { + request.headers['Authorization'] = `Bearer ${sessionId}` } return request }) api.interceptors.response.use(undefined, (error) => { if (error.response?.status === 401) { - clearAuthToken() + clearSessionId() } return Promise.reject(error) }) // This handler will run on other tabs (not the active one calling API functions), -// and will ensure they know about auth token changes. +// and will ensure they know about auth session ID changes. // Ref: https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event // "Note: This won't work on the same page that is making the changes — it is really a way // for other pages on the domain using the storage to sync any changes that are made." window.addEventListener('storage', (event) => { - if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_TOKEN_NAME)) { + if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_SESSION_ID_NAME)) { if (!!event.newValue) { - authToken = event.newValue - apiEventsEmitter.emit('authToken.set') + waspAppAuthSessionId = event.newValue + apiEventsEmitter.emit('sessionId.set') } else { - authToken = undefined - apiEventsEmitter.emit('authToken.clear') + waspAppAuthSessionId = undefined + apiEventsEmitter.emit('sessionId.clear') } } }) diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/api/events.ts b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/api/events.ts index 9a59b366d3..a72e48dda8 100644 --- a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/api/events.ts +++ b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/api/events.ts @@ -3,9 +3,9 @@ import mitt, { Emitter } from 'mitt'; type ApiEvents = { // key: Event name // type: Event payload type - 'authToken.set': void; - 'authToken.clear': void; + 'sessionId.set': void; + 'sessionId.clear': void; }; -// Used to allow API clients to register for auth token change events. +// Used to allow API clients to register for auth session ID change events. export const apiEventsEmitter: Emitter = mitt(); diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/files.manifest b/waspc/e2e-test/test-outputs/waspMigrate-golden/files.manifest index a8c4f8eeb5..3738a312e8 100644 --- a/waspc/e2e-test/test-outputs/waspMigrate-golden/files.manifest +++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/files.manifest @@ -29,6 +29,7 @@ waspMigrate/.wasp/out/server/src/entities/index.ts waspMigrate/.wasp/out/server/src/middleware/globalMiddleware.ts waspMigrate/.wasp/out/server/src/middleware/index.ts waspMigrate/.wasp/out/server/src/middleware/operations.ts +waspMigrate/.wasp/out/server/src/polyfill.ts waspMigrate/.wasp/out/server/src/queries/types.ts waspMigrate/.wasp/out/server/src/routes/index.js waspMigrate/.wasp/out/server/src/routes/operations/index.js diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/.waspchecksums b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/.waspchecksums index a136b28147..4d50ee0978 100644 --- a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/.waspchecksums +++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/.waspchecksums @@ -174,6 +174,13 @@ ], "864c7492c27f6da1e67645fbc358dc803a168852bfd24f2c4dd13fccf6917b07" ], + [ + [ + "file", + "server/src/polyfill.ts" + ], + "66d3dca514bdd01be402714d0dfe3836e76f612346dea57ee595ae4f3da915cf" + ], [ [ "file", @@ -200,14 +207,14 @@ "file", "server/src/server.ts" ], - "1c5af223cf0309b341e87cf8b6afd58b0cb21217e64cd9ee498048136a9da5be" + "d0666b659cdc75db181ea2bbb50c4e157f0a7fbe00c4ff8fda0933b1a13e5a0e" ], [ [ "file", "server/src/types/index.ts" ], - "1958cfc3e3b5f59490168797e4b8dcdc38f32346e734f90df3fb6baa264b36b5" + "f7621082fc7d8467a0967eb0bd82ff7956052b766e9e82d50584b8de88e0d28a" ], [ [ @@ -333,21 +340,21 @@ "file", "web-app/src/actions/index.ts" ], - "3afb54edb61cbc95a9b2133f9b3bdc460ca97580aca700adad988bf0515ab092" + "607c3311861456ae47c246a950c8e29593f9837a9f5c48923d99cd7fac1ce0bb" ], [ [ "file", "web-app/src/api.ts" ], - "93118387834981574ce1773d33275308e68ef8ca87408a35be8931c44a8889bf" + "850331885230117aa56317186c6d38f696fb1fbd0c56470ff7c6e4f3c1c43104" ], [ [ "file", "web-app/src/api/events.ts" ], - "7220e570cfb823028ad6c076cbcf033d217acfb88537bcac47020f1085757044" + "91ec1889f649b608ca81cab8f048538b9dcc70f49444430b1e5b572af2a4970a" ], [ [ diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/polyfill.ts b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/polyfill.ts new file mode 100644 index 0000000000..a59302451a --- /dev/null +++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/polyfill.ts @@ -0,0 +1,7 @@ +// This is a polyfill for Node.js 18 webcrypto API so Lucia can use it +// for random number generation. + +import { webcrypto } from "node:crypto"; + +// @ts-ignore +globalThis.crypto = webcrypto as Crypto; diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/server.ts b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/server.ts index 1df46663f5..fff57f199b 100644 --- a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/server.ts +++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/server.ts @@ -6,6 +6,8 @@ import config from './config.js' +import './polyfill.js' + const startServer = async () => { const port = normalizePort(config.port) diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/types/index.ts b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/types/index.ts index 9540b5dddc..b30c92f5ec 100644 --- a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/types/index.ts +++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/types/index.ts @@ -12,4 +12,3 @@ export type ServerSetupFnContext = { export type { Application } from 'express' export type { Server } from 'http' - diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/actions/index.ts b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/actions/index.ts index 5e4dfedd12..7fb2de2f9e 100644 --- a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/actions/index.ts +++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/actions/index.ts @@ -42,7 +42,7 @@ export type UpdateQuery = (item: ActionInput, oldData: /** * A public query specifier used for addressing Wasp queries. See our docs for details: - * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates */ export type QuerySpecifier = [Query, ...any[]] @@ -116,7 +116,7 @@ type InternalAction = Action & { * * @param publicOptimisticUpdateDefinition An optimistic update definition * object that's a part of the public API: - * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates * @returns An internally-used optimistic update definition object. */ function translateToInternalDefinition( @@ -260,7 +260,7 @@ function getOptimisticUpdateDefinitionForSpecificItem( * Translates a Wasp query specifier to a query cache key used by React Query. * * @param querySpecifier A query specifier that's a part of the public API: - * https://wasp-lang.dev/docs/language/features#the-useaction-hook. + * https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates * @returns A cache key React Query internally uses for addressing queries. */ function getRqQueryKeyFromSpecifier(querySpecifier: QuerySpecifier): QueryKey { diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/api.ts b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/api.ts index d7532f65c6..17e36c1248 100644 --- a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/api.ts +++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/api.ts @@ -8,59 +8,60 @@ const api = axios.create({ baseURL: config.apiUrl, }) -const WASP_APP_AUTH_TOKEN_NAME = 'authToken' +const WASP_APP_AUTH_SESSION_ID_NAME = 'sessionId' -let authToken = storage.get(WASP_APP_AUTH_TOKEN_NAME) as string | undefined +let waspAppAuthSessionId = storage.get(WASP_APP_AUTH_SESSION_ID_NAME) as string | undefined -export function setAuthToken(token: string): void { - authToken = token - storage.set(WASP_APP_AUTH_TOKEN_NAME, token) - apiEventsEmitter.emit('authToken.set') +export function setSessionId(sessionId: string): void { + waspAppAuthSessionId = sessionId + storage.set(WASP_APP_AUTH_SESSION_ID_NAME, sessionId) + apiEventsEmitter.emit('sessionId.set') } -export function getAuthToken(): string | undefined { - return authToken +export function getSessionId(): string | undefined { + return waspAppAuthSessionId } -export function clearAuthToken(): void { - authToken = undefined - storage.remove(WASP_APP_AUTH_TOKEN_NAME) - apiEventsEmitter.emit('authToken.clear') +export function clearSessionId(): void { + waspAppAuthSessionId = undefined + storage.remove(WASP_APP_AUTH_SESSION_ID_NAME) + apiEventsEmitter.emit('sessionId.clear') } export function removeLocalUserData(): void { - authToken = undefined + waspAppAuthSessionId = undefined storage.clear() - apiEventsEmitter.emit('authToken.clear') + apiEventsEmitter.emit('sessionId.clear') } api.interceptors.request.use((request) => { - if (authToken) { - request.headers['Authorization'] = `Bearer ${authToken}` + const sessionId = getSessionId() + if (sessionId) { + request.headers['Authorization'] = `Bearer ${sessionId}` } return request }) api.interceptors.response.use(undefined, (error) => { if (error.response?.status === 401) { - clearAuthToken() + clearSessionId() } return Promise.reject(error) }) // This handler will run on other tabs (not the active one calling API functions), -// and will ensure they know about auth token changes. +// and will ensure they know about auth session ID changes. // Ref: https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event // "Note: This won't work on the same page that is making the changes — it is really a way // for other pages on the domain using the storage to sync any changes that are made." window.addEventListener('storage', (event) => { - if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_TOKEN_NAME)) { + if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_SESSION_ID_NAME)) { if (!!event.newValue) { - authToken = event.newValue - apiEventsEmitter.emit('authToken.set') + waspAppAuthSessionId = event.newValue + apiEventsEmitter.emit('sessionId.set') } else { - authToken = undefined - apiEventsEmitter.emit('authToken.clear') + waspAppAuthSessionId = undefined + apiEventsEmitter.emit('sessionId.clear') } } }) diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/api/events.ts b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/api/events.ts index 9a59b366d3..a72e48dda8 100644 --- a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/api/events.ts +++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/api/events.ts @@ -3,9 +3,9 @@ import mitt, { Emitter } from 'mitt'; type ApiEvents = { // key: Event name // type: Event payload type - 'authToken.set': void; - 'authToken.clear': void; + 'sessionId.set': void; + 'sessionId.clear': void; }; -// Used to allow API clients to register for auth token change events. +// Used to allow API clients to register for auth session ID change events. export const apiEventsEmitter: Emitter = mitt(); diff --git a/waspc/examples/crud-testing/main.wasp b/waspc/examples/crud-testing/main.wasp index 1e1807d030..d75c9a86b0 100644 --- a/waspc/examples/crud-testing/main.wasp +++ b/waspc/examples/crud-testing/main.wasp @@ -9,12 +9,11 @@ app crudTesting { auth: { userEntity: User, methods: { - usernameAndPassword: {}, + usernameAndPassword: { + userSignupFields: import { userSignupFields } from "@server/auth.js", + }, }, onAuthFailedRedirectTo: "/login", - signup: { - additionalFields: import { fields } from "@server/auth.js", - }, }, dependencies: [ ("zod", "^3.22.2") diff --git a/waspc/examples/crud-testing/migrations/20231106110220_initial/migration.sql b/waspc/examples/crud-testing/migrations/20231106110220_initial/migration.sql deleted file mode 100644 index 08b9d8d3a0..0000000000 --- a/waspc/examples/crud-testing/migrations/20231106110220_initial/migration.sql +++ /dev/null @@ -1,24 +0,0 @@ --- CreateTable -CREATE TABLE "User" ( - "id" SERIAL NOT NULL, - "username" TEXT NOT NULL, - "password" TEXT NOT NULL, - "address" TEXT, - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Task" ( - "id" SERIAL NOT NULL, - "title" TEXT NOT NULL, - "userId" INTEGER NOT NULL, - - CONSTRAINT "Task_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); - --- AddForeignKey -ALTER TABLE "Task" ADD CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/waspc/examples/crud-testing/migrations/20231124155039_add_auth/migration.sql b/waspc/examples/crud-testing/migrations/20231124155039_add_auth/migration.sql deleted file mode 100644 index 280d1f5fc5..0000000000 --- a/waspc/examples/crud-testing/migrations/20231124155039_add_auth/migration.sql +++ /dev/null @@ -1,38 +0,0 @@ --- CreateTable -CREATE TABLE "Auth" ( - "id" TEXT NOT NULL, - "email" TEXT, - "username" TEXT, - "password" TEXT, - "isEmailVerified" BOOLEAN NOT NULL DEFAULT false, - "emailVerificationSentAt" TIMESTAMP(3), - "passwordResetSentAt" TIMESTAMP(3), - "userId" INTEGER, - - CONSTRAINT "Auth_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "SocialAuthProvider" ( - "id" TEXT NOT NULL, - "provider" TEXT NOT NULL, - "providerId" TEXT NOT NULL, - "authId" TEXT NOT NULL, - - CONSTRAINT "SocialAuthProvider_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "Auth_email_key" ON "Auth"("email"); - --- CreateIndex -CREATE UNIQUE INDEX "Auth_username_key" ON "Auth"("username"); - --- CreateIndex -CREATE UNIQUE INDEX "Auth_userId_key" ON "Auth"("userId"); - --- AddForeignKey -ALTER TABLE "Auth" ADD CONSTRAINT "Auth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "SocialAuthProvider" ADD CONSTRAINT "SocialAuthProvider_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/waspc/examples/crud-testing/migrations/20231212120120_inital/migration.sql b/waspc/examples/crud-testing/migrations/20231212120120_inital/migration.sql deleted file mode 100644 index dcc286e347..0000000000 --- a/waspc/examples/crud-testing/migrations/20231212120120_inital/migration.sql +++ /dev/null @@ -1,24 +0,0 @@ -/* - Warnings: - - - You are about to drop the `SocialAuthProvider` table. If the table is not empty, all the data it contains will be lost. - -*/ --- DropForeignKey -ALTER TABLE "SocialAuthProvider" DROP CONSTRAINT "SocialAuthProvider_authId_fkey"; - --- DropTable -DROP TABLE "SocialAuthProvider"; - --- CreateTable -CREATE TABLE "AuthIdentity" ( - "providerName" TEXT NOT NULL, - "providerUserId" TEXT NOT NULL, - "providerData" TEXT NOT NULL DEFAULT '{}', - "authId" TEXT NOT NULL, - - CONSTRAINT "AuthIdentity_pkey" PRIMARY KEY ("providerName","providerUserId") -); - --- AddForeignKey -ALTER TABLE "AuthIdentity" ADD CONSTRAINT "AuthIdentity_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/waspc/examples/crud-testing/migrations/20231212132054_random/migration.sql b/waspc/examples/crud-testing/migrations/20231212132054_random/migration.sql deleted file mode 100644 index 90ca6265e3..0000000000 --- a/waspc/examples/crud-testing/migrations/20231212132054_random/migration.sql +++ /dev/null @@ -1,24 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `email` on the `Auth` table. All the data in the column will be lost. - - You are about to drop the column `emailVerificationSentAt` on the `Auth` table. All the data in the column will be lost. - - You are about to drop the column `isEmailVerified` on the `Auth` table. All the data in the column will be lost. - - You are about to drop the column `password` on the `Auth` table. All the data in the column will be lost. - - You are about to drop the column `passwordResetSentAt` on the `Auth` table. All the data in the column will be lost. - - You are about to drop the column `username` on the `Auth` table. All the data in the column will be lost. - -*/ --- DropIndex -DROP INDEX "Auth_email_key"; - --- DropIndex -DROP INDEX "Auth_username_key"; - --- AlterTable -ALTER TABLE "Auth" DROP COLUMN "email", -DROP COLUMN "emailVerificationSentAt", -DROP COLUMN "isEmailVerified", -DROP COLUMN "password", -DROP COLUMN "passwordResetSentAt", -DROP COLUMN "username"; diff --git a/waspc/examples/crud-testing/migrations/20231212132224_thrid/migration.sql b/waspc/examples/crud-testing/migrations/20231212132224_thrid/migration.sql deleted file mode 100644 index 5ad32a2c57..0000000000 --- a/waspc/examples/crud-testing/migrations/20231212132224_thrid/migration.sql +++ /dev/null @@ -1,13 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `password` on the `User` table. All the data in the column will be lost. - - You are about to drop the column `username` on the `User` table. All the data in the column will be lost. - -*/ --- DropIndex -DROP INDEX "User_username_key"; - --- AlterTable -ALTER TABLE "User" DROP COLUMN "password", -DROP COLUMN "username"; diff --git a/waspc/examples/crud-testing/migrations/20240110121223_initial/migration.sql b/waspc/examples/crud-testing/migrations/20240110121223_initial/migration.sql new file mode 100644 index 0000000000..373557995f --- /dev/null +++ b/waspc/examples/crud-testing/migrations/20240110121223_initial/migration.sql @@ -0,0 +1,64 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "address" TEXT, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Task" ( + "id" SERIAL NOT NULL, + "title" TEXT NOT NULL, + "userId" INTEGER NOT NULL, + + CONSTRAINT "Task_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Auth" ( + "id" TEXT NOT NULL, + "userId" INTEGER, + + CONSTRAINT "Auth_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AuthIdentity" ( + "providerName" TEXT NOT NULL, + "providerUserId" TEXT NOT NULL, + "providerData" TEXT NOT NULL DEFAULT '{}', + "authId" TEXT NOT NULL, + + CONSTRAINT "AuthIdentity_pkey" PRIMARY KEY ("providerName","providerUserId") +); + +-- CreateTable +CREATE TABLE "Session" ( + "id" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "userId" TEXT NOT NULL, + + CONSTRAINT "Session_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Auth_userId_key" ON "Auth"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_id_key" ON "Session"("id"); + +-- CreateIndex +CREATE INDEX "Session_userId_idx" ON "Session"("userId"); + +-- AddForeignKey +ALTER TABLE "Task" ADD CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Auth" ADD CONSTRAINT "Auth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AuthIdentity" ADD CONSTRAINT "AuthIdentity_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Auth"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/waspc/examples/crud-testing/src/server/auth.ts b/waspc/examples/crud-testing/src/server/auth.ts index c0b111b628..981fcd33be 100644 --- a/waspc/examples/crud-testing/src/server/auth.ts +++ b/waspc/examples/crud-testing/src/server/auth.ts @@ -1,5 +1,5 @@ import * as z from 'zod' -import { defineAdditionalSignupFields } from '@wasp/auth/index.js' +import { defineUserSignupFields } from '@wasp/auth/index.js' import { ensurePasswordIsPresent, ensureValidPassword, @@ -9,9 +9,8 @@ import prisma from '@wasp/dbClient.js' import { CustomSignup } from '@wasp/actions/types' import { sanitizeAndSerializeProviderData } from '@wasp/auth/utils.js' -export const fields = defineAdditionalSignupFields({ +export const userSignupFields = defineUserSignupFields({ address: (data) => { - console.log('Received data:', data) const AddressSchema = z .string({ required_error: 'Address is required', diff --git a/waspc/examples/crud-testing/src/server/auth_simple.js b/waspc/examples/crud-testing/src/server/auth_simple.js index bf98625829..ebac9fa278 100644 --- a/waspc/examples/crud-testing/src/server/auth_simple.js +++ b/waspc/examples/crud-testing/src/server/auth_simple.js @@ -1,5 +1,5 @@ -import { defineAdditionalSignupFields } from 'wasp/auth/index.js' +import { defineUserSignupFields } from 'wasp/auth/index.js' -export const fields = defineAdditionalSignupFields({ +export const userSignupFields = defineUserSignupFields({ address: (data) => data.address, }) diff --git a/waspc/examples/pg-vector-example/main.wasp b/waspc/examples/pg-vector-example/main.wasp index 8e0c31ca6a..8e869b72d0 100644 --- a/waspc/examples/pg-vector-example/main.wasp +++ b/waspc/examples/pg-vector-example/main.wasp @@ -1,6 +1,6 @@ app pgVectorExample { wasp: { - version: "^0.11.3" + version: "^0.12.0" }, title: "PG Vector Example", dependencies: [ diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/events.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/events.ts index 9a59b366d3..a72e48dda8 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/events.ts +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/events.ts @@ -3,9 +3,9 @@ import mitt, { Emitter } from 'mitt'; type ApiEvents = { // key: Event name // type: Event payload type - 'authToken.set': void; - 'authToken.clear': void; + 'sessionId.set': void; + 'sessionId.clear': void; }; -// Used to allow API clients to register for auth token change events. +// Used to allow API clients to register for auth session ID change events. export const apiEventsEmitter: Emitter = mitt(); diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/index.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/index.ts index 9aad1ead59..8b22dd7ebc 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/index.ts +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/index.ts @@ -8,59 +8,60 @@ const api = axios.create({ baseURL: config.apiUrl, }) -const WASP_APP_AUTH_TOKEN_NAME = 'authToken' +const WASP_APP_AUTH_SESSION_ID_NAME = 'sessionId' -let authToken = storage.get(WASP_APP_AUTH_TOKEN_NAME) as string | undefined +let waspAppAuthSessionId = storage.get(WASP_APP_AUTH_SESSION_ID_NAME) as string | undefined -export function setAuthToken(token: string): void { - authToken = token - storage.set(WASP_APP_AUTH_TOKEN_NAME, token) - apiEventsEmitter.emit('authToken.set') +export function setSessionId(sessionId: string): void { + waspAppAuthSessionId = sessionId + storage.set(WASP_APP_AUTH_SESSION_ID_NAME, sessionId) + apiEventsEmitter.emit('sessionId.set') } -export function getAuthToken(): string | undefined { - return authToken +export function getSessionId(): string | undefined { + return waspAppAuthSessionId } -export function clearAuthToken(): void { - authToken = undefined - storage.remove(WASP_APP_AUTH_TOKEN_NAME) - apiEventsEmitter.emit('authToken.clear') +export function clearSessionId(): void { + waspAppAuthSessionId = undefined + storage.remove(WASP_APP_AUTH_SESSION_ID_NAME) + apiEventsEmitter.emit('sessionId.clear') } export function removeLocalUserData(): void { - authToken = undefined + waspAppAuthSessionId = undefined storage.clear() - apiEventsEmitter.emit('authToken.clear') + apiEventsEmitter.emit('sessionId.clear') } api.interceptors.request.use((request) => { - if (authToken) { - request.headers['Authorization'] = `Bearer ${authToken}` + const sessionId = getSessionId() + if (sessionId) { + request.headers['Authorization'] = `Bearer ${sessionId}` } return request }) api.interceptors.response.use(undefined, (error) => { if (error.response?.status === 401) { - clearAuthToken() + clearSessionId() } return Promise.reject(error) }) // This handler will run on other tabs (not the active one calling API functions), -// and will ensure they know about auth token changes. +// and will ensure they know about auth session ID changes. // Ref: https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event // "Note: This won't work on the same page that is making the changes — it is really a way // for other pages on the domain using the storage to sync any changes that are made." window.addEventListener('storage', (event) => { - if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_TOKEN_NAME)) { + if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_SESSION_ID_NAME)) { if (!!event.newValue) { - authToken = event.newValue - apiEventsEmitter.emit('authToken.set') + waspAppAuthSessionId = event.newValue + apiEventsEmitter.emit('sessionId.set') } else { - authToken = undefined - apiEventsEmitter.emit('authToken.clear') + waspAppAuthSessionId = undefined + apiEventsEmitter.emit('sessionId.clear') } } }) diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/helpers/user.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/helpers/user.ts index c3e6a4072b..498f2588a8 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/helpers/user.ts +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/helpers/user.ts @@ -1,8 +1,8 @@ -import { setAuthToken } from 'wasp/api' +import { setSessionId } from 'wasp/api' import { invalidateAndRemoveQueries } from 'wasp/operations/resources' -export async function initSession(token: string): Promise { - setAuthToken(token) +export async function initSession(sessionId: string): Promise { + setSessionId(sessionId) // We need to invalidate queries after login in order to get the correct user // data in the React components (using `useAuth`). // Redirects after login won't work properly without this. diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/login.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/login.ts index 487b45b981..2b4ec4b9fe 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/login.ts +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/login.ts @@ -6,7 +6,7 @@ export default async function login(username: string, password: string): Promise const args = { username, password } const response = await api.post('/auth/username/login', args) - await initSession(response.data.token) + await initSession(response.data.sessionId) } catch (error) { handleApiError(error) } diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/logout.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/logout.ts index 340e9dec9c..cc41b6989c 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/logout.ts +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/logout.ts @@ -1,9 +1,17 @@ -import { removeLocalUserData } from 'wasp/api' +import api, { removeLocalUserData } from 'wasp/api' import { invalidateAndRemoveQueries } from 'wasp/operations/resources' export default async function logout(): Promise { - removeLocalUserData() - // TODO(filip): We are currently invalidating and removing all the queries, but - // we should remove only the non-public, user-dependent ones. - await invalidateAndRemoveQueries() + try { + await api.post('/auth/logout') + } finally { + // Even if the logout request fails, we still want to remove the local user data + // in case the logout failed because of a network error and the user walked away + // from the computer. + removeLocalUserData() + + // TODO(filip): We are currently invalidating and removing all the queries, but + // we should remove only the non-public, user-dependent ones. + await invalidateAndRemoveQueries() + } } diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/providers/types.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/providers/types.ts index 5bbc99ca83..76e1114850 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/providers/types.ts +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/providers/types.ts @@ -23,16 +23,18 @@ export type InitData = { export type RequestWithWasp = Request & { wasp?: { [key: string]: any } } -export type PossibleAdditionalSignupFields = Expand> +export type PossibleUserFields = Expand> -export function defineAdditionalSignupFields(config: { - [key in keyof PossibleAdditionalSignupFields]: FieldGetter< - PossibleAdditionalSignupFields[key] +export type UserSignupFields = { + [key in keyof PossibleUserFields]: FieldGetter< + PossibleUserFields[key] > -}) { - return config } type FieldGetter = ( data: { [key: string]: unknown } ) => Promise | T | undefined + +export function defineUserSignupFields(fields: UserSignupFields) { + return fields +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/types.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/types.ts index 9240b4e4b0..f9f079a57a 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/types.ts +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/types.ts @@ -1,2 +1,2 @@ // todo(filip): turn into a proper import/path -export type { SanitizedUser as User, ProviderName, DeserializedAuthEntity } from 'wasp/server/_types/' +export type { SanitizedUser as User, ProviderName, DeserializedAuthIdentity } from 'wasp/server/_types/' diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/user.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/user.ts index 5799c71ea7..aa0da24824 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/user.ts +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/user.ts @@ -2,7 +2,7 @@ // We have them duplicated in this file and in data/Generator/templates/server/src/auth/user.ts // If you are changing the logic here, make sure to change it there as well. -import type { User, ProviderName, DeserializedAuthEntity } from './types' +import type { User, ProviderName, DeserializedAuthIdentity } from './types' export function getEmail(user: User): string | null { return findUserIdentity(user, "email")?.providerUserId ?? null; @@ -20,7 +20,7 @@ export function getFirstProviderUserId(user?: User): string | null { return user.auth.identities[0].providerUserId ?? null; } -export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthEntity | undefined { +export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthIdentity | undefined { return user.auth.identities.find( (identity) => identity.providerName === providerName ); diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/utils.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/utils.ts index 7a180abdc6..603e9a4b11 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/utils.ts +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/utils.ts @@ -1,4 +1,5 @@ -import { hashPassword, sign, verify } from 'wasp/core/auth' +import { hashPassword } from './password.js' +import { verify } from './jwt.js' import AuthError from '../core/AuthError.js' import HttpError from '../core/HttpError.js' import prisma from '../server/dbClient.js' @@ -12,9 +13,7 @@ import { Prisma } from '@prisma/client'; import { throwValidationError } from './validation.js' - -import { defineAdditionalSignupFields, type PossibleAdditionalSignupFields } from './providers/types.js' -const _waspAdditionalSignupFieldsConfig = {} as ReturnType +import { type UserSignupFields, type PossibleUserFields } from './providers/types.js' export type EmailProviderData = { hashedPassword: string; @@ -127,8 +126,10 @@ export async function findAuthWithUserBy( export async function createUser( providerId: ProviderId, serializedProviderData?: string, - userFields?: PossibleAdditionalSignupFields, -): Promise { + userFields?: PossibleUserFields, +): Promise { return prisma.user.create({ data: { // Using any here to prevent type errors when userFields are not @@ -145,7 +146,12 @@ export async function createUser( }, } }, - } + }, + // We need to include the Auth entity here because we need `authId` + // to be able to create a session. + include: { + auth: true, + }, }) } @@ -155,12 +161,6 @@ export async function deleteUserByAuthId(authId: string): Promise<{ count: numbe } } }) } -export async function createAuthToken( - userId: User['id'] -): Promise { - return sign(userId); -} - export async function verifyToken(token: string): Promise { return verify(token); } @@ -224,15 +224,23 @@ export function rethrowPossibleAuthError(e: unknown): void { throw e } -export async function validateAndGetAdditionalFields(data: { - [key: string]: unknown -}): Promise> { +export async function validateAndGetUserFields( + data: { + [key: string]: unknown + }, + userSignupFields?: UserSignupFields, +): Promise> { const { password: _password, ...sanitizedData } = data; const result: Record = {}; - for (const [field, getFieldValue] of Object.entries(_waspAdditionalSignupFieldsConfig)) { + + if (!userSignupFields) { + return result; + } + + for (const [field, getFieldValue] of Object.entries(userSignupFields)) { try { const value = await getFieldValue(sanitizedData) result[field] = value @@ -288,3 +296,7 @@ function providerDataHasPasswordField( ): providerData is { hashedPassword: string } { return 'hashedPassword' in providerData; } + +export function throwInvalidCredentialsError(message?: string): void { + throw new HttpError(401, 'Invalid credentials', { message }) +} diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/auth.js b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/auth.js index 75e77a7fb9..6908bfb517 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/auth.js +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/core/auth.js @@ -1,23 +1,22 @@ -import jwt from 'jsonwebtoken' -import SecurePassword from 'secure-password' -import util from 'util' import { randomInt } from 'node:crypto' -import prisma from '@server/dbClient.js' -import { handleRejection } from '../server/utils' -import HttpError from './HttpError.js' -import config from '../config.js' -import { deserializeAndSanitizeProviderData } from 'wasp/auth/utils' - -const jwtSign = util.promisify(jwt.sign) -const jwtVerify = util.promisify(jwt.verify) - -const JWT_SECRET = config.auth.jwtSecret - -export const signData = (data, options) => jwtSign(data, JWT_SECRET, options) -export const sign = (id, options) => signData({ id }, options) -export const verify = (token) => jwtVerify(token, JWT_SECRET) - +import prisma from '../server/dbClient.js' +import { handleRejection } from '../utils.js' +import { getSessionAndUserFromBearerToken } from 'wasp/auth/session' +import { throwInvalidCredentialsError } from 'wasp/auth/utils' + +/** + * Auth middleware + * + * If the request includes an `Authorization` header it will try to authenticate the request, + * otherwise it will let the request through. + * + * - If authentication succeeds it sets `req.sessionId` and `req.user` + * - `req.user` is the user that made the request and it's used in + * all Wasp features that need to know the user that made the request. + * - `req.sessionId` is the ID of the session that authenticated the request. + * - If the request is not authenticated, it throws an error. + */ const auth = handleRejection(async (req, res, next) => { const authHeader = req.get('Authorization') if (!authHeader) { @@ -27,119 +26,16 @@ const auth = handleRejection(async (req, res, next) => { return next() } - if (authHeader.startsWith('Bearer ')) { - const token = authHeader.substring(7, authHeader.length) - req.user = await getUserFromToken(token) - } else { - throwInvalidCredentialsError() - } + const { session, user } = await getSessionAndUserFromBearerToken(req); - next() -}) - -export async function getUserFromToken(token) { - let userIdFromToken - try { - userIdFromToken = (await verify(token)).id - } catch (error) { - if (['TokenExpiredError', 'JsonWebTokenError', 'NotBeforeError'].includes(error.name)) { - throwInvalidCredentialsError() - } else { - throw error - } - } - - const user = await prisma.user - .findUnique({ - where: { id: userIdFromToken }, - include: { - auth: { - include: { - identities: true - } - } - } - }) - if (!user) { + if (!session || !user) { throwInvalidCredentialsError() } - // TODO: This logic must match the type in types/index.ts (if we remove the - // password field from the object here, we must to do the same there). - // Ideally, these two things would live in the same place: - // https://github.com/wasp-lang/wasp/issues/965 - let sanitizedUser = { ...user } - sanitizedUser.auth.identities = sanitizedUser.auth.identities.map(identity => { - identity.providerData = deserializeAndSanitizeProviderData(identity.providerData, { shouldRemovePasswordField: true }) - return identity - }); - return sanitizedUser -} - -const SP = new SecurePassword() - -export const hashPassword = async (password) => { - const hashedPwdBuffer = await SP.hash(Buffer.from(password)) - return hashedPwdBuffer.toString("base64") -} - -export const verifyPassword = async (hashedPassword, password) => { - const result = await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64")) - if (result !== SecurePassword.VALID) { - throw new Error('Invalid password.') - } -} + req.sessionId = session.id + req.user = user -// Generates an unused username that looks similar to "quick-purple-sheep-91231". -// It generates several options and ensures it picks one that is not currently in use. -export function generateAvailableDictionaryUsername() { - const adjectives = ['fuzzy', 'tall', 'short', 'nice', 'happy', 'quick', 'slow', 'good', 'new', 'old', 'first', 'last', 'old', 'young'] - const colors = ['red', 'green', 'blue', 'white', 'black', 'brown', 'purple', 'orange', 'yellow'] - const nouns = ['wasp', 'cat', 'dog', 'lion', 'rabbit', 'duck', 'pig', 'bee', 'goat', 'crab', 'fish', 'chicken', 'horse', 'llama', 'camel', 'sheep'] - - const potentialUsernames = [] - for (let i = 0; i < 10; i++) { - const potentialUsername = `${adjectives[randomInt(adjectives.length)]}-${colors[randomInt(colors.length)]}-${nouns[randomInt(nouns.length)]}-${randomInt(100_000)}` - potentialUsernames.push(potentialUsername) - } - - return findAvailableUsername(potentialUsernames) -} - -// Generates an unused username based on an array of username segments and a separator. -// It generates several options and ensures it picks one that is not currently in use. -export function generateAvailableUsername(usernameSegments, config) { - const separator = config?.separator || '-' - const baseUsername = usernameSegments.join(separator) - - const potentialUsernames = [] - for (let i = 0; i < 10; i++) { - const potentialUsername = `${baseUsername}${separator}${randomInt(100_000)}` - potentialUsernames.push(potentialUsername) - } - - return findAvailableUsername(potentialUsernames) -} - -// Checks the database for an unused username from an array provided and returns first. -async function findAvailableUsername(potentialUsernames) { - const users = await prisma.user.findMany({ - where: { - username: { in: potentialUsernames }, - } - }) - const takenUsernames = users.map(user => user.username) - const availableUsernames = potentialUsernames.filter(username => !takenUsernames.includes(username)) - - if (availableUsernames.length === 0) { - throw new Error('Unable to generate a unique username. Please contact Wasp.') - } - - return availableUsernames[0] -} - -export function throwInvalidCredentialsError(message) { - throw new HttpError(401, 'Invalid credentials', { message }) -} + next() +}) export default auth diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json index 13ab00fecf..c30d8034cb 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json @@ -48,7 +48,9 @@ "secure-password": "^4.0.0", "superjson": "^1.12.2", "@types/express-serve-static-core": "^4.17.13", - "@stitches/react": "^1.2.8" + "@stitches/react": "^1.2.8", + "lucia": "^3.0.0-beta.14", + "@lucia-auth/adapter-prisma": "^4.0.0-beta.9" }, "devDependencies": {"@tsconfig/node18": "latest" } diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/index.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/index.ts index a29ec5ad16..141cc718a1 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/index.ts +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/_types/index.ts @@ -81,18 +81,18 @@ type Context = Expand<{ type ContextWithUser = Expand & { user?: SanitizedUser }> -// TODO: This type must match the logic in core/auth.js (if we remove the +// TODO: This type must match the logic in auth/session.js (if we remove the // password field from the object there, we must do the same here). Ideally, // these two things would live in the same place: // https://github.com/wasp-lang/wasp/issues/965 -export type DeserializedAuthEntity = Expand & { +export type DeserializedAuthIdentity = Expand & { providerData: Omit | Omit | OAuthProviderData }> export type SanitizedUser = User & { auth: Auth & { - identities: DeserializedAuthEntity[] + identities: DeserializedAuthIdentity[] } | null } diff --git a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/utils.ts b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/utils.ts index df3052cd57..aa07513822 100644 --- a/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/utils.ts +++ b/waspc/examples/todo-typescript/.wasp/out/sdk/wasp/server/utils.ts @@ -8,7 +8,8 @@ import { fileURLToPath } from 'url' import { type SanitizedUser } from 'wasp/server/_types/index.js' type RequestWithExtraFields = Request & { - user?: SanitizedUser + user?: SanitizedUser; + sessionId?: string; } /** diff --git a/waspc/examples/todo-typescript/cleanstart b/waspc/examples/todo-typescript/cleanstart index 33e58b6caa..e32b3aecac 100755 --- a/waspc/examples/todo-typescript/cleanstart +++ b/waspc/examples/todo-typescript/cleanstart @@ -1,3 +1,3 @@ #!/bin/bash -cabal run wasp-cli reset; cabal run wasp-cli db migrate-dev && ./fix; cabal run wasp-cli start +cabal run wasp-cli clean; cabal run wasp-cli db migrate-dev && ./fix; cabal run wasp-cli start diff --git a/waspc/examples/todo-typescript/migrations/20240110163721_init/migration.sql b/waspc/examples/todo-typescript/migrations/20240110163721_init/migration.sql deleted file mode 100644 index 0ea8e16da6..0000000000 --- a/waspc/examples/todo-typescript/migrations/20240110163721_init/migration.sql +++ /dev/null @@ -1,34 +0,0 @@ --- CreateTable -CREATE TABLE "User" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT -); - --- CreateTable -CREATE TABLE "Task" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "description" TEXT NOT NULL, - "isDone" BOOLEAN NOT NULL DEFAULT false, - "userId" INTEGER NOT NULL, - CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE -); - --- CreateTable -CREATE TABLE "Auth" ( - "id" TEXT NOT NULL PRIMARY KEY, - "userId" INTEGER, - CONSTRAINT "Auth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE -); - --- CreateTable -CREATE TABLE "AuthIdentity" ( - "providerName" TEXT NOT NULL, - "providerUserId" TEXT NOT NULL, - "providerData" TEXT NOT NULL DEFAULT '{}', - "authId" TEXT NOT NULL, - - PRIMARY KEY ("providerName", "providerUserId"), - CONSTRAINT "AuthIdentity_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth" ("id") ON DELETE CASCADE ON UPDATE CASCADE -); - --- CreateIndex -CREATE UNIQUE INDEX "Auth_userId_key" ON "Auth"("userId"); diff --git a/examples/todo-typescript/migrations/20231214130914_new_auth/migration.sql b/waspc/examples/todo-typescript/migrations/20240121113923_init/migration.sql similarity index 72% rename from examples/todo-typescript/migrations/20231214130914_new_auth/migration.sql rename to waspc/examples/todo-typescript/migrations/20240121113923_init/migration.sql index 0ea8e16da6..919941fb15 100644 --- a/examples/todo-typescript/migrations/20231214130914_new_auth/migration.sql +++ b/waspc/examples/todo-typescript/migrations/20240121113923_init/migration.sql @@ -30,5 +30,19 @@ CREATE TABLE "AuthIdentity" ( CONSTRAINT "AuthIdentity_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth" ("id") ON DELETE CASCADE ON UPDATE CASCADE ); +-- CreateTable +CREATE TABLE "Session" ( + "id" TEXT NOT NULL PRIMARY KEY, + "expiresAt" DATETIME NOT NULL, + "userId" TEXT NOT NULL, + CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Auth" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + -- CreateIndex CREATE UNIQUE INDEX "Auth_userId_key" ON "Auth"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_id_key" ON "Session"("id"); + +-- CreateIndex +CREATE INDEX "Session_userId_idx" ON "Session"("userId"); diff --git a/waspc/examples/todo-typescript/package-lock.json b/waspc/examples/todo-typescript/package-lock.json index 76cde59255..58a30899d9 100644 --- a/waspc/examples/todo-typescript/package-lock.json +++ b/waspc/examples/todo-typescript/package-lock.json @@ -20,6 +20,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@lucia-auth/adapter-prisma": "^4.0.0-beta.9", "@prisma/client": "4.16.2", "@stitches/react": "^1.2.8", "@tanstack/react-query": "^4.29.0", @@ -27,6 +28,7 @@ "axios": "^1.4.0", "express": "~4.18.1", "jsonwebtoken": "^8.5.1", + "lucia": "^3.0.0-beta.14", "mitt": "3.0.0", "prisma": "4.16.2", "react": "^18.2.0", @@ -50,358 +52,6 @@ "node": ">=6.9.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@prisma/client": { "version": "4.16.2", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.16.2.tgz", @@ -476,12 +126,6 @@ } } }, - "node_modules/@tsconfig/node18": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.2.tgz", - "integrity": "sha512-d6McJeGsuoRlwWZmVIeE8CUA27lu6jLjvv1JzqmpsytOYYbVi1tHZEnwCNVOXnj4pyLvneZlFlpXUK+X9wBWyw==", - "dev": true - }, "node_modules/@types/express-serve-static-core": { "version": "4.17.41", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", @@ -1178,6 +822,14 @@ "loose-envify": "cli.js" } }, + "node_modules/lucia": { + "version": "3.0.0-beta.14", + "resolved": "https://registry.npmjs.org/lucia/-/lucia-3.0.0-beta.14.tgz", + "integrity": "sha512-MXJILHb4xyvf3qjO7w7mDnvVOub2LGWLSjgP1TBGPLDkBF62uXNfvPNH7QRvOwvuSLtQK+w7JoPjnjiFiIj9rg==", + "dependencies": { + "oslo": "^0.27.0" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1307,6 +959,15 @@ "node": ">= 0.8" } }, + "node_modules/oslo": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/oslo/-/oslo-0.27.1.tgz", + "integrity": "sha512-AYU0LpwZ50wIMD3dr4NX0tQzjwxaejSSV9reiY9jbQfgODt49al3f3tMcijyddyvPknUPArz845vpswWyJTWvA==", + "dependencies": { + "@node-rs/argon2": "^1.5.2", + "@node-rs/bcrypt": "^1.7.3" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1705,6 +1366,12 @@ "node": ">=0.6" } }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "optional": true + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/waspc/examples/todoApp/migrations/20240110132515_add_session/migration.sql b/waspc/examples/todoApp/migrations/20240110132515_add_session/migration.sql new file mode 100644 index 0000000000..47f98ace24 --- /dev/null +++ b/waspc/examples/todoApp/migrations/20240110132515_add_session/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "Session" ( + "id" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "userId" TEXT NOT NULL, + + CONSTRAINT "Session_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_id_key" ON "Session"("id"); + +-- CreateIndex +CREATE INDEX "Session_userId_idx" ON "Session"("userId"); + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Auth"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/waspc/examples/todoApp/src/server/auth/email.ts b/waspc/examples/todoApp/src/server/auth/email.ts index 6af04c1bae..d43c2f5500 100644 --- a/waspc/examples/todoApp/src/server/auth/email.ts +++ b/waspc/examples/todoApp/src/server/auth/email.ts @@ -2,6 +2,7 @@ import { GetPasswordResetEmailContentFn, GetVerificationEmailContentFn, } from '@wasp/types' +import { defineUserSignupFields } from '@wasp/auth/index.js' export const getPasswordResetEmailContent: GetPasswordResetEmailContentFn = ({ passwordResetLink, @@ -24,3 +25,15 @@ export const getVerificationEmailContent: GetVerificationEmailContentFn = ({ Verify email `, }) + +export const userSignupFields = defineUserSignupFields({ + address: (data) => { + if (typeof data.address !== 'string') { + throw new Error('Address is required.') + } + if (data.address.length < 10) { + throw new Error('Address must be at least 10 characters long.') + } + return data.address + }, +}) diff --git a/waspc/examples/todoApp/src/server/auth/github.js b/waspc/examples/todoApp/src/server/auth/github.js index 930bbc5690..63f6dbb62f 100644 --- a/waspc/examples/todoApp/src/server/auth/github.js +++ b/waspc/examples/todoApp/src/server/auth/github.js @@ -1,16 +1,10 @@ -import { generateAvailableUsername } from '@wasp/core/auth.js' - export function config() { - console.log("Inside user-supplied GitHub config") + console.log('Inside user-supplied GitHub config') return { clientID: process.env['GITHUB_CLIENT_ID'], clientSecret: process.env['GITHUB_CLIENT_SECRET'], - scope: [] + scope: [], } } -export async function getUserFields(_context, args) { - console.log("Inside user-supplied GitHub getUserFields") - const username = await generateAvailableUsername([args.profile.username], { separator: '-' }) - return { username } -} +export const userSignupFields = {} diff --git a/waspc/examples/todoApp/src/server/auth/google.js b/waspc/examples/todoApp/src/server/auth/google.js index 3bee3abcd9..9071d973f6 100644 --- a/waspc/examples/todoApp/src/server/auth/google.js +++ b/waspc/examples/todoApp/src/server/auth/google.js @@ -7,7 +7,4 @@ export function config() { } } -export async function getUserFields(_context, args) { - console.log('Inside user-supplied Google getUserFields') - return {} -} +export const userSignupFields = {} diff --git a/waspc/examples/todoApp/src/server/auth/signup.ts b/waspc/examples/todoApp/src/server/auth/signup.ts index d2e9414dbf..35043eeced 100644 --- a/waspc/examples/todoApp/src/server/auth/signup.ts +++ b/waspc/examples/todoApp/src/server/auth/signup.ts @@ -1,6 +1,6 @@ -import { defineAdditionalSignupFields } from '@wasp/auth/index.js' +import { defineUserSignupFields } from '@wasp/auth/index.js' -export const fields = defineAdditionalSignupFields({ +export const userSignupFields = defineUserSignupFields({ address: (data) => { if (typeof data.address !== 'string') { throw new Error('Address is required.') diff --git a/waspc/examples/todoApp/src/server/dbSeeds.ts b/waspc/examples/todoApp/src/server/dbSeeds.ts index b5ec2528fc..e7a418ce76 100644 --- a/waspc/examples/todoApp/src/server/dbSeeds.ts +++ b/waspc/examples/todoApp/src/server/dbSeeds.ts @@ -1,7 +1,7 @@ import { createTask } from './actions.js' import type { DbSeedFn } from '@wasp/dbSeed/types.js' import { PrismaClient } from '@prisma/client/index.js' -import { hashPassword } from '@wasp/core/auth.js' +import { hashPassword } from '@wasp/auth/password.js' async function createUser(prismaClient: PrismaClient, data: any) { const newUser = await prismaClient.user.create({ diff --git a/waspc/examples/todoApp/todoApp.wasp b/waspc/examples/todoApp/todoApp.wasp index 58bd869233..e70f28ab6e 100644 --- a/waspc/examples/todoApp/todoApp.wasp +++ b/waspc/examples/todoApp/todoApp.wasp @@ -15,16 +15,19 @@ app todoApp { auth: { userEntity: User, methods: { - // usernameAndPassword: {}, + // usernameAndPassword: { + // userSignupFields: import { userSignupFields } from "@server/auth/github.js", + // }, google: { configFn: import { config } from "@server/auth/google.js", - getUserFieldsFn: import { getUserFields } from "@server/auth/google.js" + userSignupFields: import { userSignupFields } from "@server/auth/google.js" }, // gitHub: { // // configFn: import { config } from "@server/auth/github.js", - // // getUserFieldsFn: import { getUserFields } from "@server/auth/github.js" + // // userSignupFields: import { getUserFields } from "@server/auth/github.js" // }, email: { + userSignupFields: import { userSignupFields } from "@server/auth/email.js", fromField: { name: "ToDO App", email: "mihovil@ilakovac.com" @@ -37,12 +40,8 @@ app todoApp { getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js", clientRoute: PasswordResetRoute }, - allowUnverifiedLogin: false, }, }, - signup: { - additionalFields: import { fields } from "@server/auth/signup.js", - }, onAuthFailedRedirectTo: "/login", onAuthSucceededRedirectTo: "/profile" }, diff --git a/waspc/headless-test/examples/todoApp/migrations/20240115130723_add_session/migration.sql b/waspc/headless-test/examples/todoApp/migrations/20240115130723_add_session/migration.sql new file mode 100644 index 0000000000..47f98ace24 --- /dev/null +++ b/waspc/headless-test/examples/todoApp/migrations/20240115130723_add_session/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "Session" ( + "id" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "userId" TEXT NOT NULL, + + CONSTRAINT "Session_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_id_key" ON "Session"("id"); + +-- CreateIndex +CREATE INDEX "Session_userId_idx" ON "Session"("userId"); + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Auth"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/waspc/headless-test/examples/todoApp/sample.env.server b/waspc/headless-test/examples/todoApp/sample.env.server index b590869737..fd6dccba45 100644 --- a/waspc/headless-test/examples/todoApp/sample.env.server +++ b/waspc/headless-test/examples/todoApp/sample.env.server @@ -1,2 +1,3 @@ GOOGLE_CLIENT_ID="mock-client-id" -GOOGLE_CLIENT_SECRET="mock-client-secret" \ No newline at end of file +GOOGLE_CLIENT_SECRET="mock-client-secret" +SKIP_EMAIL_VERIFICATION_IN_DEV=true \ No newline at end of file diff --git a/waspc/headless-test/examples/todoApp/todoApp.wasp b/waspc/headless-test/examples/todoApp/todoApp.wasp index eabd93d2dd..f496314af0 100644 --- a/waspc/headless-test/examples/todoApp/todoApp.wasp +++ b/waspc/headless-test/examples/todoApp/todoApp.wasp @@ -22,8 +22,7 @@ app todoApp { passwordReset: { getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js", clientRoute: PasswordResetRoute - }, - allowUnverifiedLogin: true, + } }, google: {} }, diff --git a/waspc/headless-test/tests/simple.spec.ts b/waspc/headless-test/tests/simple.spec.ts index 7706f56a1d..5fedcfc189 100644 --- a/waspc/headless-test/tests/simple.spec.ts +++ b/waspc/headless-test/tests/simple.spec.ts @@ -17,7 +17,9 @@ test.describe("signup and login", () => { await page.waitForSelector("text=Create a new account"); - await expect(page.locator("a[href='http://localhost:3001/auth/google/login']")).toBeVisible(); + await expect( + page.locator("a[href='http://localhost:3001/auth/google/login']") + ).toBeVisible(); }); test("can sign up", async ({ page }) => { @@ -29,7 +31,9 @@ test.describe("signup and login", () => { await page.locator("input[type='password']").fill(password); await page.locator("button").click(); - await expect(page).toHaveURL("/profile"); + 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 }) => { diff --git a/waspc/src/Wasp/AI/CodeAgent.hs b/waspc/src/Wasp/AI/CodeAgent.hs index d67a50a006..971986565b 100644 --- a/waspc/src/Wasp/AI/CodeAgent.hs +++ b/waspc/src/Wasp/AI/CodeAgent.hs @@ -73,7 +73,10 @@ runCodeAgent config codeAgent = _isGpt4Available = Nothing } - shortenWithEllipsisTo maxLen text = if length text <= maxLen then text else (take maxLen text) <> "..." + shortenWithEllipsisTo maxLen text = + if length text <= maxLen + then text + else take maxLen text <> "..." showShortException :: forall e. Exception e => e -> String showShortException = shortenWithEllipsisTo 30 . displayException diff --git a/waspc/src/Wasp/AppSpec/App/Auth.hs b/waspc/src/Wasp/AppSpec/App/Auth.hs index f1f7e1df87..0739bbd067 100644 --- a/waspc/src/Wasp/AppSpec/App/Auth.hs +++ b/waspc/src/Wasp/AppSpec/App/Auth.hs @@ -6,14 +6,15 @@ module Wasp.AppSpec.App.Auth AuthMethods (..), ExternalAuthConfig (..), EmailAuthConfig (..), - SignupOptions (..), - usernameAndPasswordConfig, + UsernameAndPasswordConfig (..), isUsernameAndPasswordAuthEnabled, isExternalAuthEnabled, isGoogleAuthEnabled, isGitHubAuthEnabled, isEmailAuthEnabled, - isEmailVerificationRequired, + userSignupFieldsForEmailAuth, + userSignupFieldsForUsernameAuth, + userSignupFieldsForExternalAuth, ) where @@ -30,7 +31,6 @@ data Auth = Auth { userEntity :: Ref Entity, externalAuthEntity :: Maybe (Ref Entity), methods :: AuthMethods, - signup :: Maybe SignupOptions, onAuthFailedRedirectTo :: String, onAuthSucceededRedirectTo :: Maybe String } @@ -45,33 +45,24 @@ data AuthMethods = AuthMethods deriving (Show, Eq, Data) data UsernameAndPasswordConfig = UsernameAndPasswordConfig - { -- NOTE: Not used right now, but Analyzer does not support an empty data type. - configFn :: Maybe ExtImport + { userSignupFields :: Maybe ExtImport } deriving (Show, Eq, Data) data ExternalAuthConfig = ExternalAuthConfig { configFn :: Maybe ExtImport, - getUserFieldsFn :: Maybe ExtImport + userSignupFields :: Maybe ExtImport } deriving (Show, Eq, Data) data EmailAuthConfig = EmailAuthConfig - { fromField :: EmailFromField, + { userSignupFields :: Maybe ExtImport, + fromField :: EmailFromField, emailVerification :: EmailVerificationConfig, - passwordReset :: PasswordResetConfig, - allowUnverifiedLogin :: Maybe Bool + passwordReset :: PasswordResetConfig } deriving (Show, Eq, Data) -data SignupOptions = SignupOptions - { additionalFields :: Maybe ExtImport - } - deriving (Show, Eq, Data) - -usernameAndPasswordConfig :: UsernameAndPasswordConfig -usernameAndPasswordConfig = UsernameAndPasswordConfig Nothing - isUsernameAndPasswordAuthEnabled :: Auth -> Bool isUsernameAndPasswordAuthEnabled = isJust . usernameAndPassword . methods @@ -87,7 +78,14 @@ isGitHubAuthEnabled = isJust . gitHub . methods isEmailAuthEnabled :: Auth -> Bool isEmailAuthEnabled = isJust . email . methods -isEmailVerificationRequired :: Auth -> Bool -isEmailVerificationRequired auth = case email . methods $ auth of - Nothing -> False - Just emailAuthConfig -> allowUnverifiedLogin emailAuthConfig /= Just True +-- These helper functions are used to avoid ambiguity when using the +-- `userSignupFields` function (otherwise we need to use the DuplicateRecordFields +-- extension in each module that uses them). +userSignupFieldsForEmailAuth :: EmailAuthConfig -> Maybe ExtImport +userSignupFieldsForEmailAuth = userSignupFields + +userSignupFieldsForUsernameAuth :: UsernameAndPasswordConfig -> Maybe ExtImport +userSignupFieldsForUsernameAuth = userSignupFields + +userSignupFieldsForExternalAuth :: ExternalAuthConfig -> Maybe ExtImport +userSignupFieldsForExternalAuth = userSignupFields diff --git a/waspc/src/Wasp/AppSpec/App/Dependency.hs b/waspc/src/Wasp/AppSpec/App/Dependency.hs index f355c3a11b..ecb3ab0d36 100644 --- a/waspc/src/Wasp/AppSpec/App/Dependency.hs +++ b/waspc/src/Wasp/AppSpec/App/Dependency.hs @@ -14,6 +14,8 @@ import GHC.Generics data Dependency = Dependency { name :: String, + -- | NOTE: By npm docs, this can be semver version range, + -- but it can also be a URL (tarball, git or Github), or a local file path. version :: String } deriving (Show, Eq, Data, Generic) diff --git a/waspc/src/Wasp/AppSpec/App/EmailSender.hs b/waspc/src/Wasp/AppSpec/App/EmailSender.hs index ee10a40908..7721f579bd 100644 --- a/waspc/src/Wasp/AppSpec/App/EmailSender.hs +++ b/waspc/src/Wasp/AppSpec/App/EmailSender.hs @@ -15,7 +15,7 @@ data EmailSender = EmailSender } deriving (Show, Eq, Data) -data EmailProvider = SMTP | SendGrid | Mailgun +data EmailProvider = SMTP | SendGrid | Mailgun | Dummy deriving (Eq, Data, Show) data EmailFromField = EmailFromField diff --git a/waspc/src/Wasp/AppSpec/PackageJson.hs b/waspc/src/Wasp/AppSpec/PackageJson.hs index 3e90b4f600..cc90def775 100644 --- a/waspc/src/Wasp/AppSpec/PackageJson.hs +++ b/waspc/src/Wasp/AppSpec/PackageJson.hs @@ -1,10 +1,8 @@ {-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE TemplateHaskell #-} module Wasp.AppSpec.PackageJson where -import Control.Applicative (liftA2) -import Data.Aeson.TH +import Data.Aeson (FromJSON) import Data.Map (Map) import qualified Data.Map as M import GHC.Generics (Generic) @@ -12,20 +10,16 @@ import Wasp.AppSpec.App.Dependency (Dependency) import qualified Wasp.AppSpec.App.Dependency as D data PackageJson = PackageJson - { _name :: !String, - -- todo(filip): do this properly once you merge martin's PR - _dependencies :: !(Map String String), - _devDependencies :: !(Map String String) + { name :: !String, + dependencies :: !(Map String String), + devDependencies :: !(Map String String) } deriving (Show, Generic) -$(deriveJSON defaultOptions {fieldLabelModifier = drop 1} ''PackageJson) +instance FromJSON PackageJson -dependencies :: PackageJson -> [Dependency] -dependencies packageJson = D.fromList $ M.toList $ _dependencies packageJson +getDependencies :: PackageJson -> [Dependency] +getDependencies packageJson = D.fromList $ M.toList $ dependencies packageJson -devDependencies :: PackageJson -> [Dependency] -devDependencies packageJson = D.fromList $ M.toList $ _devDependencies packageJson - -allDependencies :: PackageJson -> [Dependency] -allDependencies = liftA2 (++) dependencies devDependencies +getDevDependencies :: PackageJson -> [Dependency] +getDevDependencies packageJson = D.fromList $ M.toList $ devDependencies packageJson diff --git a/waspc/src/Wasp/AppSpec/Valid.hs b/waspc/src/Wasp/AppSpec/Valid.hs index 02bb1d10a4..6880edd293 100644 --- a/waspc/src/Wasp/AppSpec/Valid.hs +++ b/waspc/src/Wasp/AppSpec/Valid.hs @@ -28,6 +28,7 @@ import qualified Wasp.AppSpec.App as App import qualified Wasp.AppSpec.App.Auth as Auth import qualified Wasp.AppSpec.App.Client as Client import qualified Wasp.AppSpec.App.Db as AS.Db +import qualified Wasp.AppSpec.App.EmailSender as AS.EmailSender import qualified Wasp.AppSpec.App.Wasp as Wasp import Wasp.AppSpec.Core.Decl (takeDecls) import qualified Wasp.AppSpec.Crud as AS.Crud @@ -70,6 +71,7 @@ validateAppSpec spec = validateUserEntity spec, validateOnlyEmailOrUsernameAndPasswordAuthIsUsed spec, validateEmailSenderIsDefinedIfEmailAuthIsUsed spec, + validateDummyEmailSenderIsNotUsedInProduction spec, validateDbIsPostgresIfPgBossUsed spec, validateApiRoutesAreUnique spec, validateApiNamespacePathsAreUnique spec, @@ -177,14 +179,21 @@ validateEmailSenderIsDefinedIfEmailAuthIsUsed :: AppSpec -> [ValidationError] validateEmailSenderIsDefinedIfEmailAuthIsUsed spec = case App.auth app of Nothing -> [] Just auth -> - if not $ Auth.isEmailAuthEnabled auth - then [] - else case App.emailSender app of - Nothing -> [GenericValidationError "app.emailSender must be specified when using email auth."] - Just _ -> [] + if Auth.isEmailAuthEnabled auth && isNothing (App.emailSender app) + then [GenericValidationError "app.emailSender must be specified when using email auth. You can use the Dummy email sender for development purposes."] + else [] where app = snd $ getApp spec +validateDummyEmailSenderIsNotUsedInProduction :: AppSpec -> [ValidationError] +validateDummyEmailSenderIsNotUsedInProduction spec = + if AS.isBuild spec && isDummyEmailSenderUsed + then [GenericValidationError "app.emailSender must not be set to Dummy when building for production."] + else [] + where + isDummyEmailSenderUsed = (AS.EmailSender.provider <$> App.emailSender app) == Just AS.EmailSender.Dummy + app = snd $ getApp spec + validateApiRoutesAreUnique :: AppSpec -> [ValidationError] validateApiRoutesAreUnique spec = if null groupsOfConflictingRoutes diff --git a/waspc/src/Wasp/Generator/DbGenerator.hs b/waspc/src/Wasp/Generator/DbGenerator.hs index bd2411f113..dda90bd13d 100644 --- a/waspc/src/Wasp/Generator/DbGenerator.hs +++ b/waspc/src/Wasp/Generator/DbGenerator.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE TypeApplications #-} - module Wasp.Generator.DbGenerator ( genDb, warnIfDbNeedsMigration, diff --git a/waspc/src/Wasp/Generator/DbGenerator/Auth.hs b/waspc/src/Wasp/Generator/DbGenerator/Auth.hs index 849b45ee1a..b6e0989692 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Auth.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Auth.hs @@ -12,6 +12,7 @@ import Wasp.Generator.Monad ) import qualified Wasp.Psl.Ast.Model as Psl.Model import qualified Wasp.Psl.Ast.Model as Psl.Model.Field +import qualified Wasp.Util as Util {-- @@ -59,12 +60,22 @@ authIdentityEntityName = "AuthIdentity" identitiesFieldOnAuthEntityName :: String identitiesFieldOnAuthEntityName = "identities" +sessionEntityName :: String +sessionEntityName = "Session" + +sessionsFieldOnAuthEntityName :: String +sessionsFieldOnAuthEntityName = "sessions" + +authFieldOnSessionEntityName :: String +authFieldOnSessionEntityName = Util.toLowerFirst authEntityName + injectAuth :: [(String, AS.Entity.Entity)] -> (String, AS.Entity.Entity) -> Generator [(String, AS.Entity.Entity)] injectAuth entities (userEntityName, userEntity) = do authEntity <- makeAuthEntity userEntityIdField (userEntityName, userEntity) authIdentityEntity <- makeAuthIdentityEntity + sessionEntity <- makeSessionEntity let entitiesWithAuth = injectAuthIntoUserEntity userEntityName entities - return $ entitiesWithAuth ++ [authEntity, authIdentityEntity] + return $ entitiesWithAuth ++ [authEntity, authIdentityEntity, sessionEntity] where -- We validated the AppSpec so we are sure that the user entity has an id field. userEntityIdField = fromJust $ AS.Entity.getIdField userEntity @@ -94,7 +105,7 @@ makeAuthIdentityEntity = case parsePslBody authIdentityPslBody of makeAuthEntity :: Psl.Model.Field -> (String, AS.Entity.Entity) -> Generator (String, AS.Entity.Entity) makeAuthEntity userEntityIdField (userEntityName, _) = case parsePslBody authEntityPslBody of - Left err -> logAndThrowGeneratorError $ GenericGeneratorError $ "Error while generating Auth entity: " ++ show err + Left err -> logAndThrowGeneratorError $ GenericGeneratorError $ "Error while generating " ++ authEntityName ++ " entity: " ++ show err Right pslBody -> return (authEntityName, AS.Entity.makeEntity pslBody) where authEntityPslBody = @@ -104,6 +115,7 @@ makeAuthEntity userEntityIdField (userEntityName, _) = case parsePslBody authEnt userId ${userEntityIdTypeText}? @unique ${userFieldOnAuthEntityNameText} ${userEntityNameText}? @relation(fields: [userId], references: [${userEntityIdFieldName}], onDelete: Cascade) ${identitiesFieldOnAuthEntityNameText} ${authIdentityEntityNameText}[] + ${sessionsFieldOnAuthEntityNameText} ${sessionEntityNameText}[] |] authEntityIdTypeText = T.pack authEntityIdType @@ -111,10 +123,35 @@ makeAuthEntity userEntityIdField (userEntityName, _) = case parsePslBody authEnt userFieldOnAuthEntityNameText = T.pack userFieldOnAuthEntityName authIdentityEntityNameText = T.pack authIdentityEntityName identitiesFieldOnAuthEntityNameText = T.pack identitiesFieldOnAuthEntityName + sessionsFieldOnAuthEntityNameText = T.pack sessionsFieldOnAuthEntityName + sessionEntityNameText = T.pack sessionEntityName userEntityIdTypeText = T.pack $ show . Psl.Model.Field._type $ userEntityIdField userEntityIdFieldName = T.pack $ Psl.Model.Field._name userEntityIdField +makeSessionEntity :: Generator (String, AS.Entity.Entity) +makeSessionEntity = case parsePslBody sessionEntityPslBody of + Left err -> logAndThrowGeneratorError $ GenericGeneratorError $ "Error while generating " ++ sessionEntityName ++ " entity: " ++ show err + Right pslBody -> return (sessionEntityName, AS.Entity.makeEntity pslBody) + where + sessionEntityPslBody = + T.unpack + [trimming| + id String @id @unique + expiresAt DateTime + + // Needs to be called `userId` for Lucia to be able to create sessions + userId String + // The relation needs to be named as lowercased entity name, because that's what Lucia expects. + // If the entity is named `Foo`, the relation needs to be named `foo`. + ${authFieldOnSessionEntityNameText} ${authEntityNameText} @relation(references: [id], fields: [userId], onDelete: Cascade) + + @@index([userId]) + |] + + authEntityNameText = T.pack authEntityName + authFieldOnSessionEntityNameText = T.pack authFieldOnSessionEntityName + injectAuthIntoUserEntity :: String -> [(String, AS.Entity.Entity)] -> [(String, AS.Entity.Entity)] injectAuthIntoUserEntity userEntityName entities = let userEntity = fromJust $ lookup userEntityName entities diff --git a/waspc/src/Wasp/Generator/DbGenerator/Common.hs b/waspc/src/Wasp/Generator/DbGenerator/Common.hs index 6db5e574a1..116e081d85 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Common.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Common.hs @@ -16,7 +16,6 @@ module Wasp.Generator.DbGenerator.Common serverRootDirFromDbRootDir, webAppRootDirFromDbRootDir, dbSchemaFileInProjectRootDir, - waspProjectDirFromProjectRootDir, DbSchemaChecksumFile, ) where @@ -24,7 +23,6 @@ where import StrongPath (Dir, File, File', Path', Rel, reldir, relfile, ()) import Wasp.Generator.Common (AppComponentRootDir, DbRootDir, ProjectRootDir, ServerRootDir) import Wasp.Generator.Templates (TemplatesDir) -import Wasp.Project.Common (WaspProjectDir) import Wasp.Project.Db.Migrations (DbMigrationsDir) data DbTemplatesDir @@ -90,9 +88,6 @@ dbSchemaChecksumOnLastGenerateFileInDbRootDir = [relfile|schema.prisma.wasp-gene dbSchemaChecksumOnLastGenerateFileProjectRootDir :: Path' (Rel ProjectRootDir) (File DbSchemaChecksumOnLastGenerateFile) dbSchemaChecksumOnLastGenerateFileProjectRootDir = dbRootDirInProjectRootDir dbSchemaChecksumOnLastGenerateFileInDbRootDir -waspProjectDirFromProjectRootDir :: Path' (Rel ProjectRootDir) (Dir WaspProjectDir) -waspProjectDirFromProjectRootDir = [reldir|../../|] - data MigrateArgs = MigrateArgs { _migrationName :: Maybe String, _isCreateOnlyMigration :: Bool diff --git a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs index 94990324e0..a60a9db0f3 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs @@ -18,16 +18,12 @@ import qualified StrongPath as SP import StrongPath.TH (relfile) import qualified System.Info import Wasp.Generator.Common (ProjectRootDir) -import Wasp.Generator.DbGenerator.Common - ( MigrateArgs (..), - dbSchemaFileInProjectRootDir, - waspProjectDirFromProjectRootDir, - ) +import Wasp.Generator.DbGenerator.Common (MigrateArgs (..), dbSchemaFileInProjectRootDir) import qualified Wasp.Generator.Job as J import Wasp.Generator.Job.Process (runNodeCommandAsJob, runNodeCommandAsJobWithExtraEnv) import Wasp.Generator.ServerGenerator.Common (serverRootDirInProjectRootDir) import Wasp.Generator.ServerGenerator.Db.Seed (dbSeedNameEnvVarName) -import Wasp.Project.Common (WaspProjectDir) +import Wasp.Project.Common (WaspProjectDir, waspProjectDirFromProjectRootDir) migrateDev :: Path' Abs (Dir ProjectRootDir) -> MigrateArgs -> J.Job migrateDev projectRootDir migrateArgs = @@ -142,10 +138,9 @@ seed projectRootDir seedName = -- SQL command, which works perfectly for checking if the database is running. dbExecuteTest :: Path' Abs (Dir ProjectRootDir) -> J.Job dbExecuteTest projectRootDir = - let absSchemaPath = projectRootDir dbSchemaFileInProjectRootDir - in runPrismaCommandAsJob - projectRootDir - ["db", "execute", "--stdin", "--schema", SP.fromAbsFile absSchemaPath] + runPrismaCommandAsJob projectRootDir ["db", "execute", "--stdin", "--schema", SP.fromAbsFile schema] + where + schema = projectRootDir dbSchemaFileInProjectRootDir -- | Runs `prisma studio` - Prisma's db inspector. runStudio :: Path' Abs (Dir ProjectRootDir) -> J.Job @@ -156,13 +151,9 @@ runStudio projectRootDir = generatePrismaClient :: Path' Abs (Dir ProjectRootDir) -> J.Job generatePrismaClient projectRootDir = - runPrismaCommandAsJob projectRootDir cmdArgs + runPrismaCommandAsJob projectRootDir ["generate", "--schema", SP.fromAbsFile schema] where - cmdArgs = - [ "generate", - "--schema", - SP.fromAbsFile $ projectRootDir dbSchemaFileInProjectRootDir - ] + schema = projectRootDir dbSchemaFileInProjectRootDir runPrismaCommandAsJob :: Path' Abs (Dir ProjectRootDir) -> [String] -> J.Job runPrismaCommandAsJob projectRootDir cmdArgs = diff --git a/waspc/src/Wasp/Generator/DbGenerator/Operations.hs b/waspc/src/Wasp/Generator/DbGenerator/Operations.hs index 8596c1a418..503e912938 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Operations.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Operations.hs @@ -36,7 +36,6 @@ import Wasp.Generator.DbGenerator.Common ) import qualified Wasp.Generator.DbGenerator.Jobs as DbJobs import Wasp.Generator.FileDraft.WriteableMonad (WriteableMonad (copyDirectoryRecursive, doesDirectoryExist)) -import qualified Wasp.Generator.Job as J import Wasp.Generator.Job.IO ( collectJobTextOutputUntilExitReceived, printJobMsgsUntilExitReceived, diff --git a/waspc/src/Wasp/Generator/Job/IO/PrefixedWriter.hs b/waspc/src/Wasp/Generator/Job/IO/PrefixedWriter.hs index 2e8015f673..d3f3bd0d97 100644 --- a/waspc/src/Wasp/Generator/Job/IO/PrefixedWriter.hs +++ b/waspc/src/Wasp/Generator/Job/IO/PrefixedWriter.hs @@ -1,4 +1,5 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE TupleSections #-} module Wasp.Generator.Job.IO.PrefixedWriter ( printJobMessagePrefixed, @@ -10,10 +11,13 @@ where import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Monad.State (get, put) import Control.Monad.State.Strict (MonadState, StateT, runStateT) +import Data.List (maximumBy) +import Data.Ord (comparing) import qualified Data.Set as S import qualified Data.Text as T import qualified Data.Text.IO as T.IO import System.IO (hFlush, stderr) +import Wasp.Generator.Job (JobType) import qualified Wasp.Generator.Job as J import Wasp.Generator.Job.Common (getJobMessageContent, getJobMessageOutHandle) import qualified Wasp.Util.Terminal as Term @@ -169,31 +173,44 @@ getJobMessageOutput jm = makeJobMessagePrefix :: J.JobMessage -> T.Text makeJobMessagePrefix jobMsg = - (T.pack . buildPrefix . concat) - [ [("[", jobStyles)], + T.pack . concatMap (\(text, styles) -> Term.applyStyles styles text) . concat $ + [ [(startDelimiter, jobStyles)], + [unstyled namePaddingFront], [(jobName, jobStyles)], - [("]", jobStyles)], - styledFlags + [unstyled namePaddingBack], + styledFlags, + [(endDelimiter, jobStyles)], + [unstyled " "] ] where - buildPrefix :: [StyledText] -> String - buildPrefix styledTexts = - concatMap styledTextToTermText styledTexts <> replicate paddingLength ' ' + (namePaddingFront, namePaddingBack) = + ( replicate namePaddingLengthFront ' ', + replicate namePaddingLengthBack ' ' + ) where - numVisibleChars = length $ concatMap fst styledTexts + namePaddingLengthFront = paddingLength `div` 2 + namePaddingLengthBack = paddingLength `div` 2 + paddingLength `mod` 2 - length (concatMap fst styledFlags) paddingLength = max 0 (minPrefixLength - numVisibleChars) - styledTextToTermText (text, styles) = Term.applyStyles styles text - -- NOTE: Adjust this number if you expect longer prefixes! - minPrefixLength = 10 + numVisibleChars = length . concat $ [startDelimiter, jobName, endDelimiter] + minPrefixLength = length $ startDelimiter <> " " <> longestJobName <> " " <> endDelimiter + longestJobName = + maximumBy (comparing length) $ + fst . getJobNameAndStyles <$> [(minBound :: JobType) .. maxBound] - (jobName, jobStyles) = case J._jobType jobMsg of - J.Wasp -> (" Wasp ", [Term.Yellow]) - J.Server -> ("Server", [Term.Magenta]) - J.WebApp -> ("Client", [Term.Cyan]) - J.Db -> (" Db ", [Term.Blue]) + (startDelimiter, endDelimiter) = ("[", "]") styledFlags :: [StyledText] styledFlags = - [("!", [Term.Red]) | getJobMessageOutHandle jobMsg == stderr] + [("!", [Term.Red, Term.Bold]) | getJobMessageOutHandle jobMsg == stderr] + + (jobName, jobStyles) = getJobNameAndStyles $ J._jobType jobMsg + + getJobNameAndStyles = \case + J.Wasp -> ("Wasp", [Term.Yellow]) + J.Server -> ("Server", [Term.Magenta]) + J.WebApp -> ("Client", [Term.Cyan]) + J.Db -> ("Db", [Term.Blue]) + + unstyled = (,[]) type StyledText = (String, [Term.Style]) diff --git a/waspc/src/Wasp/Generator/NpmDependencies.hs b/waspc/src/Wasp/Generator/NpmDependencies.hs index 2f66df485a..faaed68adb 100644 --- a/waspc/src/Wasp/Generator/NpmDependencies.hs +++ b/waspc/src/Wasp/Generator/NpmDependencies.hs @@ -107,10 +107,9 @@ buildNpmDepsForFullStack spec forServer forWebApp = getUserNpmDepsForPackage :: AppSpec -> NpmDepsForUser getUserNpmDepsForPackage spec = NpmDepsForUser - { -- todo(filip): what if package.json has no dependencies field? - userDependencies = AS.PackageJson.dependencies $ AS.packageJson spec, + { userDependencies = AS.PackageJson.getDependencies $ AS.packageJson spec, -- Should we allow user devDependencies? https://github.com/wasp-lang/wasp/issues/456 - userDevDependencies = AS.PackageJson.devDependencies $ AS.packageJson spec + userDevDependencies = AS.PackageJson.getDevDependencies $ AS.packageJson spec } conflictErrorToMessage :: DependencyConflictError -> String @@ -161,8 +160,8 @@ combineNpmDepsForPackage npmDepsForWasp npmDepsForUser = allWaspDepsByName = waspDepsByName `Map.union` waspDevDepsByName conflictErrors = determineConflictErrors allWaspDepsByName userDepsByName devConflictErrors = determineConflictErrors allWaspDepsByName userDevDepsByName - remainingWapsDeps = allWaspDepsByName `Map.difference` userDepsByName - remainingWaspDevDeps = allWaspDepsByName `Map.difference` userDevDepsByName + remainingWapsDeps = waspDepsByName `Map.difference` userDepsByName + remainingWaspDevDeps = waspDevDepsByName `Map.difference` userDevDepsByName type DepsByName = Map.Map String D.Dependency diff --git a/waspc/src/Wasp/Generator/NpmInstall.hs b/waspc/src/Wasp/Generator/NpmInstall.hs index 353fe845e9..fd0cd2fa8a 100644 --- a/waspc/src/Wasp/Generator/NpmInstall.hs +++ b/waspc/src/Wasp/Generator/NpmInstall.hs @@ -10,6 +10,7 @@ import Control.Monad (when) import Control.Monad.IO.Class (liftIO) import qualified Data.Aeson as Aeson import qualified Data.ByteString.Lazy as B +import Data.Functor ((<&>)) import qualified Data.Text as T import StrongPath (Abs, Dir, File', Path', Rel, relfile, ()) import qualified StrongPath as SP @@ -130,12 +131,14 @@ reportInstallationProgress chan jobType = reportPeriodically allPossibleMessages ] installNpmDependenciesAndReport :: Job -> Chan JobMessage -> JobType -> IO ExitCode -installNpmDependenciesAndReport installF chan jobType = do +installNpmDependenciesAndReport installJob chan jobType = do writeChan chan $ J.JobMessage {J._data = J.JobOutput "Starting npm install\n" J.Stdout, J._jobType = jobType} - result <- installF chan `race` reportInstallationProgress chan jobType + result <- installJob chan `race` reportInstallationProgress chan jobType case result of Left exitCode -> return exitCode - Right _ -> error "This should be impossible" + Right _ -> error "This should never happen, reporting installation progress should run forever." + +{- HLINT ignore installNpmDependencies "Redundant <$>" -} -- Run the individual `npm install` commands for both server and webapp projects -- It runs these concurrently, collects the output produced by these commands @@ -143,25 +146,34 @@ installNpmDependenciesAndReport installF chan jobType = do installNpmDependencies :: Path' Abs (Dir WaspProjectDir) -> Path' Abs (Dir ProjectRootDir) -> IO (Either String ()) installNpmDependencies projectDir dstDir = do messagesChan <- newChan - (_, exitCode) <- - concurrently - (handleProjectInstallMessage messagesChan) - (installNpmDependenciesAndReport (SdkGenerator.installNpmDependencies projectDir) messagesChan J.Wasp) - case exitCode of + installProjectNpmDependencies messagesChan projectDir >>= \case ExitFailure code -> return $ Left $ "Project setup failed with exit code " ++ show code ++ "." - _ -> do - let handleMessagesJob = handleJobMessages messagesChan - let runSetupJobs = - concurrently - (installNpmDependenciesAndReport (ServerSetup.installNpmDependencies dstDir) messagesChan J.Server) - (installNpmDependenciesAndReport (WebAppSetup.installNpmDependencies dstDir) messagesChan J.WebApp) - (_, results) <- concurrently handleMessagesJob runSetupJobs - case results of - (ExitSuccess, ExitSuccess) -> return $ Right () - exitCodes -> return $ Left $ setupFailedMessage exitCodes + _success -> do + installWebAppAndServerNpmDependencies messagesChan dstDir <&> \case + (ExitSuccess, ExitSuccess) -> Right () + exitCodes -> Left $ setupFailedMessage exitCodes + where + setupFailedMessage (serverExitCode, webAppExitCode) = + let serverErrorMessage = case serverExitCode of + ExitFailure code -> " Server setup failed with exit code " ++ show code ++ "." + _success -> "" + webAppErrorMessage = case webAppExitCode of + ExitFailure code -> " Web app setup failed with exit code " ++ show code ++ "." + _success -> "" + in "Setup failed!" ++ serverErrorMessage ++ webAppErrorMessage + +installProjectNpmDependencies :: + Chan JobMessage -> SP.Path SP.System Abs (Dir WaspProjectDir) -> IO ExitCode +installProjectNpmDependencies messagesChan projectDir = + snd <$> handleProjectInstallMessages messagesChan `concurrently` installProjectDepsJob where - handleProjectInstallMessage :: Chan J.JobMessage -> IO () - handleProjectInstallMessage = runPrefixedWriter . processMessages + installProjectDepsJob = + installNpmDependenciesAndReport + (SdkGenerator.installNpmDependencies projectDir) + messagesChan + J.Wasp + handleProjectInstallMessages :: Chan J.JobMessage -> IO () + handleProjectInstallMessages = runPrefixedWriter . processMessages where processMessages :: Chan J.JobMessage -> PrefixedWriter () processMessages chan = do @@ -169,7 +181,16 @@ installNpmDependencies projectDir dstDir = do case J._data jobMsg of J.JobOutput {} -> printJobMessagePrefixed jobMsg >> processMessages chan J.JobExit {} -> return () - handleJobMessages = runPrefixedWriter . processMessages (False, False) + +installWebAppAndServerNpmDependencies :: + Chan JobMessage -> SP.Path SP.System Abs (Dir ProjectRootDir) -> IO (ExitCode, ExitCode) +installWebAppAndServerNpmDependencies messagesChan dstDir = + snd <$> handleSetupJobsMessages messagesChan `concurrently` (installServerDepsJob `concurrently` installWebAppDepsJob) + where + installServerDepsJob = installNpmDependenciesAndReport (ServerSetup.installNpmDependencies dstDir) messagesChan J.Server + installWebAppDepsJob = installNpmDependenciesAndReport (WebAppSetup.installNpmDependencies dstDir) messagesChan J.WebApp + + handleSetupJobsMessages = runPrefixedWriter . processMessages (False, False) where processMessages :: (Bool, Bool) -> Chan J.JobMessage -> PrefixedWriter () processMessages (True, True) _ = return () @@ -182,14 +203,5 @@ installNpmDependencies projectDir dstDir = do J.JobExit {} -> case J._jobType jobMsg of J.WebApp -> processMessages (True, isServerDone) chan J.Server -> processMessages (isWebAppDone, True) chan - J.Db -> error "This should never happen. No db job should be active." - J.Wasp -> error "This should never happen. No db job should be active." - - setupFailedMessage (serverExitCode, webAppExitCode) = - let serverErrorMessage = case serverExitCode of - ExitFailure code -> " Server setup failed with exit code " ++ show code ++ "." - _ -> "" - webAppErrorMessage = case webAppExitCode of - ExitFailure code -> " Web app setup failed with exit code " ++ show code ++ "." - _ -> "" - in "Setup failed!" ++ serverErrorMessage ++ webAppErrorMessage + J.Db -> error "This should never happen. No Db job should be active." + J.Wasp -> error "This should never happen. No Wasp job should be active." diff --git a/waspc/src/Wasp/Generator/SdkGenerator.hs b/waspc/src/Wasp/Generator/SdkGenerator.hs index 8299187a1a..173e9ae9a3 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator.hs @@ -40,6 +40,7 @@ import qualified Wasp.Generator.NpmDependencies as N import Wasp.Generator.SdkGenerator.Common (SdkTemplatesDir) import qualified Wasp.Generator.SdkGenerator.Common as C import Wasp.Generator.SdkGenerator.ServerOpsGenerator (genOperations) +import qualified Wasp.Generator.ServerGenerator.AuthG as ServerAuthG import Wasp.Generator.Templates (getTemplatesDirAbsPath) import qualified Wasp.Node.Version as NodeVersion import Wasp.Project.Common (WaspProjectDir) @@ -182,7 +183,14 @@ genPackageJson spec = ("superjson", "^1.12.2"), ("@types/express-serve-static-core", "^4.17.13") ] - ++ depsRequiredForAuth spec, + ++ depsRequiredForAuth spec + -- This must be installed in the SDK because it lists prisma/client as a dependency. + -- Installing it inside .wasp/out/server/node_modules would also + -- install prisma/client in the same folder, which would cause our + -- runtime to load the wrong (uninitialized prisma/client) + -- TODO(filip): Find a better way to handle duplicate + -- dependencies: https://github.com/wasp-lang/wasp/issues/1640 + ++ ServerAuthG.depsRequiredByAuth spec, N.devDependencies = AS.Dependency.fromList [ ("@tsconfig/node" <> majorNodeVersionStr, "latest") @@ -210,8 +218,8 @@ depsRequiredForAuth spec = where versionRange = SV.Range [SV.backwardsCompatibleWith (SV.Version 1 2 8)] --- todo(filip): figure out where this belongs --- also, fix imports for wasp project +-- TODO(filip): Figure out where this belongs. Check https://github.com/wasp-lang/wasp/pull/1602#discussion_r1437144166 . +-- Also, fix imports for wasp project. installNpmDependencies :: Path' Abs (Dir WaspProjectDir) -> J.Job installNpmDependencies projectDir = runNodeCommandAsJob projectDir "npm" ["install"] J.Wasp diff --git a/waspc/src/Wasp/Generator/ServerGenerator.hs b/waspc/src/Wasp/Generator/ServerGenerator.hs index 039775296d..e3eadc50f3 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator.hs @@ -211,7 +211,8 @@ genSrcDir spec = genFileCopy [relfile|core/AuthError.js|], genFileCopy [relfile|core/HttpError.js|], genConfigFile spec, - genServerJs spec + genServerJs spec, + genFileCopy [relfile|polyfill.ts|] ] <++> genRoutesDir spec <++> genOperationsRoutes spec diff --git a/waspc/src/Wasp/Generator/ServerGenerator/Auth/EmailAuthG.hs b/waspc/src/Wasp/Generator/ServerGenerator/Auth/EmailAuthG.hs index e37f9b4efa..26226783fe 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/Auth/EmailAuthG.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/Auth/EmailAuthG.hs @@ -61,7 +61,8 @@ genEmailAuthConfig spec emailAuthConfig = return $ C.mkTmplFdWithDstAndData tmpl "passwordResetClientRoute" .= passwordResetClientRoute, "getPasswordResetEmailContent" .= getPasswordResetEmailContent, "getVerificationEmailContent" .= getVerificationEmailContent, - "allowUnverifiedLogin" .= fromMaybe False (AS.Auth.allowUnverifiedLogin emailAuthConfig) + "userSignupFields" .= extImportToImportJson relPathToServerSrcDir maybeUserSignupFields, + "isDevelopment" .= isDevelopment ] fromFieldJson = @@ -74,10 +75,13 @@ genEmailAuthConfig spec emailAuthConfig = return $ C.mkTmplFdWithDstAndData tmpl maybeName = AS.EmailSender.name fromField email = AS.EmailSender.email fromField + isDevelopment = not $ AS.isBuild spec + emailVerificationClientRoute = getRoutePathFromRef spec $ AS.Auth.EmailVerification.clientRoute emailVerification passwordResetClientRoute = getRoutePathFromRef spec $ AS.Auth.PasswordReset.clientRoute passwordReset getPasswordResetEmailContent = extImportToImportJson relPathToServerSrcDir $ AS.Auth.PasswordReset.getEmailContentFn passwordReset getVerificationEmailContent = extImportToImportJson relPathToServerSrcDir $ AS.Auth.EmailVerification.getEmailContentFn emailVerification + maybeUserSignupFields = AS.Auth.userSignupFieldsForEmailAuth emailAuthConfig emailVerification = AS.Auth.emailVerification emailAuthConfig passwordReset = AS.Auth.passwordReset emailAuthConfig diff --git a/waspc/src/Wasp/Generator/ServerGenerator/Auth/LocalAuthG.hs b/waspc/src/Wasp/Generator/ServerGenerator/Auth/LocalAuthG.hs index 9454616204..4209e81cfa 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/Auth/LocalAuthG.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/Auth/LocalAuthG.hs @@ -5,10 +5,14 @@ where import Data.Aeson (object, (.=)) import StrongPath - ( File', + ( Dir, + File', + Path, Path', + Posix, Rel, reldir, + reldirP, relfile, (), ) @@ -20,20 +24,23 @@ import qualified Wasp.Generator.AuthProviders.Local as Local import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.Monad (Generator) import qualified Wasp.Generator.ServerGenerator.Common as C +import Wasp.Generator.ServerGenerator.JsImport (extImportToImportJson) import qualified Wasp.Util as Util genLocalAuth :: AS.Auth.Auth -> Generator [FileDraft] -genLocalAuth auth - | AS.Auth.isUsernameAndPasswordAuthEnabled auth = - sequence - [ genLoginRoute auth, - genSignupRoute auth, - genLocalAuthConfig - ] - | otherwise = return [] +genLocalAuth auth = case usernameAndPasswordAuth of + Just usernameAndPasswordAuthConfig -> + sequence + [ genLocalAuthConfig usernameAndPasswordAuthConfig, + genLoginRoute auth, + genSignupRoute auth + ] + Nothing -> return [] + where + usernameAndPasswordAuth = AS.Auth.usernameAndPassword $ AS.Auth.methods auth -genLocalAuthConfig :: Generator FileDraft -genLocalAuthConfig = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) +genLocalAuthConfig :: AS.Auth.UsernameAndPasswordConfig -> Generator FileDraft +genLocalAuthConfig usernameAndPasswordConfig = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) where tmplFile = C.srcDirInServerTemplatesDir SP.castRel authIndexFileInSrcDir dstFile = C.serverSrcDirInServerRootDir authIndexFileInSrcDir @@ -41,12 +48,18 @@ genLocalAuthConfig = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tm tmplData = object [ "providerId" .= Local.providerId localAuthProvider, - "displayName" .= Local.displayName localAuthProvider + "displayName" .= Local.displayName localAuthProvider, + "userSignupFields" .= extImportToImportJson relPathToServerSrcDir maybeUserSignupFields ] + maybeUserSignupFields = AS.Auth.userSignupFieldsForUsernameAuth usernameAndPasswordConfig + authIndexFileInSrcDir :: Path' (Rel C.ServerSrcDir) File' authIndexFileInSrcDir = [relfile|auth/providers/config/username.ts|] + relPathToServerSrcDir :: Path Posix (Rel importLocation) (Dir C.ServerSrcDir) + relPathToServerSrcDir = [reldirP|../../../|] + genLoginRoute :: AS.Auth.Auth -> Generator FileDraft genLoginRoute auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) where diff --git a/waspc/src/Wasp/Generator/ServerGenerator/Auth/OAuthAuthG.hs b/waspc/src/Wasp/Generator/ServerGenerator/Auth/OAuthAuthG.hs index ec052152e1..6004c9c70a 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/Auth/OAuthAuthG.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/Auth/OAuthAuthG.hs @@ -111,10 +111,10 @@ genOAuthConfig provider maybeUserConfig pathToConfigDst = return $ C.mkTmplFdWit "npmPackage" .= App.Dependency.name (OAuth.passportDependency provider), "oAuthConfigProps" .= getJsonForOAuthConfigProps provider, "configFn" .= extImportToImportJson relPathFromAuthConfigToServerSrcDir maybeConfigFn, - "userFieldsFn" .= extImportToImportJson relPathFromAuthConfigToServerSrcDir maybeGetUserFieldsFn + "userSignupFields" .= extImportToImportJson relPathFromAuthConfigToServerSrcDir maybeUserSignupFields ] maybeConfigFn = AS.Auth.configFn =<< maybeUserConfig - maybeGetUserFieldsFn = AS.Auth.getUserFieldsFn =<< maybeUserConfig + maybeUserSignupFields = AS.Auth.userSignupFieldsForExternalAuth =<< maybeUserConfig relPathFromAuthConfigToServerSrcDir :: Path Posix (Rel importLocation) (Dir C.ServerSrcDir) relPathFromAuthConfigToServerSrcDir = [reldirP|../../../|] diff --git a/waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs b/waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs index 9c2fc4abd0..83560ffc7c 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs @@ -1,5 +1,6 @@ module Wasp.Generator.ServerGenerator.AuthG ( genAuth, + depsRequiredByAuth, ) where @@ -10,7 +11,6 @@ import StrongPath Path', Rel, reldir, - reldirP, relfile, (), ) @@ -19,6 +19,7 @@ import Wasp.AppSpec (AppSpec) import qualified Wasp.AppSpec as AS import qualified Wasp.AppSpec.App as AS.App import qualified Wasp.AppSpec.App.Auth as AS.Auth +import qualified Wasp.AppSpec.App.Dependency as AS.Dependency import Wasp.AppSpec.Valid (getApp) import Wasp.Generator.AuthProviders (emailAuthProvider, gitHubAuthProvider, googleAuthProvider, localAuthProvider) import qualified Wasp.Generator.AuthProviders.Email as EmailProvider @@ -31,7 +32,6 @@ import Wasp.Generator.ServerGenerator.Auth.EmailAuthG (genEmailAuth) import Wasp.Generator.ServerGenerator.Auth.LocalAuthG (genLocalAuth) import Wasp.Generator.ServerGenerator.Auth.OAuthAuthG (genOAuthAuth) import qualified Wasp.Generator.ServerGenerator.Common as C -import Wasp.Generator.ServerGenerator.JsImport (extImportToImportJson) import Wasp.Util ((<++>)) import qualified Wasp.Util as Util @@ -41,12 +41,17 @@ genAuth spec = case maybeAuth of sequence [ genCoreAuth auth, genAuthRoutesIndex auth, - genMeRoute auth, + genFileCopy [relfile|routes/auth/me.js|], + genFileCopy [relfile|routes/auth/logout.ts|], genUtils auth, genProvidersIndex auth, genProvidersTypes auth, genFileCopy [relfile|auth/validation.ts|], - genFileCopy [relfile|auth/user.ts|] + genFileCopy [relfile|auth/user.ts|], + genFileCopy [relfile|auth/password.ts|], + genFileCopy [relfile|auth/jwt.ts|], + genSessionTs auth, + genLuciaTs auth ] <++> genIndexTs auth <++> genLocalAuth auth @@ -69,9 +74,7 @@ genCoreAuth auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmpl let userEntityName = AS.refName $ AS.Auth.userEntity auth in object [ "userEntityUpper" .= (userEntityName :: String), - "userEntityLower" .= (Util.toLowerFirst userEntityName :: String), - "authFieldOnUserEntityName" .= (DbAuth.authFieldOnUserEntityName :: String), - "identitiesFieldOnAuthEntityName" .= (DbAuth.identitiesFieldOnAuthEntityName :: String) + "userEntityLower" .= (Util.toLowerFirst userEntityName :: String) ] genAuthRoutesIndex :: AS.Auth.Auth -> Generator FileDraft @@ -85,15 +88,6 @@ genAuthRoutesIndex auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Ju authIndexFileInSrcDir :: Path' (Rel C.ServerSrcDir) File' authIndexFileInSrcDir = [relfile|routes/auth/index.js|] -genMeRoute :: AS.Auth.Auth -> Generator FileDraft -genMeRoute auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) - where - meRouteRelToSrc = [relfile|routes/auth/me.js|] - tmplFile = C.asTmplFile $ [reldir|src|] meRouteRelToSrc - dstFile = C.serverSrcDirInServerRootDir C.asServerSrcFile meRouteRelToSrc - - tmplData = object ["userEntityLower" .= (Util.toLowerFirst (AS.refName $ AS.Auth.userEntity auth) :: String)] - genUtils :: AS.Auth.Auth -> Generator FileDraft genUtils auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) where @@ -112,15 +106,12 @@ genUtils auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplDat "authFieldOnUserEntityName" .= (DbAuth.authFieldOnUserEntityName :: String), "identitiesFieldOnAuthEntityName" .= (DbAuth.identitiesFieldOnAuthEntityName :: String), "failureRedirectPath" .= AS.Auth.onAuthFailedRedirectTo auth, - "successRedirectPath" .= getOnAuthSucceededRedirectToOrDefault auth, - "additionalSignupFields" .= extImportToImportJson [reldirP|../|] additionalSignupFields + "successRedirectPath" .= getOnAuthSucceededRedirectToOrDefault auth ] utilsFileInSrcDir :: Path' (Rel C.ServerSrcDir) File' utilsFileInSrcDir = [relfile|auth/utils.ts|] - additionalSignupFields = AS.Auth.signup auth >>= AS.Auth.additionalFields - genIndexTs :: AS.Auth.Auth -> Generator [FileDraft] genIndexTs auth = return $ @@ -158,3 +149,38 @@ genProvidersTypes auth = return $ C.mkTmplFdWithData [relfile|src/auth/providers userEntityName = AS.refName $ AS.Auth.userEntity auth tmplData = object ["userEntityUpper" .= (userEntityName :: String)] + +genLuciaTs :: AS.Auth.Auth -> Generator FileDraft +genLuciaTs auth = return $ C.mkTmplFdWithData [relfile|src/auth/lucia.ts|] (Just tmplData) + where + tmplData = + object + [ "sessionEntityLower" .= (Util.toLowerFirst DbAuth.sessionEntityName :: String), + "authEntityLower" .= (Util.toLowerFirst DbAuth.authEntityName :: String), + "userEntityUpper" .= (userEntityName :: String) + ] + + userEntityName = AS.refName $ AS.Auth.userEntity auth + +genSessionTs :: AS.Auth.Auth -> Generator FileDraft +genSessionTs auth = return $ C.mkTmplFdWithData [relfile|src/auth/session.ts|] (Just tmplData) + where + tmplData = + object + [ "userEntityUpper" .= userEntityName, + "userEntityLower" .= Util.toLowerFirst userEntityName, + "authFieldOnUserEntityName" .= DbAuth.authFieldOnUserEntityName, + "identitiesFieldOnAuthEntityName" .= DbAuth.identitiesFieldOnAuthEntityName + ] + + userEntityName = AS.refName $ AS.Auth.userEntity auth + +depsRequiredByAuth :: AppSpec -> [AS.Dependency.Dependency] +depsRequiredByAuth spec = maybe [] (const authDeps) maybeAuth + where + maybeAuth = AS.App.auth $ snd $ getApp spec + authDeps = + AS.Dependency.fromList + [ ("lucia", "^3.0.0-beta.14"), + ("@lucia-auth/adapter-prisma", "^4.0.0-beta.9") + ] diff --git a/waspc/src/Wasp/Generator/ServerGenerator/EmailSender/Providers.hs b/waspc/src/Wasp/Generator/ServerGenerator/EmailSender/Providers.hs index afcff7682a..f12c26ed36 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/EmailSender/Providers.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/EmailSender/Providers.hs @@ -2,6 +2,7 @@ module Wasp.Generator.ServerGenerator.EmailSender.Providers ( smtp, sendGrid, mailgun, + dummy, providersDirInServerSrc, EmailSenderProvider (..), ) @@ -13,7 +14,7 @@ import qualified Wasp.Generator.ServerGenerator.Common as C import qualified Wasp.SemanticVersion as SV data EmailSenderProvider = EmailSenderProvider - { npmDependency :: AS.Dependency.Dependency, + { npmDependency :: Maybe AS.Dependency.Dependency, setupFnFile :: Path' (Rel ProvidersDir) File', -- We have to use explicit boolean keys in templates (e.g. "isSMTPProviderEnabled") so each -- provider provides its own key which we pass to the template. @@ -26,7 +27,7 @@ data ProvidersDir smtp :: EmailSenderProvider smtp = EmailSenderProvider - { npmDependency = nodeMailerDependency, + { npmDependency = Just nodeMailerDependency, setupFnFile = [relfile|smtp.ts|], isEnabledKey = "isSmtpProviderUsed" } @@ -40,7 +41,7 @@ smtp = sendGrid :: EmailSenderProvider sendGrid = EmailSenderProvider - { npmDependency = sendGridDependency, + { npmDependency = Just sendGridDependency, setupFnFile = [relfile|sendgrid.ts|], isEnabledKey = "isSendGridProviderUsed" } @@ -54,7 +55,7 @@ sendGrid = mailgun :: EmailSenderProvider mailgun = EmailSenderProvider - { npmDependency = mailgunDependency, + { npmDependency = Just mailgunDependency, setupFnFile = [relfile|mailgun.ts|], isEnabledKey = "isMailgunProviderUsed" } @@ -65,5 +66,13 @@ mailgun = mailgunDependency :: AS.Dependency.Dependency mailgunDependency = AS.Dependency.make ("ts-mailgun", show mailgunVersionRange) +dummy :: EmailSenderProvider +dummy = + EmailSenderProvider + { npmDependency = Nothing, + setupFnFile = [relfile|dummy.ts|], + isEnabledKey = "isDummyProviderUsed" + } + providersDirInServerSrc :: Path' (Rel C.ServerTemplatesSrcDir) (Dir ProvidersDir) providersDirInServerSrc = [reldir|email/core/providers|] diff --git a/waspc/src/Wasp/Generator/ServerGenerator/EmailSenderG.hs b/waspc/src/Wasp/Generator/ServerGenerator/EmailSenderG.hs index 63c59df100..0ba63ee8bc 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/EmailSenderG.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/EmailSenderG.hs @@ -39,8 +39,7 @@ genCore email = sequence [ genCoreIndex email, genCoreTypes email, - genCoreHelpers email, - genFileCopy [relfile|email/core/providers/dummy.ts|] + genCoreHelpers email ] <++> genEmailSenderProviderSetupFn email @@ -94,7 +93,7 @@ depsRequiredByEmail spec = maybeToList maybeNpmDepedency where maybeProvider :: Maybe Providers.EmailSenderProvider maybeProvider = getEmailSenderProvider <$> (AS.App.emailSender . snd . getApp $ spec) - maybeNpmDepedency = Providers.npmDependency <$> maybeProvider + maybeNpmDepedency = maybeProvider >>= Providers.npmDependency getEmailProvidersJson :: EmailSender -> Aeson.Value getEmailProvidersJson email = @@ -109,6 +108,7 @@ getEmailSenderProvider email = case AS.EmailSender.provider email of AS.EmailSender.SMTP -> Providers.smtp AS.EmailSender.SendGrid -> Providers.sendGrid AS.EmailSender.Mailgun -> Providers.mailgun + AS.EmailSender.Dummy -> Providers.dummy genFileCopy :: Path' (Rel C.ServerTemplatesSrcDir) File' -> Generator FileDraft genFileCopy = return . C.mkSrcTmplFd diff --git a/waspc/src/Wasp/Generator/WebAppGenerator.hs b/waspc/src/Wasp/Generator/WebAppGenerator.hs index 5ea41861e2..49848e41aa 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator.hs @@ -147,7 +147,7 @@ npmDepsForWasp spec = ("@types/react", "^18.0.37"), ("@types/react-dom", "^18.0.11"), ("@types/react-router-dom", "^5.3.3"), - ("@vitejs/plugin-react-swc", "^3.0.0"), + ("@vitejs/plugin-react", "^4.2.1"), ("dotenv", "^16.0.3"), -- NOTE: Make sure to bump the version of the tsconfig -- when updating Vite or React versions diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/Auth/AuthFormsG.hs b/waspc/src/Wasp/Generator/WebAppGenerator/Auth/AuthFormsG.hs index b82ca2e54b..a208440260 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/Auth/AuthFormsG.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/Auth/AuthFormsG.hs @@ -118,8 +118,7 @@ genLoginSignupForm auth = -- Username and password "isUsernameAndPasswordAuthEnabled" .= AS.Auth.isUsernameAndPasswordAuthEnabled auth, -- Email - "isEmailAuthEnabled" .= AS.Auth.isEmailAuthEnabled auth, - "isEmailVerificationRequired" .= AS.Auth.isEmailVerificationRequired auth + "isEmailAuthEnabled" .= AS.Auth.isEmailAuthEnabled auth ] areBothSocialAndPasswordBasedAuthEnabled = AS.Auth.isExternalAuthEnabled auth && isAnyPasswordBasedAuthEnabled isAnyPasswordBasedAuthEnabled = AS.Auth.isUsernameAndPasswordAuthEnabled auth || AS.Auth.isEmailAuthEnabled auth diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/AuthG.hs b/waspc/src/Wasp/Generator/WebAppGenerator/AuthG.hs index 72004178f2..6a67b77f12 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/AuthG.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/AuthG.hs @@ -26,10 +26,10 @@ genAuth spec = Nothing -> return [] Just auth -> sequence - [ genFileCopy [relfile|auth/logout.ts|], - genFileCopy [relfile|auth/helpers/user.ts|], + [ genFileCopy [relfile|auth/helpers/user.ts|], genFileCopy [relfile|auth/types.ts|], genFileCopy [relfile|auth/user.ts|], + genFileCopy [relfile|auth/logout.ts|], genUseAuth auth, genCreateAuthRequiredPage auth ] diff --git a/waspc/src/Wasp/Project/Analyze.hs b/waspc/src/Wasp/Project/Analyze.hs index 7261d889b0..1f799610aa 100644 --- a/waspc/src/Wasp/Project/Analyze.hs +++ b/waspc/src/Wasp/Project/Analyze.hs @@ -2,6 +2,7 @@ module Wasp.Project.Analyze ( analyzeWaspProject, readPackageJsonFile, analyzeWaspFileContent, + findWaspFile, ) where @@ -9,7 +10,6 @@ import Control.Arrow (ArrowChoice (left)) import qualified Data.Aeson as Aeson import Data.List (find, isSuffixOf) import StrongPath (Abs, Dir, File', Path', toFilePath, ()) -import StrongPath.TH (relfile) import qualified Wasp.Analyzer as Analyzer import Wasp.Analyzer.AnalyzeError (getErrorMessageAndCtx) import Wasp.Analyzer.Parser.Ctx (Ctx) @@ -21,7 +21,7 @@ import qualified Wasp.CompileOptions as CompileOptions import qualified Wasp.ConfigFile as CF import Wasp.Error (showCompilerErrorForTerminal) import qualified Wasp.Generator.ConfigFile as G.CF -import Wasp.Project.Common (CompileError, CompileWarning, WaspProjectDir, findFileInWaspProjectDir) +import Wasp.Project.Common (CompileError, CompileWarning, WaspProjectDir, findFileInWaspProjectDir, packageJsonInWaspProjectDir) import Wasp.Project.Db (makeDevDatabaseUrl) import Wasp.Project.Db.Migrations (findMigrationsDir) import Wasp.Project.Deployment (loadUserDockerfileContents) @@ -119,9 +119,9 @@ analyzePackageJsonContent waspProjectDir = Nothing -> return $ Left [fileNotFoundMessage] where fileNotFoundMessage = "couldn't find package.json file in the " ++ toFilePath waspProjectDir ++ " directory" - findPackageJsonFile = findFileInWaspProjectDir waspProjectDir [relfile|package.json|] + findPackageJsonFile = findFileInWaspProjectDir waspProjectDir packageJsonInWaspProjectDir readPackageJsonFile :: Path' Abs File' -> IO (Either [CompileError] PackageJson) readPackageJsonFile packageJsonFile = do byteString <- IOUtil.readFileBytes packageJsonFile - return $ maybeToEither ["Error reading the package.json file"] $ Aeson.decode byteString + return $ maybeToEither ["Error parsing the package.json file"] $ Aeson.decode byteString diff --git a/waspc/src/Wasp/Project/Common.hs b/waspc/src/Wasp/Project/Common.hs index 84b959484c..69aeaab6db 100644 --- a/waspc/src/Wasp/Project/Common.hs +++ b/waspc/src/Wasp/Project/Common.hs @@ -1,24 +1,77 @@ module Wasp.Project.Common - ( findFileInWaspProjectDir, - extCodeDirInWaspProjectDir, - extPublicDirInWaspProjectDir, + ( WaspProjectDir, + DotWaspDir, + NodeModulesDir, CompileError, CompileWarning, - WaspProjectDir, + findFileInWaspProjectDir, + dotWaspDirInWaspProjectDir, + generatedCodeDirInDotWaspDir, + buildDirInDotWaspDir, + waspProjectDirFromProjectRootDir, + dotWaspRootFileInWaspProjectDir, + dotWaspInfoFileInGeneratedCodeDir, + packageJsonInWaspProjectDir, + nodeModulesDirInWaspProjectDir, + extCodeDirInWaspProjectDir, + extPublicDirInWaspProjectDir, ) where -import StrongPath (Abs, Dir, File', Path', Rel, toFilePath, ()) -import StrongPath.TH (reldir) +import StrongPath (Abs, Dir, File', Path', Rel, reldir, relfile, toFilePath, ()) import System.Directory (doesFileExist) import Wasp.AppSpec.ExternalFiles (SourceExternalCodeDir, SourceExternalPublicDir) - -data WaspProjectDir -- Root dir of Wasp project, containing source files. +import qualified Wasp.Generator.Common type CompileError = String type CompileWarning = String +data WaspProjectDir -- Root dir of Wasp project, containing source files. + +data NodeModulesDir + +data DotWaspDir -- Here we put everything that wasp generates. + +-- | NOTE: If you change the depth of this path, also update @waspProjectDirFromProjectRootDir@ below. +-- TODO: SHould this be renamed to include word "root"? +dotWaspDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir DotWaspDir) +dotWaspDirInWaspProjectDir = [reldir|.wasp|] + +nodeModulesDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir NodeModulesDir) +nodeModulesDirInWaspProjectDir = [reldir|node_modules|] + +-- | NOTE: If you change the depth of this path, also update @waspProjectDirFromProjectRootDir@ below. +-- TODO: Hm this has different name than it has in Generator. +generatedCodeDirInDotWaspDir :: Path' (Rel DotWaspDir) (Dir Wasp.Generator.Common.ProjectRootDir) +generatedCodeDirInDotWaspDir = [reldir|out|] + +-- | NOTE: If you change the depth of this path, also update @waspProjectDirFromProjectRootDir@ below. +buildDirInDotWaspDir :: Path' (Rel DotWaspDir) (Dir Wasp.Generator.Common.ProjectRootDir) +buildDirInDotWaspDir = [reldir|build|] + +-- | NOTE: This path is calculated from the values of @dotWaspDirInWaspProjectDir@, +-- @generatedCodeDirInDotWaspDir@ and @buildDirInDotWaspDir@., which are the three functions just above. +-- Also, it assumes @generatedCodeDirInDotWaspDir@ and @buildDirInDotWaspDir@ have same depth. +-- If any of those change significantly (their depth), this path should be adjusted. +waspProjectDirFromProjectRootDir :: Path' (Rel Wasp.Generator.Common.ProjectRootDir) (Dir WaspProjectDir) +waspProjectDirFromProjectRootDir = [reldir|../../|] + +dotWaspRootFileInWaspProjectDir :: Path' (Rel WaspProjectDir) File' +dotWaspRootFileInWaspProjectDir = [relfile|.wasproot|] + +dotWaspInfoFileInGeneratedCodeDir :: Path' (Rel Wasp.Generator.Common.ProjectRootDir) File' +dotWaspInfoFileInGeneratedCodeDir = [relfile|.waspinfo|] + +packageJsonInWaspProjectDir :: Path' (Rel WaspProjectDir) File' +packageJsonInWaspProjectDir = [relfile|package.json|] + +extCodeDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir SourceExternalCodeDir) +extCodeDirInWaspProjectDir = [reldir|src|] + +extPublicDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir SourceExternalPublicDir) +extPublicDirInWaspProjectDir = [reldir|public|] + findFileInWaspProjectDir :: Path' Abs (Dir WaspProjectDir) -> Path' (Rel WaspProjectDir) File' -> @@ -27,9 +80,3 @@ findFileInWaspProjectDir waspDir file = do let fileAbsFp = waspDir file fileExists <- doesFileExist $ toFilePath fileAbsFp return $ if fileExists then Just fileAbsFp else Nothing - -extCodeDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir SourceExternalCodeDir) -extCodeDirInWaspProjectDir = [reldir|src|] - -extPublicDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir SourceExternalPublicDir) -extPublicDirInWaspProjectDir = [reldir|public|] diff --git a/waspc/src/Wasp/Util/Terminal.hs b/waspc/src/Wasp/Util/Terminal.hs index c9a17a629e..0713cf65a3 100644 --- a/waspc/src/Wasp/Util/Terminal.hs +++ b/waspc/src/Wasp/Util/Terminal.hs @@ -18,6 +18,14 @@ data Style | Magenta | Cyan | White + | BlackBg + | RedBg + | GreenBg + | YellowBg + | BlueBg + | MagentaBg + | CyanBg + | WhiteBg | Bold | Underline | Blink @@ -41,6 +49,14 @@ styleCode Blue = "[34m" styleCode Magenta = "[35m" styleCode Cyan = "[36m" styleCode White = "[37m" +styleCode BlackBg = "[40m" +styleCode RedBg = "[41m" +styleCode GreenBg = "[42m" +styleCode YellowBg = "[43m" +styleCode BlueBg = "[44m" +styleCode MagentaBg = "[45m" +styleCode CyanBg = "[46m" +styleCode WhiteBg = "[47m" styleCode Bold = "[1m" styleCode Underline = "[4m" styleCode Blink = "[5m" -- Blink does not work in all terminal emulators (e.g. on mac in iTerm2). diff --git a/waspc/test/AI/GenerateNewProject/PageComponentFileTest.hs b/waspc/test/AI/GenerateNewProject/PageComponentFileTest.hs index 99c7b39c7a..000b8e4207 100644 --- a/waspc/test/AI/GenerateNewProject/PageComponentFileTest.hs +++ b/waspc/test/AI/GenerateNewProject/PageComponentFileTest.hs @@ -9,7 +9,7 @@ spec_PageComponentFileTest :: Spec spec_PageComponentFileTest = do describe "getPageComponentFileContentWithFixedImports" $ do let mockAllPossibleWaspClientImports = - M.fromList $ + M.fromList [ ("useQuery", "import { useQuery } from '@wasp/queries';"), ("useAction", "import { useAction } from '@wasp/actions';"), ("useAuth", "import useAuth from '@wasp/auth/useAuth';"), diff --git a/waspc/test/AnalyzerTest.hs b/waspc/test/AnalyzerTest.hs index a82f4ed41a..4c33aa30cb 100644 --- a/waspc/test/AnalyzerTest.hs +++ b/waspc/test/AnalyzerTest.hs @@ -9,6 +9,7 @@ import Data.List (intercalate) import Data.Maybe (fromJust) import qualified StrongPath as SP import Test.Tasty.Hspec +import qualified Wasp.AI.GenerateNewProject.Common as Auth import Wasp.Analyzer import Wasp.Analyzer.Parser (Ctx) import qualified Wasp.Analyzer.TypeChecker as TC @@ -48,11 +49,12 @@ spec_Analyzer = do " head: [\"foo\", \"bar\"],", " auth: {", " userEntity: User,", - " methods: { usernameAndPassword: {} },", - " onAuthFailedRedirectTo: \"/\",", - " signup: {", - " additionalFields: import { fields } from \"@server/auth/signup.js\",", + " methods: {", + " usernameAndPassword: {", + " userSignupFields: import { getUserFields } from \"@server/auth/signup.js\",", + " }", " },", + " onAuthFailedRedirectTo: \"/\",", " },", " dependencies: [", " (\"redux\", \"^4.0.5\")", @@ -139,15 +141,13 @@ spec_Analyzer = do Auth.Auth { Auth.userEntity = Ref "User" :: Ref Entity, Auth.externalAuthEntity = Nothing, - Auth.signup = - Just $ - Auth.SignupOptions - { Auth.additionalFields = - Just $ ExtImport (ExtImportField "fields") (fromJust $ SP.parseRelFileP "auth/signup.js") - }, Auth.methods = Auth.AuthMethods - { Auth.usernameAndPassword = Just Auth.usernameAndPasswordConfig, + { Auth.usernameAndPassword = + Just + Auth.UsernameAndPasswordConfig + { Auth.userSignupFields = Just $ ExtImport (ExtImportField "getUserFields") (fromJust $ SP.parseRelFileP "auth/signup.js") + }, Auth.google = Nothing, Auth.gitHub = Nothing, Auth.email = Nothing diff --git a/waspc/test/AppSpec/ValidTest.hs b/waspc/test/AppSpec/ValidTest.hs index fb2a07d490..5c789aee31 100644 --- a/waspc/test/AppSpec/ValidTest.hs +++ b/waspc/test/AppSpec/ValidTest.hs @@ -99,14 +99,13 @@ spec_AppSpecValid = do AS.Auth.externalAuthEntity = Nothing, AS.Auth.methods = AS.Auth.AuthMethods - { AS.Auth.usernameAndPassword = Just AS.Auth.usernameAndPasswordConfig, + { AS.Auth.usernameAndPassword = Just AS.Auth.UsernameAndPasswordConfig {AS.Auth.userSignupFields = Nothing}, AS.Auth.google = Nothing, AS.Auth.gitHub = Nothing, AS.Auth.email = Nothing }, AS.Auth.onAuthFailedRedirectTo = "/", - AS.Auth.onAuthSucceededRedirectTo = Nothing, - AS.Auth.signup = Nothing + AS.Auth.onAuthSucceededRedirectTo = Nothing } describe "should validate that when a page has authRequired, app.auth is also set." $ do @@ -149,8 +148,7 @@ spec_AppSpecValid = do AS.Auth.userEntity = AS.Core.Ref.Ref userEntityName, AS.Auth.externalAuthEntity = Nothing, AS.Auth.onAuthFailedRedirectTo = "/", - AS.Auth.onAuthSucceededRedirectTo = Nothing, - AS.Auth.signup = Nothing + AS.Auth.onAuthSucceededRedirectTo = Nothing }, AS.App.emailSender = Just @@ -166,7 +164,8 @@ spec_AppSpecValid = do } let emailAuthConfig = AS.Auth.EmailAuthConfig - { AS.Auth.fromField = + { AS.Auth.userSignupFields = Nothing, + AS.Auth.fromField = AS.EmailSender.EmailFromField { AS.EmailSender.email = "dummy@info.com", AS.EmailSender.name = Nothing @@ -180,19 +179,47 @@ spec_AppSpecValid = do AS.Auth.PasswordReset.PasswordResetConfig { AS.Auth.PasswordReset.clientRoute = AS.Core.Ref.Ref basicRouteName, AS.Auth.PasswordReset.getEmailContentFn = Nothing - }, - AS.Auth.allowUnverifiedLogin = Nothing + } } it "returns no error if app.auth is not set" $ do ASV.validateAppSpec (makeSpec (AS.Auth.AuthMethods {usernameAndPassword = Nothing, google = Nothing, gitHub = Nothing, email = Nothing}) validUserEntity) `shouldBe` [] it "returns no error if app.auth is set and only one of UsernameAndPassword and Email is used" $ do - ASV.validateAppSpec (makeSpec (AS.Auth.AuthMethods {usernameAndPassword = Just AS.Auth.usernameAndPasswordConfig, google = Nothing, gitHub = Nothing, email = Nothing}) validUserEntity) `shouldBe` [] + ASV.validateAppSpec + ( makeSpec + ( AS.Auth.AuthMethods + { usernameAndPassword = + Just + AS.Auth.UsernameAndPasswordConfig + { AS.Auth.userSignupFields = Nothing + }, + google = Nothing, + gitHub = Nothing, + email = Nothing + } + ) + validUserEntity + ) + `shouldBe` [] ASV.validateAppSpec (makeSpec (AS.Auth.AuthMethods {usernameAndPassword = Nothing, google = Nothing, gitHub = Nothing, email = Just emailAuthConfig}) validUserEntity) `shouldBe` [] it "returns an error if app.auth is set and both UsernameAndPassword and Email are used" $ do - ASV.validateAppSpec (makeSpec (AS.Auth.AuthMethods {usernameAndPassword = Just AS.Auth.usernameAndPasswordConfig, google = Nothing, gitHub = Nothing, email = Just emailAuthConfig}) validUserEntity) + ASV.validateAppSpec + ( makeSpec + ( AS.Auth.AuthMethods + { usernameAndPassword = + Just + AS.Auth.UsernameAndPasswordConfig + { AS.Auth.userSignupFields = Nothing + }, + google = Nothing, + gitHub = Nothing, + email = Just emailAuthConfig + } + ) + validUserEntity + ) `shouldContain` [ASV.GenericValidationError "Expected app.auth to use either email or username and password authentication, but not both."] describe "should validate that when app.auth is using UsernameAndPassword, user entity is of valid shape." $ do @@ -220,6 +247,77 @@ spec_AppSpecValid = do `shouldBe` [ ASV.GenericValidationError "Entity 'User' (referenced by app.auth.userEntity) must have an ID field (specified with the '@id' attribute)" ] + + describe "should validate email sender setup." $ do + let emailAuthConfig = + AS.Auth.EmailAuthConfig + { AS.Auth.userSignupFields = Nothing, + AS.Auth.fromField = + AS.EmailSender.EmailFromField + { AS.EmailSender.email = "dummy@info.com", + AS.EmailSender.name = Nothing + }, + AS.Auth.emailVerification = + AS.Auth.EmailVerification.EmailVerificationConfig + { AS.Auth.EmailVerification.clientRoute = AS.Core.Ref.Ref basicRouteName, + AS.Auth.EmailVerification.getEmailContentFn = Nothing + }, + AS.Auth.passwordReset = + AS.Auth.PasswordReset.PasswordResetConfig + { AS.Auth.PasswordReset.clientRoute = AS.Core.Ref.Ref basicRouteName, + AS.Auth.PasswordReset.getEmailContentFn = Nothing + } + } + + let makeSpec emailSender isBuild = + basicAppSpec + { AS.isBuild = isBuild, + AS.decls = + [ AS.Decl.makeDecl "TestApp" $ + basicApp + { AS.App.auth = + Just + AS.Auth.Auth + { AS.Auth.methods = + AS.Auth.AuthMethods {email = Just emailAuthConfig, usernameAndPassword = Nothing, google = Nothing, gitHub = Nothing}, + AS.Auth.userEntity = AS.Core.Ref.Ref userEntityName, + AS.Auth.externalAuthEntity = Nothing, + AS.Auth.onAuthFailedRedirectTo = "/", + AS.Auth.onAuthSucceededRedirectTo = Nothing + }, + AS.App.emailSender = emailSender + }, + AS.Decl.makeDecl userEntityName $ + AS.Entity.makeEntity + ( PslM.Body + [ PslM.ElementField $ makeIdField "id" PslM.String + ] + ), + basicPageDecl, + basicRouteDecl + ] + } + let mailgunEmailSender = + AS.EmailSender.EmailSender + { AS.EmailSender.provider = AS.EmailSender.Mailgun, + AS.EmailSender.defaultFrom = Nothing + } + + let dummyEmailSender = + AS.EmailSender.EmailSender + { AS.EmailSender.provider = AS.EmailSender.Dummy, + AS.EmailSender.defaultFrom = Nothing + } + + it "returns an error if no email sender is set but email auth is used" $ do + ASV.validateAppSpec (makeSpec Nothing False) `shouldBe` [ASV.GenericValidationError "app.emailSender must be specified when using email auth. You can use the Dummy email sender for development purposes."] + it "returns no error if email sender is defined while using email auth" $ do + ASV.validateAppSpec (makeSpec (Just mailgunEmailSender) False) `shouldBe` [] + it "returns no error if the Dummy email sender is used in development" $ do + ASV.validateAppSpec (makeSpec (Just dummyEmailSender) False) `shouldBe` [] + it "returns an error if the Dummy email sender is used when building the app" $ do + ASV.validateAppSpec (makeSpec (Just dummyEmailSender) True) + `shouldBe` [ASV.GenericValidationError "app.emailSender must not be set to Dummy when building for production."] where makeIdField name typ = PslM.Field diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index c082573810..37d49460f6 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -491,13 +491,11 @@ library cli-lib Wasp.Cli.Command.CreateNewProject.Common Wasp.Cli.Command.CreateNewProject.ProjectDescription Wasp.Cli.Command.CreateNewProject.StarterTemplates + Wasp.Cli.Command.CreateNewProject.StarterTemplates.GhRepo Wasp.Cli.Command.CreateNewProject.StarterTemplates.Local - Wasp.Cli.Command.CreateNewProject.StarterTemplates.Remote - Wasp.Cli.Command.CreateNewProject.StarterTemplates.Remote.Github Wasp.Cli.Command.CreateNewProject.StarterTemplates.Templating Wasp.Cli.Command.Db Wasp.Cli.Command.Db.Migrate - Wasp.Cli.Command.Db.Reset Wasp.Cli.Command.Db.Seed Wasp.Cli.Command.Db.Studio Wasp.Cli.Command.Deps @@ -506,7 +504,6 @@ library cli-lib Wasp.Cli.Command.Info Wasp.Cli.Command.Studio Wasp.Cli.Command.Require - Wasp.Cli.Command.Reset Wasp.Cli.Command.Start Wasp.Cli.Command.Start.Db Wasp.Cli.Command.Telemetry diff --git a/waspc/waspls/src/Wasp/LSP/Diagnostic.hs b/waspc/waspls/src/Wasp/LSP/Diagnostic.hs index b5b3de03d7..09e853d345 100644 --- a/waspc/waspls/src/Wasp/LSP/Diagnostic.hs +++ b/waspc/waspls/src/Wasp/LSP/Diagnostic.hs @@ -119,5 +119,5 @@ waspErrorRange err = clearMissingExtImportDiagnostics :: [WaspDiagnostic] -> [WaspDiagnostic] clearMissingExtImportDiagnostics = filter (not . isMissingImportDiagnostic) where - isMissingImportDiagnostic (MissingExtImportDiagnostic _ _ _) = True + isMissingImportDiagnostic MissingExtImportDiagnostic {} = True isMissingImportDiagnostic _ = False diff --git a/waspc/waspls/src/Wasp/LSP/ServerMonads.hs b/waspc/waspls/src/Wasp/LSP/ServerMonads.hs index 8e4b93de39..5bfa012fb3 100644 --- a/waspc/waspls/src/Wasp/LSP/ServerMonads.hs +++ b/waspc/waspls/src/Wasp/LSP/ServerMonads.hs @@ -1,6 +1,5 @@ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE TypeSynonymInstances #-} module Wasp.LSP.ServerMonads ( -- * LSP Server Monads @@ -106,6 +105,8 @@ sendToReactor act = do rin <- handler $ asks (^. reactorIn) liftIO $ atomically $ writeTChan rin $ ReactorAction $ LSP.runLspT env $ runRLspM stateTVar act +{- HLINT ignore "Redundant <$>" -} + instance HasProjectRootDir HandlerM where -- Returns the folder that contains the active .wasp file, which is assumed -- to be the root of the wasp project. diff --git a/web/README.md b/web/README.md index ecdd5f8b38..9b4dba2e90 100644 --- a/web/README.md +++ b/web/README.md @@ -2,6 +2,12 @@ This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator. +It consists of three main parts: + - Landing page ([src/pages/index.js](src/pages/index.js)) + - Blog ([blog/](blog/)) + - Docs ([docs/](docs/)) + + ### Installation ``` @@ -14,7 +20,8 @@ $ npm install $ npm start ``` -This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. +This command starts a local development server and opens up a browser window. +Most changes are reflected live without having to restart the server. ### Build @@ -36,4 +43,52 @@ First, ensure you are on the `release` branch. Next, run: $ GIT_USER= USE_SSH=true npm run deploy ``` -This command will build the website and push it to the `gh-pages` branch. +This command will build the website and push it to the `gh-pages` branch, +which will get it deployed to https://wasp-lang.dev ! + +### Multiple documentation versions + +We maintain docs for multiple versions of Wasp. + +Docusaurus docs on this: https://docusaurus.io/docs/versioning . + +Docusaurus recognizes "current" docs, which are docs in ./docs dir, and also +individual versioned docs, which are docs under versioned_docs/{version}/. +So we have 1 "current" docs and a number of versioned docs. + +We stick with Docusaurus' recommended/default approach for multiple doc versions, which says that "current" docs, which reside in `docs/` dir and are served under `/docs/next` URL, are work in progress (even though they are named "current", which is a bit misleading). +So "current" docs are docs for the version of Wasp that is currently in development, not for the last released version, and they are not meant to be consumed by typical Wasp user. + +Each versioned documentation consists of versioned_docs/{version} and +versioned_sidebars/{version}. +There is also versions.json file which is just a list of versioned docs. + +By default, "current" docs are served under URL {baseUrl}/docs/next, +each versioned doc is served under URL {baseUrl}/docs/{version}, +and last versioned docs (first version in versions.json) +are served under URL {baseUrl}/docs/, as the default/latest docs. + +Since we don't want our users to read `docs/next` ("current" docs), we don't publish these when we deploy docs, instead we build them only during development. + +#### When/how do we create new version of docs from "current" docs? + +When releasing new version of Wasp, what we do is run `npm run docusaurus docs:version {version}` to create new versioned docs from the current docs. We do this on every new Wasp release. + +This command does everything for us, and since we use Docusaurus' default settings for versions, +there is nothing else we need to do, it will be picked up as the lastest version by default. + +#### Which version of docs should I be editing? + +If you are writing/updating docs on `main` for the new release of Wasp, you should edit "current" docs (docs/). + +If you are (hot)fixing currently published docs on `release`, then you should edit the versioned docs (versioned_docs/{version}), for whatever version you want to do this for. If you want this change to also be present in all the new docs, then you should also do it for the "current" docs (docs/) (yes, that means duplicating the same change). + +Prefer doing doc edits on `main`, as that keeps the whole process simpler, and do changes to docs on `release` only if it really matters to fix the already published versioned docs. + +#### Deleting versions + +We should not keep too many versions of documentation, especially now in Beta when we are moving fast. + +Therefore, we should be quite liberal with deleting the older versions of docs. + +Also, it might make sense to delete the previous version of docs if only bug fixes were done in the latest version. diff --git a/web/blog/2023-03-02-wasp-beta-update-feb.md b/web/blog/2023-03-02-wasp-beta-update-feb.md index fa59b96848..ca23a5f36e 100644 --- a/web/blog/2023-03-02-wasp-beta-update-feb.md +++ b/web/blog/2023-03-02-wasp-beta-update-feb.md @@ -60,7 +60,7 @@ This is one of the features we are most excited about! Now, when you define an e This feature beautifully showcases the power of the Wasp language approach and how much it can cut down on the boilerplate. And we're just getting started! -For more details, [check out our docs on reusing entity types on both a client and a server](/docs/typescript#entity-types). +For more details, [check out our entity docs](/docs/data-model/entities). ## 🗓 We set a date for the next launch - April 11th! 🚀 diff --git a/web/blog/2023-08-09-build-real-time-voting-app-websockets-react-typescript.md b/web/blog/2023-08-09-build-real-time-voting-app-websockets-react-typescript.md index f7f23187a3..6274394a5e 100644 --- a/web/blog/2023-08-09-build-real-time-voting-app-websockets-react-typescript.md +++ b/web/blog/2023-08-09-build-real-time-voting-app-websockets-react-typescript.md @@ -710,7 +710,7 @@ You should see a login screen this time. Go ahead and first register a user, the Once logged in, you’ll see the same hardcoded poll data as in the previous example, because, again, we haven’t set up the [Socket.IO](http://Socket.IO) client on the frontend. But this time it should be much easier. -Why? Well, besides less configuration, another nice benefit of working with [TypeScript with Wasp](/docs/typescript#websocket-full-stack-type-support), is that you just have to define payload types with matching event names on the server, and those types will get exposed automatically on the client! +Why? Well, besides less configuration, another nice benefit of working with [TypeScript with Wasp](/docs/advanced/web-sockets), is that you just have to define payload types with matching event names on the server, and those types will get exposed automatically on the client! Let’s take a look at how that works now. @@ -885,4 +885,4 @@ And if you know of a better, cooler, sleeker way of implementing WebSockets into \ No newline at end of file +/> diff --git a/web/blog/2023-11-21-guide-windows-development-wasp-wsl.md b/web/blog/2023-11-21-guide-windows-development-wasp-wsl.md new file mode 100644 index 0000000000..92f0b2a0da --- /dev/null +++ b/web/blog/2023-11-21-guide-windows-development-wasp-wsl.md @@ -0,0 +1,189 @@ +--- +title: 'A Guide to Windows Development with Wasp & WSL' +authors: [martinovicdev] +image: /img/wsl-guide/wsl-guide-banner.jpeg +tags: [wsl, windows, tutorial] +--- + +import Link from '@docusaurus/Link'; +import useBaseUrl from '@docusaurus/useBaseUrl'; + +import ImgWithCaption from './components/ImgWithCaption' + + + +If you are having a hard time with Wasp development on Windows, don't be afraid! We will go through all necessary steps to set up your dev environment and get you started with Wasp development in Windows in no time. + +## What is WSL and why should I be interested in it? + +Windows Subsystem for Linux (or WSL) lets developers run a fully functional and native GNU/Linux environment directly on Windows. In other words, we can run Linux directly without using a virtual machine or dual-booting the system. + +**The first cool thing about it is that WSL allows you to never switch OS’s, but still have the best of both worlds inside your OS.** +What does that mean for us regular users? When you look at the way WSL works in practice, it can be considered a Windows feature that runs a Linux OS directly inside Windows 10 or 11, with a fully functional Linux file system, Linux command line tools, and Linux GUI apps (_really cool, btw_). Besides that, it uses much fewer resources for running when compared to a virtual machine and also doesn’t require a separate tool for creating and managing those virtual machines. + +WSL is mainly catered to developers, so this article will be focused on developer usage and how to set up a fully working dev environment with VS Code. Inside this article, we’ll go through some of the cool features and how they can be used in practice. Plus, the best way to understand new things is to actually start using them. + +## Installing WSL on the Windows operating system + +In order to install WSL on your Windows, first enable [Hyper-V](https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v) architecture is Microsoft’s hardware virtualization solution. To install it, right-click on the Windows Terminal/Powershell and open it in Administrator mode. + +![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6wm5xniz2nehrccczeh6.png) + +Then, run the following command: + +```bash +Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All +``` + +That will ensure that you have all the prerequisites for the installation. Then, open the Powershell (best done in Windows Terminal) in the Administrator mode. Then, run + +```bash +wsl —install +``` + +There is a plethora of Linux distributions to be installed, but Ubuntu is the one installed by default. This guide will feature many console commands, but most of them will be a copy-paste process. + +If you have installed Docker before, there is a decent chance that you have WSL 2 installed on your system already. In that case, you will get a prompt to install the distribution of your choice. Since this tutorial will be using Ubuntu, I suggest running. + +```bash + wsl --install -d Ubuntu +``` + +After installing Ubuntu (or another distro of your choice), you will enter your Linux OS and be prompted with a welcome screen. There, you will enter some basic info. First, you will enter your username and after that your password. Both of those will be Linux-specific, so you don’t necessarily have to repeat your Windows credentials. After we’ve done this, the installation part is over! You have successfully installed Ubuntu on your Windows machine! It still feels weird to say this, right? + +### Cool WSL featues to help you along the way + +But before we get down to our dev environment setup, I want to show you a couple of cool tricks that will make your life easier and help you understand why WSL is actually a game-changer for Windows users. + +The first cool thing with WSL is that you don’t have to give up the current way of managing files through Windows Explorer. In your sidebar in Windows Explorer, you can find the Linux option now right under the network tab. + +![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/647jdnzilrucsijtye3v.png) + +From there, you can access and manage your Linux OS’s file system directly from the Windows Explorer. What is really cool with this feature is that you can basically copy, paste, and move files between different operating systems without any issues, which opens up a whole world of possibilities. Effectively, you don’t have to change much in your workflow with files and you can move many projects and files from one OS to another effortlessly. If you download an image for your web app on your Windows browser, just copy and paste it to your Linux OS. + +![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iqjsd1oz5a4alu6q08re.png) + +Another very important thing, which we will use in our example is WSL2 virtual routes. As you now have OS inside your OS, they have a way of communicating. When you want to access your Linux OS’s network (for example, when you want to access your web app running locally in Linux), you can use _${PC-name}.local_. For me, since my PC name is Boris-PC, my network address is boris-pc.local. That way you don’t have to remember different IP addresses, which is really cool. If you want your address for whatever reason, you can go to your Linux distro’s terminal, and type ipconfig. Then, you can see your Windows IP and Linux’s IP address. With that, you can communicate with both operating systems without friction. + +![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lkhcfiybnobuoziitwtm.png) + +The final cool thing I want to highlight is Linux GUI apps. It is a very cool feature that helps make WSL a more attractive proposal for regular users as well. You can install any app you want on your Linux system using popular package managers, such as apt (default on Ubuntu) or flatpak. Then you can launch them as well from the command line and the app will start and be visible inside your Windows OS. But that can cause some friction and is not user-friendly. The really ground-breaking part of this feature is that you can launch them directly from your Windows OS without even starting WSL yourself. Therefore, you can create shortcuts and pin them to the Start menu or taskbar without any friction and really have no need to think about where your app comes from. For the showcase, I have installed Dolphin File Manager and run it through Windows OS. You can see it action below side by side with Windows Explorer. + +![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yq1nxj244jd1fci13oay.png) + +## Getting started with development on WSL + +After hearing all about the cool features of WSL, let’s slowly get back on track with our tutorial. Next up is setting up our dev environment and starting our first app. I’ll be setting up a web dev environment and we’ll use [Wasp](https://wasp-lang.dev/) as an example. + +If you aren’t familiar with it, Wasp is a Rails-like framework for React, Node.js, and Prisma. It’s a fast and easy way to develop and deploy your full-stack web apps. For our tutorial, Wasp is a perfect candidate, since it doesn’t support Windows development natively, but only through WSL as it requires a Unix environment. + +Let’s get started with installing Node.js first. At the moment, Wasp requires users to use the Node v18 (version requirement will be relaxed very soon), so we want to start with both Node.js and NVM installation. + +But first things first, let’s start with Node.js. In WSL, run: + +```jsx +sudo apt install nodejs +``` + +in order to install Node on your Linux environment. Next up is NVM. I suggest going to https://github.com/nvm-sh/nvm and getting the latest install script from there. The current download is: + +```bash +curl -o- [https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh](https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh) | bash +``` + +After this, we have both Node.js and NVM set up in our system. + +### Installing Wasp + +Next up is installing Wasp on our Linux environment. Wasp installation is also pretty straightforward and easy. So just copy and paste this command: + +```bash +curl -sSL [https://get.wasp-lang.dev/installer.sh](https://get.wasp-lang.dev/installer.sh) | sh +``` + +and wait for the installer to finish up its thing. Great! But, if you did your WSL setup from 0, you will notice the following warning underneath: It looks like '/home/boris/.local/bin' is not on your PATH! You will not be able to invoke wasp from the terminal by its name. + +![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/em932e89tlzajv4rm6up.png) + +Let’s fix this quickly. In order to do this, let’s run + +```bash + code ~/.profile +``` + +If we don’t already have VS Code, it will automatically set up everything needed and boot up so you can add the command to the end of your file. It will be different for everyone depending on their system name. For example, mine is: + +```bash +export PATH=$PATH:/home/boris/.local/bin +``` + +Great! Now we just need to swap node version to v18.14.2 to ensure full compatibility with Wasp. We’ll install and switch to Node 18 in one go! To do this, simply run: + +```bash +nvm install v18.14.2 && nvm use v18.14.2 +``` + +### Setting up VS Code + +After setting up Wasp, we want to see how to run the app and access it from VS Code. Under the hood, you will still be using WSL for our development, but we’ll be able to use our VS Code from Host OS (Windows) for most of the things. + +![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/orifa202sph4swgbir2d.png) + +To get started, download the [WSL extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl) to your VS Code in Windows. Afterward, let’s start a new Wasp project to see how it works in action. Open your VS Code Command Palette (ctrl + shift + P) and select the option to “Open Folder in WSL”. + +![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l1le8xvk6a8a8teog8eo.png) + +The folder that I have opened is + +```bash +\\wsl.localhost\Ubuntu\home\boris\Projects +``` + +That is the “Projects” folder inside my home folder in WSL. There are 2 ways for us to know that we are in WSL: The top bar and in the bottom left corner of VS Code. In both places, we have WSL: Ubuntu written, as is shown on screenshots. + +![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mzhu765415sravn3vypu.png) + +![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cpy4kggtsobod1vk1dqn.png) + +Once inside this folder, I will open a terminal. It will also be already connected to the proper folder in WSL, so we can get down to business! Let’s run the + +```bash +wasp new +``` + +command to create a new Wasp application. I have chosen the basic template, but you are free to create a project of your choosing, e.g. [SaaS starter](https://github.com/wasp-lang/SaaS-Template-GPT) with GPT, Stripe and more preconfigured. As shown in the screenshot, we should change the current directory of our project to the proper one and then run our project with it. + +```bash +wasp start +``` + +![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l453mcae56kfa3yrm7j4.png) + +And just like that, a new screen will open on my Windows machine, showcasing that my Wasp app is open. Cool! My address is still the default localhost:3000, but it is being run from the WSL. Congratulations, you’ve successfully started your first Wasp app through WSL. That wasn’t hard, was it? + +![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vfyfok2eg0xjhqcqhgoe.png) + +For our final topic, I want to highlight Git workflow with WSL, as it is relatively painless to set up. You can always do the manual git config setup, but I have something cooler for you: Sharing credentials between Windows and WSL. To set up sharing Git credentials, we have to do the following. In Powershell (on Windows), configure the credential manager on Windows. + +```bash +git config --global credential.helper wincred +``` + +And let’s do the same inside WSL. + +```bash +git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager.exe" +``` + +This allows us to share our Git username and password. Anything set up in Windows will work in WSL (and vice-versa) and we can use Git inside WSL as we prefer (via VS Code GUI or via shell). + +## Conclusion + +Through our journey here, we have learned what WSL is, how it can be useful for enhancing our workflow with our Windows PC, but also how to set up your initial development environment on it. Microsoft has done a fantastic job with this tool and has really made Windows OS a much more approachable and viable option for all developers. We went through how to install the dev tools needed to kickstart development and how to get a handle on a basic dev workflow. Here are some important links if you want to dive deeper into the topic: + +- [https://github.com/microsoft/WSL](https://github.com/microsoft/WSL) +- [https://learn.microsoft.com/en-us/windows/wsl/install](https://learn.microsoft.com/en-us/windows/wsl/install) +- [https://code.visualstudio.com/docs/remote/wsl](https://code.visualstudio.com/docs/remote/wsl) diff --git a/web/blog/2023-12-05-writing-rfcs.md b/web/blog/2023-12-05-writing-rfcs.md new file mode 100644 index 0000000000..3a954076d4 --- /dev/null +++ b/web/blog/2023-12-05-writing-rfcs.md @@ -0,0 +1,190 @@ +--- +title: "On the Importance of RFCs in Programming" +authors: [matijasos] +image: /img/writing-rfcs/rfc-prophet.png +tags: [programming, clean-code] +--- + +import ImgWithCaption from './components/ImgWithCaption' + +Imagine you’ve been tasked to implement a sizeable new feature for the product you’re working on. That’s the opportunity you’ve been waiting for - everybody will see what a 10x developer you are! You open a list of the coolest new libraries and design patterns you’ve wanted to try out and get right into it, full “basement” mode. One week later, you victoriously emerge and present your perfect pull request! + +**But then, the senior dev in a team immediately rejects it - *“Too complex, you should have simply used library X and reused Y.”***. What!? Before you know it, you’re looking at 100 comments on your PR and days of refactoring to follow. + +If only there were **a way of knowing about X and Y before implementing everything**. Well, it is, and it’s called RFC! + + + + +We’ll learn about it through the example of [RFC about implementing an authentication system in a web framework Wasp](https://wasp-lang.notion.site/RFC-Auth-without-user-defined-entities-6d2925439627456ab01b74ff4b4cd087?pvs=4). [Wasp](https://github.com/wasp-lang/wasp) is a full-stack web framework built on top of React, Node.js and Prisma. It is used by [MAGE](https://usemage.ai/), a free GPT-powered codebase generator, which has been used to start over 30,000 applications. + +Let's dive in! + +## So, what is an RFC? + +RFC (*Request For Comments*) is, simply explained, a document proposing a codebase change to solve a specific problem. **Its main purpose is to find the best way to solve a problem, as a team effort, before the implementation starts**. RFCs were first adopted by the open-source community, but today, they are used in almost any type of developer organization. + + + +There are other names for this type of document you might encounter in the industry, like TDD (*Technical Design Document*) or SDD (*Software Design Document*). Some people argue over the distinction between them, but we won’t. + +**Fun fact**: RFCs were invented by IETF (*Internet Engineering Task Force*), the engineering organization behind some of the most important internet standards and protocols we use today, like TCP/IP! Not too shabby, right? + +## When should I write RFC, and when can I skip it? + + + +So, why bother writing about what you will eventually code, instead of saving time and simply doing it? **If you’re dealing with a bug or a relatively simple feature, where it’s very clear what you must do and doesn’t affect project structure, then there’s no need for an RFC - fire up that IDE and get cracking!** + +But, if you are introducing a completely new concept (e.g., introducing a role-based permission system) or altering the project’s architecture (e.g., adding support for running background jobs), then you might want to take a step back before typing `git checkout -b my-new-feature` and diving into that sweet coding zone. + +All the above being said, sometimes it's not easy to figure out if you should write an RFC or not. Maybe it’s a more prominent feature, but you’ve done something similar before, and you’ve already mapped everything out in your head and pretty much have no questions. To help with that, here’s a simple heuristic I like to use: **Is there more than one obvious way to implement this feature? Is there a new library/service we have to pick?** If the answer to both of these is “No", you probably don’t need an RFC. Otherwise, there’s a discussion to be had, and RFC is the way to do it. + + + +## It sounds useful. But what’s in it for me? + +We’ve established how to decide *when* to write an RFC, but here is also *why* you should do it: + +- **You will organize your thoughts and get clarity**. If you’ve decided to write an RFC, that means you’re dealing with a non-trivial, open-ended problem. Writing things down will help distill your thoughts and have an objective look at them. +- **You will learn more** than if you just jumped into coding. You will give yourself space to explore different approaches and oftentimes discover something you haven’t even thought of initially. +- **You will crowdsource your team’s knowledge.** By asking your team for feedback (hence Request For Comments), you will get a complete picture of the problem you’re solving and fill in any remaining gaps. +- **You will advance your team’s understanding of the codebase.** By collaborating on your RFC, everybody on the team will understand what you’re doing and how you eventually did it. That means next time somebody has to touch that part of the code, they will need to ask you much less questions (=== more uninterrupted coding time!). +- **PR reviews will go *much* smoother**. Remember that situation from the beginning of this article, when your PR got rejected as "too complex"? That’s because the reviewer is missing the context, and you made a sizeable change without a previous buy-in from the rest of the team. By writing an RFC first, you’ll never encounter this type of situation again. +- **Your documentation is already 50% done!** To be clear, RFC is not the final documentation, and you cannot simply point to it, but you can likely reuse a lot - images, diagrams, paragraphs, etc. + +Wow, this sounds so good that I want to come up with a new feature right now just so I can write an RFC for it! Joke aside, going through with the RFC first makes the coding part so much more enjoyable - you know exactly what you need to do, and you don’t need to question your approach and how it will be received once you create that PR. + +## Ok, ok, I’m sold! So, how do I go about writing one? + +Glad you asked! Many different formats are being used, more or less formal, but I prefer to keep it simple. RFCs that we write at Wasp don’t follow a strict format, but there are some common parts: + +- **Metadata** - Title, date, reviewers, etc… +- **Problem / Goal** +- **Proposed solution** (or more of them) +- **Implementation overview** +- **Remarks / open questions** + +That’s pretty much the gist of it! Each of these can be further broken down and refined, but this is the basic outline you can start with. + +Let’s now go over each of these and see what they look like in practice, on our [Authentication in Wasp](https://wasp-lang.notion.site/RFC-Auth-without-user-defined-entities-6d2925439627456ab01b74ff4b4cd087?pvs=4) example. + +## Metadata ⌗ + + + +This one is pretty self-explanatory - you will want to track some basic info about your RFCs - status, date of creation, etc. + +Some templates also explicitly list the reviewers and the status of their “approval” of the RFC, similar to the PR review process - we don’t have it since we’re a small team where communication happens fast, but it can be handy for larger teams where not everybody knows everybody, and you want to have a bit more of a process in place (e.g. when mentoring junior developers). + + + +## The problem 🤔 + +This is where things get interesting. **The better you define the problem or the goal/feature you need to implement, and why you need to do it, the easier all the following steps will be**. So this is something worth investing in even before you start writing your RFC - make sure you talk to all the involved parties (e.g., product owner, other developers, and even users) to refine your understanding of the issue you’re about to tackle. + +By doing this, you will also very likely get first hints and pointers on the possible solutions, and develop a rough sense of the problem space you’re in. + + + +Here are a few tips from the example above: + +- **Start with a high-level summary** - that way, readers can quickly decide if this is relevant to them or not and whether they should keep reading. +- **Provide some context** - Explain a bit about the current state of the world, as it is right now. This can be a single sentence or a whole chapter, depending on the intended audience. +- **Clearly state the problem/goal** - explain why there is a problem and connect it with the user’s/company’s pain, so that motivation is clear. +- **Provide extra details if possible** - diagrams, code examples, … → anything that can help the reader get faster to that “aha” moment. Extra points for using collapsible sections, so the central part of the RFC remains of digestible length. + +If you did all this, you’re already well on your way to the excellent RFC! Since defining the problem well is essential, don’t be afraid to add more to it and break things down further. + +### Non-goals 🛑 + +This is the sub-section of the "Problem" or "Goal" section that can sometimes be super valuable. Writing what we don't want or will not be doing in this codebase change can help set the expectations and better define its scope. + +For example, if we are working on adding a role-based authentication system to our app, people might assume that we will also build some sort of an admin panel for it to manage users and add/remove roles. By explicitly stating it won't be done (and briefly explaining why - not needed, it would take too long, it will be done in the next iteration, ...), reviewers will get a better understanding of what your goal is and you will skip unnecessary discussion. + +## Solution & Implementation 🛠️ + +Once we know what we want to do, we have to figure out the best way of doing it! You might have already hinted at the possible solution in the Problem section, but now is the moment to dive deeper - research different approaches, evaluate their pros and cons, and sketch how they could fit into the existing system. + +This section is probably the most free-form of all - since it highly depends on the nature of what you are doing, it doesn’t make sense to impose many restrictions here. You may want to stay at the higher level of, e.g., system architecture, or you may need to dive deep into the code and start writing parts of the code you will need. Due to that, I don’t have an exact format for you to follow, but rather a set of guidelines: + +### Write pseudocode + +The purpose of RFC is to convey ideas and principles, not production-grade code that compiles and covers all the edge cases. Feel free to invent/imagine/sketch whatever you need (e.g., imagine you already have a function that sends an email and just use it, even if you don’t), and don’t encumber yourself or the reader with the implementation details (unless that’s exactly what the RFC is about). + +It’s better to start at the higher level, and then go deeper when you realize you need it or if one of the reviewers suggests it. + +### Find out how are others doing it + + + +How you find this out may differ depending on the type of product you’re developing, but there is almost always a way to do it. If you’re developing an open-source tool like [Wasp](https://github.com/wasp-lang/wasp) you can simply check out other popular solutions (that are also open-source) and learn how they did it. If you’re working on a SaaS and need to figure out whether to use cookies or JWTs for the authentication, you likely have some friends who have done it before, and you can ask them. Lastly, simply Google/GPT it. + +Why is this so helpful? **The reason is that it gives you (and the reviewers) confidence in your solution. If somebody else did it successfully this way, it might be a promising direction.** It also might help you discover approaches you haven’t thought of before, or serve as a basis on top of which you can build. Of course, never take anything for granted and take into account the specific needs of your situation, but definitely make use of the knowledge and expertise of others. + +### Leave things unfinished & don't make it perfect + +The main point of RFC is the “C” part, so collaboration (yes, I know it actually stands for "_comments_"). **RFC is not a test where you have to get the perfect score and have no questions asked - if that happens, you probably shouldn’t have written it in the first place.** + +Solving a problem is a team effort, and you’re just the person taking the first stab at it and pushing things forward. Your task is to lay as much groundwork as you reasonably can (refine the problem, explore multiple approaches to solving it, identify new subproblems that came to light) so the reviewers can quickly grasp the status and provide efficient feedback, directed where it’s needed the most. + +**The main job of your RFC is to identify the most important problems and direct the reviewer’s attention to them, not solve them.** + +The RFC you’re writing should be looked at as a discussion area and a work-in-progress, not a piece of art that has to be perfected before it’s displayed in front of the audience. + +## Remarks & open questions 🎯 + +In this final section of the document, you can summarise the main thoughts and highlight the biggest open questions. After going through everything, it can be helpful for the reader to be reminded of where his attention can be most valuable. + +## Now I know when and how to write an RFC! Do you have any templates I could use as a starting point? + +Of course! As mentioned, our format is extremely lightweight, but feel free to take a look at [the RFC we used as an example](https://wasp-lang.notion.site/RFC-Auth-without-user-defined-entities-6d2925439627456ab01b74ff4b4cd087?pvs=4) to get inspired. Your company could also already have a ready template they recommend. + +Here are a few you can use and/or adapt to your needs: + +- [Squarespace RFC template](https://engineering.squarespace.com/s/Squarespace-RFC-Template.pdf) +- _Do you have a template you would recommend? I'm happy to list it here!_ + +## What tool should I use to write my RFCs? There are so many choices! + +The exact tool you’re using is probably the least important part of RFC-ing, but it still matters since it sets the workflow around it. If your company has already selected a tool, then of course stick with that. If not, here are the most common choices I’ve come across, along with quick comments: + +- **Google Docs** - the classic choice. Super easy to comment on any part of the doc, which is the most important feature. +- **Notion** - also great for collaboration, plus offers some markdown components such as collapsibles and tables, which can make your RFC more readable. +- **GitHub issues / PRs** - this is sometimes used, especially for OSS projects. The drawback is that it is harder to comment on the specific part of the document (you can only comment on the whole line), plus inserting diagrams is also quite clunky. The pro is that everything (code and RFCs) stays on the same platform + +We currently use Notion, but any of the above can be a good choice. + +## Summary + +Just as it is the best practice to write a summary at the end of your RFC, we will do the same here! This article came out longer than I expected, but there were so many things to mention - I hope you'll find it useful! + +Finally, **being able to clearly express your thoughts, formulate the problem, and objectively analyze the possible solutions, with feedback from the team, is what will help you develop the right thing, which is the ultimate productivity hack**. This is how you become a 10x engineer. + +And don't forget: *Weeks of coding can save you hours of planning.* \ No newline at end of file diff --git a/web/blog/authors.yml b/web/blog/authors.yml index 0064925f9f..e03e5c1642 100644 --- a/web/blog/authors.yml +++ b/web/blog/authors.yml @@ -3,12 +3,14 @@ martinsos: title: Co-founder & CTO @ Wasp url: https://github.com/martinsos image_url: https://github.com/martinsos.png + email: martin@wasp-lang.dev matijasos: name: Matija Sosic title: Co-founder & CEO @ Wasp url: https://github.com/matijasos image_url: https://github.com/matijasos.png + email: matija@wasp-lang.dev shayneczyzewski: name: Shayne Czyzewski @@ -21,6 +23,7 @@ sodic: title: Founding Engineer @ Wasp url: https://github.com/sodic image_url: https://github.com/sodic.png + email: filip@wasp-lang.dev maksym36ua: name: Maksym Khamrovskyi @@ -37,9 +40,17 @@ vinny: title: DevRel @ Wasp url: https://vincanger.github.io image_url: https://vincanger.github.io/assets/vince_smiley.jpg + email: vince@wasp-lang.dev miho: name: Mihovil Ilakovac title: Founding Engineer @ Wasp url: https://ilakovac.com image_url: https://github.com/infomiho.png + email: miho@wasp-lang.dev + +martinovicdev: + name: Boris Martinović + title: Contributor @ Wasp + url: https://martinovic.dev + image_url: https://github.com/martinovicdev.png diff --git a/web/docs/OldDocsNote.tsx b/web/docs/OldDocsNote.tsx deleted file mode 100644 index 7274803746..0000000000 --- a/web/docs/OldDocsNote.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import Admonition from '@theme/Admonition' -import Link from '@docusaurus/Link' -import React from 'react' - -export default function OldDocsNote() { - return ( -
    - - This page is part of a previous documentation version and is no longer - actively maintained. The content is likely out of date and may no longer - be relevant to current releases. -
    -
    - Go to the current documentation for updated - content. -
    -
    - ) -} diff --git a/web/docs/advanced/apis.md b/web/docs/advanced/apis.md index 125499e348..767fce0100 100644 --- a/web/docs/advanced/apis.md +++ b/web/docs/advanced/apis.md @@ -3,9 +3,9 @@ title: Custom HTTP API Endpoints --- import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers' -import { Required } from '@site/src/components/Required' +import { Required } from '@site/src/components/Tag' -In Wasp, the default client-server interaction mechanism is through [Operations](/docs/data-model/operations/overview). However, if you need a specific URL method/path, or a specific response, Operations may not be suitable for you. For these cases, you can use an `api`. Best of all, they should look and feel very familiar. +In Wasp, the default client-server interaction mechanism is through [Operations](../data-model/operations/overview). However, if you need a specific URL method/path, or a specific response, Operations may not be suitable for you. For these cases, you can use an `api`. Best of all, they should look and feel very familiar. ## How to Create an API @@ -231,11 +231,11 @@ export const apiMiddleware: MiddlewareConfigFn = (config) => { We are returning the default middleware which enables CORS for all APIs under the `/foo` path. -For more information about middleware configuration, please see: [Middleware Configuration](/docs/advanced/middleware-config) +For more information about middleware configuration, please see: [Middleware Configuration](../advanced/middleware-config) ## Using Entities in APIs -In many cases, resources used in APIs will be [Entities](/docs/data-model/entities.md). +In many cases, resources used in APIs will be [Entities](../data-model/entities.md). To use an Entity in your API, add it to the `api` declaration in Wasp: @@ -340,4 +340,4 @@ The `api` declaration has the following fields: - `middlewareConfigFn: ServerImport` - The import statement to an Express middleware config function for this API. See more in [middleware section](/docs/advanced/middleware-config) of the docs. \ No newline at end of file + The import statement to an Express middleware config function for this API. See more in [middleware section](../advanced/middleware-config) of the docs. \ No newline at end of file diff --git a/web/docs/advanced/deployment/_addExternalAuthEnvVarsReminder.md b/web/docs/advanced/deployment/_addExternalAuthEnvVarsReminder.md index 54385d1413..29c532d974 100644 --- a/web/docs/advanced/deployment/_addExternalAuthEnvVarsReminder.md +++ b/web/docs/advanced/deployment/_addExternalAuthEnvVarsReminder.md @@ -1,4 +1,3 @@ :::tip Using an external auth method? - -If your app is using an external authentication method(s) supported by Wasp (such as [Google](/docs/auth/social-auth/google#4-adding-environment-variables) or [GitHub](/docs/auth/social-auth/github#4-adding-environment-variables)), make sure to additionally set the necessary environment variables specifically required by these method(s). +If your app is using an external authentication method(s) supported by Wasp (such as [Google](../../auth/social-auth/google#4-adding-environment-variables) or [GitHub](../../auth/social-auth/github#4-adding-environment-variables)), make sure to additionally set the necessary environment variables specifically required by these method(s). ::: diff --git a/web/docs/advanced/deployment/cli.md b/web/docs/advanced/deployment/cli.md index 2dd3378f9c..03187804ed 100644 --- a/web/docs/advanced/deployment/cli.md +++ b/web/docs/advanced/deployment/cli.md @@ -2,7 +2,7 @@ title: Deploying with the Wasp CLI --- -import { Required } from '@site/src/components/Required'; +import { Required } from '@site/src/components/Tag'; Wasp CLI can deploy your full-stack application with only a single command. The command automates the manual deployment process and is the recommended way of deploying Wasp apps. diff --git a/web/docs/advanced/deployment/manually.md b/web/docs/advanced/deployment/manually.md index a4b4f2146d..198ffc2b26 100644 --- a/web/docs/advanced/deployment/manually.md +++ b/web/docs/advanced/deployment/manually.md @@ -5,7 +5,7 @@ title: Deploying Manually import useBaseUrl from '@docusaurus/useBaseUrl'; import AddExternalAuthEnvVarsReminder from './\_addExternalAuthEnvVarsReminder.md' import BuildingTheWebClient from './\_building-the-web-client.md' -import { Required } from '@site/src/components/Required' +import { Required } from '@site/src/components/Tag' We'll cover how to deploy your Wasp app manually to a variety of providers: @@ -35,7 +35,7 @@ wasp build :::caution PostgreSQL in production You won't be able to build the app if you are using SQLite as a database (which is the default database). -You'll have to [switch to PostgreSQL](/docs/data-model/backends#migrating-from-sqlite-to-postgresql) before deploying to production. +You'll have to [switch to PostgreSQL](../../data-model/backends#migrating-from-sqlite-to-postgresql) before deploying to production. ::: ### 2. Deploying the API Server (backend) @@ -98,7 +98,7 @@ We'll cover a few different deployment providers below: We will show how to deploy the server and provision a database for it on Fly.io. :::tip We automated this process for you -If you want to do all of the work below with one command, you can use the [Wasp CLI](/docs/advanced/deployment/cli#flyio). +If you want to do all of the work below with one command, you can use the [Wasp CLI](../../advanced/deployment/cli#flyio). Wasp CLI deploys the server, deploys the client, and sets up a database. It also gives you a way to redeploy (update) your app with a single command. @@ -559,7 +559,7 @@ heroku logs --tail --app :::note Using `pg-boss` with Heroku -If you wish to deploy an app leveraging [Jobs](/docs/advanced/jobs) that use `pg-boss` as the executor to Heroku, you need to set an additional environment variable called `PG_BOSS_NEW_OPTIONS` to `{"connectionString":"","ssl":{"rejectUnauthorized":false}}`. This is because pg-boss uses the `pg` extension, which does not seem to connect to Heroku over SSL by default, which Heroku requires. Additionally, Heroku uses a self-signed cert, so we must handle that as well. +If you wish to deploy an app leveraging [Jobs](../../advanced/jobs) that use `pg-boss` as the executor to Heroku, you need to set an additional environment variable called `PG_BOSS_NEW_OPTIONS` to `{"connectionString":"","ssl":{"rejectUnauthorized":false}}`. This is because pg-boss uses the `pg` extension, which does not seem to connect to Heroku over SSL by default, which Heroku requires. Additionally, Heroku uses a self-signed cert, so we must handle that as well. Read more: https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js ::: diff --git a/web/docs/advanced/deployment/overview.md b/web/docs/advanced/deployment/overview.md index 23f841bbf9..c4750284e2 100644 --- a/web/docs/advanced/deployment/overview.md +++ b/web/docs/advanced/deployment/overview.md @@ -11,7 +11,7 @@ Wasp apps are full-stack apps that consist of: You can deploy each part **anywhere** where you can usually deploy Node.js apps or static apps. For example, you can deploy your client on [Netlify](https://www.netlify.com/), the server on [Fly.io](https://fly.io/), and the database on [Neon](https://neon.tech/). -To make deploying as smooth as possible, Wasp also offers a single-command deployment through the **Wasp CLI**. Read more about deploying through the CLI [here](/docs/advanced/deployment/cli). +To make deploying as smooth as possible, Wasp also offers a single-command deployment through the **Wasp CLI**. Read more about deploying through the CLI [here](../../advanced/deployment/cli). diff --git a/web/docs/advanced/email/_dummy-provider-note.md b/web/docs/advanced/email/_dummy-provider-note.md new file mode 100644 index 0000000000..861476e6e9 --- /dev/null +++ b/web/docs/advanced/email/_dummy-provider-note.md @@ -0,0 +1,4 @@ +:::note Dummy Provider is not for production use + +The `Dummy` provider is not for production use. It is only meant to be used during development. If you try building your app with the `Dummy` provider, the build will fail. +::: \ No newline at end of file diff --git a/web/docs/advanced/email/email.md b/web/docs/advanced/email/email.md new file mode 100644 index 0000000000..009f1848dd --- /dev/null +++ b/web/docs/advanced/email/email.md @@ -0,0 +1,401 @@ +--- +title: Sending Emails +--- + +import { Required } from '@site/src/components/Tag' +import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers' +import DummyProviderNote from './_dummy-provider-note.md' + +# Sending Emails + +With Wasp's email-sending feature, you can easily integrate email functionality into your web application. + + + + +```wasp title="main.wasp" +app Example { + ... + emailSender: { + provider: , + defaultFrom: { + name: "Example", + email: "hello@itsme.com" + }, + } +} +``` + + + + +```wasp title="main.wasp" +app Example { + ... + emailSender: { + provider: , + defaultFrom: { + name: "Example", + email: "hello@itsme.com" + }, + } +} +``` + + + + +Choose from one of the providers: + +- `Dummy` (development only), +- `Mailgun`, +- `SendGrid` +- or the good old `SMTP`. + +Optionally, define the `defaultFrom` field, so you don't need to provide it whenever sending an email. + +## Sending Emails + +Before jumping into details about setting up various providers, let's see how easy it is to send emails. + +You import the `emailSender` that is provided by the `@wasp/email/index.js` module and call the `send` method on it. + + + + +```js title="src/actions/sendEmail.js" +import { emailSender } from "@wasp/email/index.js"; + +// In some action handler... +const info = await emailSender.send({ + from: { + name: "John Doe", + email: "john@doe.com", + }, + to: "user@domain.com", + subject: "Saying hello", + text: "Hello world", + html: "Hello world", +}); +``` + + + + +```ts title="src/actions/sendEmail.ts" +import { emailSender } from "@wasp/email/index.js"; + +// In some action handler... +const info = await emailSender.send({ + from: { + name: "John Doe", + email: "john@doe.com", + }, + to: "user@domain.com", + subject: "Saying hello", + text: "Hello world", + html: "Hello world", +}); +``` + + + + +Read more about the `send` method in the [API Reference](#javascript-api). + +The `send` method returns an object with the status of the sent email. It varies depending on the provider you use. + +## Providers + +We'll go over all of the available providers in the next section. For some of them, you'll need to set up some env variables. You can do that in the `.env.server` file. + +### Using the Dummy Provider + + + +To speed up development, Wasp offers a `Dummy` email sender that `console.log`s the emails in the console. Since it doesn't send emails for real, it doesn't require any setup. + +Set the provider to `Dummy` in your `main.wasp` file. + + + + +```wasp title="main.wasp" +app Example { + ... + emailSender: { + provider: Dummy, + } +} +``` + + + + +```wasp title="main.wasp" +app Example { + ... + emailSender: { + provider: Dummy, + } +} +``` + + + + +### Using the SMTP Provider + +First, set the provider to `SMTP` in your `main.wasp` file. + + + + +```wasp title="main.wasp" +app Example { + ... + emailSender: { + provider: SMTP, + } +} +``` + + + + +```wasp title="main.wasp" +app Example { + ... + emailSender: { + provider: SMTP, + } +} +``` + + + + +Then, add the following env variables to your `.env.server` file. + +```properties title=".env.server" +SMTP_HOST= +SMTP_USERNAME= +SMTP_PASSWORD= +SMTP_PORT= +``` + +Many transactional email providers (e.g. Mailgun, SendGrid but also others) can also use SMTP, so you can use them as well. + +### Using the Mailgun Provider + +Set the provider to `Mailgun` in the `main.wasp` file. + + + + +```wasp title="main.wasp" +app Example { + ... + emailSender: { + provider: Mailgun, + } +} +``` + + + + +```wasp title="main.wasp" +app Example { + ... + emailSender: { + provider: Mailgun, + } +} +``` + + + + +Then, get the Mailgun API key and domain and add them to your `.env.server` file. + +#### Getting the API Key and Domain + +1. Go to [Mailgun](https://www.mailgun.com/) and create an account. +2. Go to [API Keys](https://app.mailgun.com/app/account/security/api_keys) and create a new API key. +3. Copy the API key and add it to your `.env.server` file. +4. Go to [Domains](https://app.mailgun.com/app/domains) and create a new domain. +5. Copy the domain and add it to your `.env.server` file. + +```properties title=".env.server" +MAILGUN_API_KEY= +MAILGUN_DOMAIN= +``` + +### Using the SendGrid Provider + +Set the provider field to `SendGrid` in your `main.wasp` file. + + + + +```wasp title="main.wasp" +app Example { + ... + emailSender: { + provider: SendGrid, + } +} +``` + + + + +```wasp title="main.wasp" +app Example { + ... + emailSender: { + provider: SendGrid, + } +} +``` + + + + +Then, get the SendGrid API key and add it to your `.env.server` file. + +#### Getting the API Key + +1. Go to [SendGrid](https://sendgrid.com/) and create an account. +2. Go to [API Keys](https://app.sendgrid.com/settings/api_keys) and create a new API key. +3. Copy the API key and add it to your `.env.server` file. + +```properties title=".env.server" +SENDGRID_API_KEY= +``` + +## API Reference + +### `emailSender` dict + + + + +```wasp title="main.wasp" +app Example { + ... + emailSender: { + provider: , + defaultFrom: { + name: "Example", + email: "hello@itsme.com" + }, + } +} +``` + + + + +```wasp title="main.wasp" +app Example { + ... + emailSender: { + provider: , + defaultFrom: { + name: "Example", + email: "hello@itsme.com" + }, + } +} +``` + + + + +The `emailSender` dict has the following fields: + +- `provider: Provider` + + The provider you want to use. Choose from `Dummy`, `SMTP`, `Mailgun` or `SendGrid`. + + + +- `defaultFrom: dict` + + The default sender's details. If you set this field, you don't need to provide the `from` field when sending an email. + +### JavaScript API + +Using the `emailSender` in TypescriptJavaScript: + + + +```js title="src/actions/sendEmail.js" +import { emailSender } from "@wasp/email/index.js"; + +// In some action handler... +const info = await emailSender.send({ + from: { + name: "John Doe", + email: "john@doe.com", + }, + to: "user@domain.com", + subject: "Saying hello", + text: "Hello world", + html: "Hello world", +}); +``` + + + + +```ts title="src/actions/sendEmail.ts" +import { emailSender } from "@wasp/email/index.js"; + +// In some action handler... +const info = await emailSender.send({ + from: { + name: "John Doe", + email: "john@doe.com", + }, + to: "user@domain.com", + subject: "Saying hello", + text: "Hello world", + html: "Hello world", +}); +``` + + + + +The `send` method accepts an object with the following fields: + +- `from: object` + + The sender's details. If you set up `defaultFrom` field in the `emailSender` dict in Wasp file, this field is optional. + + - `name: string` + + The name of the sender. + + - `email: string` + + The email address of the sender. + +- `to: string` + + The recipient's email address. + +- `subject: string` + + The subject of the email. + +- `text: string` + + The text version of the email. + +- `html: string` + + The HTML version of the email diff --git a/web/docs/advanced/jobs.md b/web/docs/advanced/jobs.md index 3fe783a145..df59da9c33 100644 --- a/web/docs/advanced/jobs.md +++ b/web/docs/advanced/jobs.md @@ -2,7 +2,7 @@ title: Recurring Jobs --- -import { Required } from '@site/src/components/Required' +import { Required } from '@site/src/components/Tag' import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers' In most web apps, users send requests to the server and receive responses with some data. When the server responds quickly, the app feels responsive and smooth. @@ -94,7 +94,7 @@ Let's write an example Job that will print a message to the console and return a `MySpecialJob` is a generic type Wasp generates to help you correctly type the Job's worker function, ensuring type information about the function's arguments and return value. Read more about type-safe jobs in the [Javascript API section](#javascript-api). -3. After successfully defining the job, you can submit work to be done in your [Operations](/docs/data-model/operations/overview) or [setupFn](/docs/project/server-config#setup-function) (or any other NodeJS code): +3. After successfully defining the job, you can submit work to be done in your [Operations](../data-model/operations/overview) or [setupFn](../project/server-config#setup-function) (or any other NodeJS code): @@ -333,7 +333,7 @@ The Job declaration has the following fields: - `entities: [Entity]` - A list of entities you wish to use inside your Job (similar to [Queries and Actions](/docs/data-model/operations/queries#using-entities-in-queries)). + A list of entities you wish to use inside your Job (similar to [Queries and Actions](../data-model/operations/queries#using-entities-in-queries)). ### JavaScript API diff --git a/web/docs/advanced/links.md b/web/docs/advanced/links.md index eb19fece93..f7510db0d7 100644 --- a/web/docs/advanced/links.md +++ b/web/docs/advanced/links.md @@ -2,7 +2,7 @@ title: Type-Safe Links --- -import { Required } from '@site/src/components/Required' +import { Required } from '@site/src/components/Tag' If you are using Typescript, you can use Wasp's custom `Link` component to create type-safe links to other pages on your site. diff --git a/web/docs/advanced/middleware-config.md b/web/docs/advanced/middleware-config.md index fe2ce9f0bc..859df3d6de 100644 --- a/web/docs/advanced/middleware-config.md +++ b/web/docs/advanced/middleware-config.md @@ -19,7 +19,7 @@ Wasp's Express server has the following middleware by default: - [express.json](https://expressjs.com/en/api.html#express.json) (which uses [body-parser](https://github.com/expressjs/body-parser#bodyparserjsonoptions)): parses incoming request bodies in a middleware before your handlers, making the result available under the `req.body` property. :::note - JSON middlware is required for [Operations](/docs/data-model/operations/overview) to function properly. + JSON middlware is required for [Operations](../data-model/operations/overview) to function properly. ::: - [express.urlencoded](https://expressjs.com/en/api.html#express.urlencoded) (which uses [body-parser](https://expressjs.com/en/resources/middleware/body-parser.html#bodyparserurlencodedoptions)): returns middleware that only parses urlencoded bodies and only looks at requests where the `Content-Type` header matches the type option. - [cookieParser](https://github.com/expressjs/cookie-parser#readme): parses Cookie header and populates `req.cookies` with an object keyed by the cookie names. diff --git a/web/docs/advanced/web-sockets.md b/web/docs/advanced/web-sockets.md index ac3f3b6053..de4032a488 100644 --- a/web/docs/advanced/web-sockets.md +++ b/web/docs/advanced/web-sockets.md @@ -3,7 +3,7 @@ title: Web Sockets --- import useBaseUrl from '@docusaurus/useBaseUrl'; import { ShowForTs } from '@site/src/components/TsJsHelpers'; -import { Required } from '@site/src/components/Required'; +import { Required } from '@site/src/components/Tag'; Wasp provides a fully integrated WebSocket experience by utilizing [Socket.IO](https://socket.io/) on the client and server. diff --git a/web/docs/auth/_multiple-identities-warning.md b/web/docs/auth/_multiple-identities-warning.md new file mode 100644 index 0000000000..ea8f5d8251 --- /dev/null +++ b/web/docs/auth/_multiple-identities-warning.md @@ -0,0 +1,6 @@ +:::caution Using multiple auth identities for a single user + +Wasp currently doesn't support multiple auth identities for a single user. This means, for example, that a user can't have both an email-based auth identity and a Google-based auth identity. This is something we will add in the future with the introduction of the [account merging feature](https://github.com/wasp-lang/wasp/issues/954). + +Account merging means that multiple auth identities can be merged into a single user account. For example, a user's email and Google identity can be merged into a single user account. Then the user can log in with either their email or Google account and they will be logged into the same account. +::: diff --git a/web/docs/auth/_read-more-about-auth-entities.md b/web/docs/auth/_read-more-about-auth-entities.md new file mode 100644 index 0000000000..bafd959cac --- /dev/null +++ b/web/docs/auth/_read-more-about-auth-entities.md @@ -0,0 +1 @@ +You can read more about how the `User` entity is connected to the rest of the auth system in the [Auth Entities](./entities) section of the docs. \ No newline at end of file diff --git a/web/docs/auth/_user-fields.md b/web/docs/auth/_user-fields.md new file mode 100644 index 0000000000..32319805d7 --- /dev/null +++ b/web/docs/auth/_user-fields.md @@ -0,0 +1,8 @@ +import { Required } from '@site/src/components/Tag'; + +The user entity needs to have the following fields: +- `id` + + It can be of any type, but it needs to be marked with `@id` + +You can add any other fields you want to the user entity. Make sure to also define them in the `userSignupFields` field if they need to be set during the sign-up process. \ No newline at end of file diff --git a/web/docs/auth/_user-signup-fields-explainer.md b/web/docs/auth/_user-signup-fields-explainer.md new file mode 100644 index 0000000000..bde9c14b4c --- /dev/null +++ b/web/docs/auth/_user-signup-fields-explainer.md @@ -0,0 +1,40 @@ +`userSignupFields` defines all the extra fields that need to be set on the `User` during the sign-up process. For example, if you have `address` and `phone` fields on your `User` entity, you can set them by defining the `userSignupFields` like this: + + + + +```ts title="server/auth.js" +import { defineUserSignupFields } from '@wasp/auth/index.js' + +export const userSignupFields = defineUserSignupFields({ + address: (data) => { + if (!data.address) { + throw new Error('Address is required') + } + return data.address + } + phone: (data) => data.phone, +}) +``` + + + + +```ts title="server/auth.ts" +import { defineUserSignupFields } from '@wasp/auth/index.js' + +export const userSignupFields = defineUserSignupFields({ + address: (data) => { + if (!data.address) { + throw new Error('Address is required') + } + return data.address + } + phone: (data) => data.phone, +}) +``` + + + + +Read more about the `userSignupFields` function [here](../auth/overview#1-defining-extra-fields). diff --git a/web/docs/auth/email.md b/web/docs/auth/email.md index 0916966d76..21473fc80b 100644 --- a/web/docs/auth/email.md +++ b/web/docs/auth/email.md @@ -2,26 +2,25 @@ title: Email --- -import { Required } from '@site/src/components/Required'; +import { Required } from '@site/src/components/Tag'; +import MultipleIdentitiesWarning from './\_multiple-identities-warning.md'; +import ReadMoreAboutAuthEntities from './\_read-more-about-auth-entities.md'; +import GetEmail from './entities/\_get-email.md'; +import UserSignupFieldsExplainer from './\_user-signup-fields-explainer.md'; +import UserFields from './\_user-fields.md'; Wasp supports e-mail authentication out of the box, along with email verification and "forgot your password?" flows. It provides you with the server-side implementation and email templates for all of these flows. ![Auth UI](/img/authui/all_screens.gif) -:::caution Using email auth and social auth together -If a user signs up with Google or Github (and you set it up to save their social provider e-mail info on the `User` entity), they'll be able to reset their password and login with e-mail and password ✅ - -If a user signs up with the e-mail and password and then tries to login with a social provider (Google or Github), they won't be able to do that ❌ - -In the future, we will lift this limitation and enable smarter merging of accounts. -::: + ## Setting Up Email Authentication We'll need to take the following steps to set up email authentication: 1. Enable email authentication in the Wasp file -1. Add the user entity -1. Add the routes and pages +1. Add the `User` entity +1. Add the auth routes and pages 1. Use Auth UI components in our pages 1. Set up the email sender @@ -73,7 +72,6 @@ app myApp { passwordReset: { clientRoute: PasswordResetRoute, }, - allowUnverifiedLogin: false, }, }, onAuthFailedRedirectTo: "/login", @@ -108,7 +106,6 @@ app myApp { passwordReset: { clientRoute: PasswordResetRoute, }, - allowUnverifiedLogin: false, }, }, onAuthFailedRedirectTo: "/login", @@ -123,20 +120,16 @@ Read more about the `email` auth method options [here](#fields-in-the-email-dict ### 2. Add the User Entity -When email authentication is enabled, Wasp expects certain fields in your `userEntity`. Let's add these fields to our `main.wasp` file: +The `User` entity can be as simple as including only the `id` field: -```wasp title="main.wasp" {4-8} +```wasp title="main.wasp" // 5. Define the user entity entity User {=psl + // highlight-next-line id Int @id @default(autoincrement()) - email String? @unique - password String? - isEmailVerified Boolean @default(false) - emailVerificationSentAt DateTime? - passwordResetSentAt DateTime? // Add your own fields below // ... psl=} @@ -144,15 +137,11 @@ psl=} -```wasp title="main.wasp" {4-8} +```wasp title="main.wasp" // 5. Define the user entity entity User {=psl + // highlight-next-line id Int @id @default(autoincrement()) - email String? @unique - password String? - isEmailVerified Boolean @default(false) - emailVerificationSentAt DateTime? - passwordResetSentAt DateTime? // Add your own fields below // ... psl=} @@ -160,7 +149,8 @@ psl=} -Read more about the `userEntity` fields [here](#userentity-fields). + + ### 3. Add the Routes and Pages @@ -240,7 +230,7 @@ We'll define the React components for these pages in the `client/pages/auth.{jsx ### 4. Create the Client Pages :::info -We are using [Tailwind CSS](https://tailwindcss.com/) to style the pages. Read more about how to add it [here](/docs/project/css-frameworks). +We are using [Tailwind CSS](https://tailwindcss.com/) to style the pages. Read more about how to add it [here](../project/css-frameworks). ::: Let's create a `auth.{jsx,tsx}` file in the `client/pages` folder and add the following to it: @@ -418,15 +408,15 @@ export function Layout({ children }: { children: React.ReactNode }) { -We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components [here](/docs/auth/ui). +We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components [here](../auth/ui). ### 5. Set up an Email Sender To support e-mail verification and password reset flows, we need an e-mail sender. Luckily, Wasp supports several email providers out of the box. -We'll use SendGrid in this guide to send our e-mails. You can use any of the supported email providers. +We'll use the `Dummy` provider to speed up the setup. It just logs the emails to the console instead of sending them. You can use any of the [supported email providers](../advanced/email#providers). -To set up SendGrid to send emails, we will add the following to our `main.wasp` file: +To set up the `Dummy` provider to send emails, add the following to the `main.wasp` file: @@ -436,7 +426,7 @@ app myApp { // ... // 7. Set up the email sender emailSender: { - provider: SendGrid, + provider: Dummy, } } ``` @@ -448,28 +438,18 @@ app myApp { // ... // 7. Set up the email sender emailSender: { - provider: SendGrid, + provider: Dummy, } } ``` -... and add the following to our `.env.server` file: - -```c title=".env.server" -SENDGRID_API_KEY= -``` - -If you are not sure how to get a SendGrid API key, read more [here](/docs/advanced/email#getting-the-api-key). - -Read more about setting up email senders in the [sending emails docs](/docs/advanced/email). - ### Conclusion That's it! We have set up email authentication in our app. 🎉 -Running `wasp db migrate-dev` and then `wasp start` should give you a working app with email authentication. If you want to put some of the pages behind authentication, read the [using auth docs](/docs/auth/overview). +Running `wasp db migrate-dev` and then `wasp start` should give you a working app with email authentication. If you want to put some of the pages behind authentication, read the [auth overview](../auth/overview). ## Login and Signup Flows @@ -477,10 +457,6 @@ Running `wasp db migrate-dev` and then `wasp start` should give you a working ap ![Auth UI](/img/authui/login.png) -If logging in with an unverified email is _allowed_, the user will be able to login with an unverified email address. If logging in with an unverified email is _not allowed_, the user will be shown an error message. - -Read more about the `allowUnverifiedLogin` option [here](#allowunverifiedlogin-bool-specifies-whether-the-user-can-login-without-verifying-their-e-mail-address). - ### Signup ![Auth UI](/img/authui/signup.png) @@ -500,10 +476,21 @@ Some of the behavior you get out of the box: 4. Password validation - Read more about the default password validation rules and how to override them in [using auth docs](/docs/auth/overview). + Read more about the default password validation rules and how to override them in [auth overview docs](../auth/overview). ## Email Verification Flow +:::info Automatic email verification in development + +In development mode, you can skip the email verification step by setting the `SKIP_EMAIL_VERIFICATION_IN_DEV` environment variable to `true` in your `.env.server` file: + +```env title=".env.server" +SKIP_EMAIL_VERIFICATION_IN_DEV=true +``` + +This is useful when you are developing your app and don't want to go through the email verification flow every time you sign up. It can be also useful when you are writing automated tests for your app. +::: + By default, Wasp requires the e-mail to be verified before allowing the user to log in. This is done by sending a verification email to the user's email address and requiring the user to click on a link in the email to verify their email address. Our setup looks like this: @@ -595,9 +582,249 @@ Users can enter their new password there. The content of the e-mail can be customized, read more about it [here](#passwordreset-passwordresetconfig-). -## Using The Auth +## Creating a Custom Sign-up Action + +:::caution Creating a custom sign-up action + +We don't recommend creating a custom sign-up action unless you have a good reason to do so. It is a complex process and you can easily make a mistake that will compromise the security of your app. +::: -To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the [using auth docs](/docs/auth/overview). +The code of your custom sign-up action can look like this: + + + + +```wasp title="main.wasp" +// ... + +action customSignup { + fn: import { signup } from "@server/auth/signup.js", +} +``` + +```js title="src/server/auth/signup.js" +import { + ensurePasswordIsPresent, + ensureValidPassword, + ensureValidEmail, +} from '@wasp/auth/validation.js' +import { + createProviderId, + sanitizeAndSerializeProviderData, + deserializeAndSanitizeProviderData, + findAuthIdentity, + createUser, +} from '@wasp/auth/utils.js' +import { + createEmailVerificationLink, + sendEmailVerificationEmail, +} from '@wasp/auth/providers/email/utils.js' + +export const signup = async (args, _context) => { + ensureValidEmail(args) + ensurePasswordIsPresent(args) + ensureValidPassword(args) + + try { + const providerId = createProviderId('email', args.email) + const existingAuthIdentity = await findAuthIdentity(providerId) + + if (existingAuthIdentity) { + const providerData = deserializeAndSanitizeProviderData(existingAuthIdentity.providerData) + // Your custom code here + } else { + // sanitizeAndSerializeProviderData will hash the user's password + const newUserProviderData = await sanitizeAndSerializeProviderData({ + hashedPassword: args.password, + isEmailVerified: false, + emailVerificationSentAt: null, + passwordResetSentAt: null, + }) + await createUser( + providerId, + providerData, + // Any additional data you want to store on the User entity + {}, + ) + + // Verification link links to a client route e.g. /email-verification + const verificationLink = await createEmailVerificationLink(args.email, '/email-verification'); + try { + await sendEmailVerificationEmail( + args.email, + { + from: { + name: "My App Postman", + email: "hello@itsme.com", + }, + to: args.email, + subject: "Verify your email", + text: `Click the link below to verify your email: ${verificationLink}`, + html: ` +

    Click the link below to verify your email

    + Verify email + `, + } + ); + } catch (e: unknown) { + console.error("Failed to send email verification email:", e); + throw new HttpError(500, "Failed to send email verification email."); + } + } + } catch (e) { + return { + success: false, + message: e.message, + } + } + + // Your custom code after sign-up. + // ... + + return { + success: true, + message: 'User created successfully', + } +} +``` +
    + + +```wasp title="main.wasp" +// ... + +action customSignup { + fn: import { signup } from "@server/auth/signup.js", +} +``` + +```ts title="src/server/auth/signup.ts" +import { + ensurePasswordIsPresent, + ensureValidPassword, + ensureValidEmail, +} from '@wasp/auth/validation.js' +import { + createProviderId, + sanitizeAndSerializeProviderData, + deserializeAndSanitizeProviderData, + findAuthIdentity, + createUser, +} from '@wasp/auth/utils.js' +import { + createEmailVerificationLink, + sendEmailVerificationEmail, +} from '@wasp/auth/providers/email/utils.js' +import type { CustomSignup } from '@wasp/actions/types' + +type CustomSignupInput = { + email: string + password: string +} +type CustomSignupOutput = { + success: boolean + message: string +} + +export const signup: CustomSignup = async (args, _context) => { + ensureValidEmail(args) + ensurePasswordIsPresent(args) + ensureValidPassword(args) + + try { + const providerId = createProviderId('email', args.email) + const existingAuthIdentity = await findAuthIdentity(providerId) + + if (existingAuthIdentity) { + const providerData = deserializeAndSanitizeProviderData<'email'>(existingAuthIdentity.providerData) + // Your custom code here + } else { + // sanitizeAndSerializeProviderData will hash the user's password + const newUserProviderData = await sanitizeAndSerializeProviderData<'email'>({ + hashedPassword: args.password, + isEmailVerified: false, + emailVerificationSentAt: null, + passwordResetSentAt: null, + }) + await createUser( + providerId, + providerData, + // Any additional data you want to store on the User entity + {}, + ) + + // Verification link links to a client route e.g. /email-verification + const verificationLink = await createEmailVerificationLink(args.email, '/email-verification'); + try { + await sendEmailVerificationEmail( + args.email, + { + from: { + name: "My App Postman", + email: "hello@itsme.com", + }, + to: args.email, + subject: "Verify your email", + text: `Click the link below to verify your email: ${verificationLink}`, + html: ` +

    Click the link below to verify your email

    + Verify email + `, + } + ); + } catch (e: unknown) { + console.error("Failed to send email verification email:", e); + throw new HttpError(500, "Failed to send email verification email."); + } + } + } catch (e) { + return { + success: false, + message: e.message, + } + } + + // Your custom code after sign-up. + // ... + + return { + success: true, + message: 'User created successfully', + } +} +``` +
    +
    + +We suggest using the built-in field validators for your authentication flow. You can import them from `@wasp/auth/validation.js`. These are the same validators that Wasp uses internally for the default authentication flow. + +#### Email + +- `ensureValidEmail(args)` + + Checks if the email is valid and throws an error if it's not. Read more about the validation rules [here](../auth/overview#default-validations). + +#### Password + +- `ensurePasswordIsPresent(args)` + + Checks if the password is present and throws an error if it's not. + +- `ensureValidPassword(args)` + + Checks if the password is valid and throws an error if it's not. Read more about the validation rules [here](../auth/overview#default-validations). + +## Using Auth + +To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the [auth overview docs](../auth/overview). + +### `getEmail` + +If you are looking to access the user's email in your code, you can do that by accessing the info about the user that is stored in the `user.auth.identities` array. + +To make things a bit easier for you, Wasp offers the `getEmail` helper. + + ## API Reference @@ -608,7 +835,7 @@ Let's go over the options we can specify when using email authentication. -```wasp title="main.wasp" {18-25} +```wasp title="main.wasp" app myApp { title: "My app", // ... @@ -625,20 +852,16 @@ app myApp { // ... } -// Using email auth requires the `userEntity` to have at least the following fields entity User {=psl + // highlight-next-line id Int @id @default(autoincrement()) - email String? @unique - password String? - isEmailVerified Boolean @default(false) - emailVerificationSentAt DateTime? - passwordResetSentAt DateTime? psl=} ``` + -```wasp title="main.wasp" {18-25} +```wasp title="main.wasp" app myApp { title: "My app", // ... @@ -655,26 +878,15 @@ app myApp { // ... } -// Using email auth requires the `userEntity` to have at least the following fields entity User {=psl + // highlight-next-line id Int @id @default(autoincrement()) - email String? @unique - password String? - isEmailVerified Boolean @default(false) - emailVerificationSentAt DateTime? - passwordResetSentAt DateTime? psl=} ``` -Email auth requires that `userEntity` specified in `auth` contains: - -- optional `email` field of type `String` -- optional `password` field of type `String` -- `isEmailVerified` field of type `Boolean` with a default value of `false` -- optional `emailVerificationSentAt` field of type `DateTime` -- optional `passwordResetSentAt` field of type `DateTime` + ### Fields in the `email` dict @@ -690,6 +902,7 @@ app myApp { userEntity: User, methods: { email: { + userSignupFields: import { userSignupFields } from "@server/auth.js", fromField: { name: "My App", email: "hello@itsme.com" @@ -702,7 +915,6 @@ app myApp { clientRoute: PasswordResetRoute, getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js", }, - allowUnverifiedLogin: false, }, }, onAuthFailedRedirectTo: "/someRoute" @@ -722,6 +934,7 @@ app myApp { userEntity: User, methods: { email: { + userSignupFields: import { userSignupFields } from "@server/auth.js", fromField: { name: "My App", email: "hello@itsme.com" @@ -734,7 +947,6 @@ app myApp { clientRoute: PasswordResetRoute, getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js", }, - allowUnverifiedLogin: false, }, }, onAuthFailedRedirectTo: "/someRoute" @@ -745,6 +957,10 @@ app myApp {
    +#### `userSignupFields: ServerImport` + + + #### `fromField: EmailFromField` `fromField` is a dict that specifies the name and e-mail address of the sender of the e-mails sent by your app. @@ -904,11 +1120,3 @@ It has the following fields: This is the default content of the e-mail, you can customize it to your liking. - -#### `allowUnverifiedLogin: bool`: specifies whether the user can login without verifying their e-mail address - -It defaults to `false`. If `allowUnverifiedLogin` is set to `true`, the user can login without verifying their e-mail address, otherwise users will receive a `401` error when trying to login without verifying their e-mail address. - -Sometimes you want to allow unverified users to login to provide them a different onboarding experience. Some of the pages can be viewed without verifying the e-mail address, but some of them can't. You can use the `isEmailVerified` field on the user entity to check if the user has verified their e-mail address. - -If you have any questions, feel free to ask them on [our Discord server](https://discord.gg/rzdnErX). \ No newline at end of file diff --git a/web/docs/auth/entities/_get-email.md b/web/docs/auth/entities/_get-email.md new file mode 100644 index 0000000000..02d09bcde5 --- /dev/null +++ b/web/docs/auth/entities/_get-email.md @@ -0,0 +1,48 @@ +The `getEmail` helper returns the user's email or `null` if the user doesn't have an email auth identity. + + + + +```jsx title="src/client/MainPage.jsx" +import { getEmail } from '@wasp/auth/user' + +const MainPage = ({ user }) => { + const email = getEmail(user) + // ... +} +``` + +```js title=src/server/tasks.js +import { getEmail } from '@wasp/auth/user.js' + +export const createTask = async (args, context) => { + const email = getEmail(context.user) + // ... +} +``` + + + + + +```tsx title="src/client/MainPage.tsx" +import { getEmail } from '@wasp/auth/user' +import { User as AuthenticatedUser } from '@wasp/auth/types' + +const MainPage = ({ user }: { user: AuthenticatedUser }) => { + const email = getEmail(user) + // ... +} +``` + +```ts title=src/server/tasks.ts +import { getEmail } from '@wasp/auth/user.js' + +export const createTask: CreateTask<...> = async (args, context) => { + const email = getEmail(context.user) + // ... +} +``` + + + diff --git a/web/docs/auth/entities/_get-username.md b/web/docs/auth/entities/_get-username.md new file mode 100644 index 0000000000..c7667b2c0a --- /dev/null +++ b/web/docs/auth/entities/_get-username.md @@ -0,0 +1,48 @@ +The `getUsername` helper returns the user's username or `null` if the user doesn't have a username auth identity. + + + + +```jsx title="src/client/MainPage.jsx" +import { getUsername } from '@wasp/auth/user' + +const MainPage = ({ user }) => { + const username = getUsername(user) + // ... +} +``` + +```js title=src/server/tasks.js +import { getUsername } from '@wasp/auth/user.js' + +export const createTask = async (args, context) => { + const username = getUsername(context.user) + // ... +} +``` + + + + + +```tsx title="src/client/MainPage.tsx" +import { getUsername } from '@wasp/auth/user' +import { User as AuthenticatedUser } from '@wasp/auth/types' + +const MainPage = ({ user }: { user: AuthenticatedUser }) => { + const username = getUsername(user) + // ... +} +``` + +```ts title=src/server/tasks.ts +import { getUsername } from '@wasp/auth/user.js' + +export const createTask: CreateTask<...> = async (args, context) => { + const username = getUsername(context.user) + // ... +} +``` + + + \ No newline at end of file diff --git a/web/docs/auth/entities/entities.md b/web/docs/auth/entities/entities.md new file mode 100644 index 0000000000..2dd6e3820d --- /dev/null +++ b/web/docs/auth/entities/entities.md @@ -0,0 +1,450 @@ +--- +title: Auth Entities +--- + +import ImgWithCaption from '@site/blog/components/ImgWithCaption' +import { Internal } from '@site/src/components/Tag' +import MultipleIdentitiesWarning from '../\_multiple-identities-warning.md'; +import GetEmail from './\_get-email.md'; +import GetUsername from './\_get-username.md'; + +Wasp supports multiple different authentication methods and for each method, we need to store different information about the user. For example, if you are using the [Username & password](./username-and-pass) authentication method, we need to store the user's username and password. On the other hand, if you are using the [Email](./email) authentication method, you will need to store the user's email, password and for example, their email verification status. + +## Entities Explained + +To store user information, Wasp creates a few entities behind the scenes. In this section, we will explain what entities are created and how they are connected. + +### User Entity + +When you want to add authentication to your app, you need to specify the user entity e.g. `User` in your Wasp file. This entity is a "business logic user" which represents a user of your app. + +You can use this entity to store any information about the user that you want to store. For example, you might want to store the user's name or address. You can also use the user entity to define the relations between users and other entities in your app. For example, you might want to define a relation between a user and the tasks that they have created. + +```wasp +entity User {=psl + id Int @id @default(autoincrement()) + // Any other fields you want to store about the user +psl=} +``` + +You **own** the user entity and you can modify it as you wish. You can add new fields to it, remove fields from it, or change the type of the fields. You can also add new relations to it or remove existing relations from it. + + + +On the other hand, the `Auth`, `AuthIdentity` and `Session` entities are created behind the scenes and are used to store the user's login credentials. You as the developer don't need to care about this entity most of the time. Wasp **owns** these entities. + +In the case you want to create a custom signup action, you will need to use the `Auth` and `AuthIdentity` entities directly. + +### Example App Model +Let's imagine we created a simple tasks management app: + + - The app has email and Google-based auth. + - Users can create tasks and see the tasks that they have created. + +Let's look at how would that look in the database: + + + +If we take a look at an example user in the database, we can see: +- The business logic user, `User` is connected to multiple `Task` entities. + - In this example, "Example User" has two tasks. +- The `User` is connected to exactly one `Auth` entity. +- Each `Auth` entity can have multiple `AuthIdentity` entities. + - In this example, the `Auth` entity has two `AuthIdentity` entities: one for the email-based auth and one for the Google-based auth. +- Each `Auth` entity can have multiple `Session` entities. + - In this example, the `Auth` entity has one `Session` entity. + + + +### `Auth` Entity + +Wasp's internal `Auth` entity is used to connect the business logic user, `User` with the user's login credentials. + +```wasp +entity Auth {=psl + id String @id @default(uuid()) + userId Int? @unique + // Wasp injects this relation on the User entity as well + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + identities AuthIdentity[] + sessions Session[] +psl=} +``` + +The `Auth` fields: + +- `id` is a unique identifier of the `Auth` entity. +- `userId` is a foreign key to the `User` entity. + - It is used to connect the `Auth` entity with the business logic user. +- `user` is a relation to the `User` entity. + - This relation is injected on the `User` entity as well. +- `identities` is a relation to the `AuthIdentity` entity. +- `sessions` is a relation to the `Session` entity. + +### `AuthIdentity` Entity + +The `AuthIdentity` entity is used to store the user's login credentials for various authentication methods. + +```wasp +entity AuthIdentity {=psl + providerName String + providerUserId String + providerData String @default("{}") + authId String + auth Auth @relation(fields: [authId], references: [id], onDelete: Cascade) + + @@id([providerName, providerUserId]) +psl=} +``` + +The `AuthIdentity` fields: +- `providerName` is the name of the authentication provider. + - For example, `email` or `google`. +- `providerUserId` is the user's ID in the authentication provider. + - For example, the user's email or Google ID. +- `providerData` is a JSON string that contains additional data about the user from the authentication provider. + - For example, for password based auth, this field contains the user's hashed password. + - This field is a `String` and not a `Json` type because [Prisma doesn't support the `Json` type for SQLite](https://github.com/prisma/prisma/issues/3786). +- `authId` is a foreign key to the `Auth` entity. + - It is used to connect the `AuthIdentity` entity with the `Auth` entity. +- `auth` is a relation to the `Auth` entity. + +### `Session` Entity + +The `Session` entity is used to store the user's session information. It is used to keep the user logged in between page refreshes. + +```wasp +entity Session {=psl + id String @id @unique + expiresAt DateTime + userId String + auth Auth @relation(references: [id], fields: [userId], onDelete: Cascade) + + @@index([userId]) +psl=} +``` + +The `Session` fields: +- `id` is a unique identifier of the `Session` entity. +- `expiresAt` is the date when the session expires. +- `userId` is a foreign key to the `Auth` entity. + - It is used to connect the `Session` entity with the `Auth` entity. +- `auth` is a relation to the `Auth` entity. + +## Accessing the Auth Fields + +If you are looking to access the user's email or username in your code, you can do that by accessing the info about the user that is stored in the `AuthIdentity` entity. + +Everywhere where Wasp gives you the `user` object, it also includes the `auth` relation with the `identities` relation. This means that you can access the auth identity info by using the `user.auth.identities` array. + +To make things a bit easier for you, Wasp offers a few helper functions that you can use to access the auth identity info. + +### `getEmail` + + + +### `getUsername` + + + +### `getFirstProviderUserId` + +The `getFirstProviderUserId` helper returns the first user ID (e.g. `username` or `email`) that it finds for the user or `null` if it doesn't find any. + +[As mentioned before](#authidentity-entity-), the `providerUserId` field is how providers identify our users. For example, the user's `username` in the case of the username auth or the user's `email` in the case of the email auth. This can be useful if you support multiple authentication methods and you need *any* ID that identifies the user in your app. + + + + +```jsx title="src/client/MainPage.jsx" +import { getFirstProviderUserId } from '@wasp/auth/user' + +const MainPage = ({ user }) => { + const userId = getFirstProviderUserId(user) + // ... +} +``` + +```js title=src/server/tasks.js +import { getFirstProviderUserId } from '@wasp/auth/user.js' + +export const createTask = async (args, context) => { + const userId = getFirstProviderUserId(context.user) + // ... +} +``` + + + + + +```tsx title="src/client/MainPage.tsx" +import { getFirstProviderUserId } from '@wasp/auth/user' +import { User as AuthenticatedUser } from '@wasp/auth/types' + +const MainPage = ({ user }: { user: AuthenticatedUser }) => { + const userId = getFirstProviderUserId(user) + // ... +} +``` + +```ts title=src/server/tasks.ts +import { getFirstProviderUserId } from '@wasp/auth/user.js' + +export const createTask: CreateTask<...> = async (args, context) => { + const userId = getFirstProviderUserId(context.user) + // ... +} +``` + + + + +### `findUserIdentity` + +You can find a specific auth identity by using the `findUserIdentity` helper function. This function takes a `user` and a `providerName` and returns the first `providerName` identity that it finds or `null` if it doesn't find any. + +Possible provider names are: +- `email` +- `username` +- `google` +- `github` + +This can be useful if you want to check if the user has a specific auth identity. For example, you might want to check if the user has an email auth identity or Google auth identity. + + + + +```jsx title="src/client/MainPage.jsx" +import { findUserIdentity } from '@wasp/auth/user' + +const MainPage = ({ user }) => { + const emailIdentity = findUserIdentity(user, 'email') + const googleIdentity = findUserIdentity(user, 'google') + if (emailIdentity) { + // ... + } else if (googleIdentity) { + // ... + } + // ... +} +``` + +```js title=src/server/tasks.js +import { findUserIdentity } from '@wasp/auth/user.js' + +export const createTask = async (args, context) => { + const emailIdentity = findUserIdentity(context.user, 'email') + const googleIdentity = findUserIdentity(context.user, 'google') + if (emailIdentity) { + // ... + } else if (googleIdentity) { + // ... + } + // ... +} +``` + + + + + +```tsx title="src/client/MainPage.tsx" +import { findUserIdentity } from '@wasp/auth/user' +import { User as AuthenticatedUser } from '@wasp/auth/types' + +const MainPage = ({ user }: { user: AuthenticatedUser }) => { + const emailIdentity = findUserIdentity(user, 'email') + const googleIdentity = findUserIdentity(user, 'google') + if (emailIdentity) { + // ... + } else if (googleIdentity) { + // ... + } + // ... +} +``` + +```ts title=src/server/tasks.ts +import { findUserIdentity } from '@wasp/auth/user.js' + +export const createTask: CreateTask<...> = async (args, context) => { + const emailIdentity = findUserIdentity(context.user, 'email') + const googleIdentity = findUserIdentity(context.user, 'google') + if (emailIdentity) { + // ... + } else if (googleIdentity) { + // ... + } + // ... +} +``` + + + + +## Custom Signup Action + +Let's take a look at how you can use the `Auth` and `AuthIdentity` entities to create custom login and signup actions. For example, you might want to create a custom signup action that creates a user in your app and also creates a user in a third-party service. + +:::info Custom Signup Examples + +In the [Email](./email#creating-a-custom-sign-up-action) section of the docs we give you an example for custom email signup and in the [Username & password](./username-and-pass#2-creating-your-custom-sign-up-action) section of the docs we give you an example for custom username & password signup. +::: + +Below is a simplified version of a custom signup action which you probably wouldn't use in your app but it shows you how you can use the `Auth` and `AuthIdentity` entities to create a custom signup action. + + + + +```wasp title="main.wasp" +// ... + +action customSignup { + fn: import { signup } from "@server/auth/signup.js", + entities: [User] +} +``` + + +```js title="src/server/auth/signup.js" +import { + createProviderId, + sanitizeAndSerializeProviderData, + createUser, +} from '@wasp/auth/utils.js' + +export const signup = async (args, { entities: { User } }) => { + try { + // Provider ID is a combination of the provider name and the provider user ID + // And it is used to uniquely identify the user in your app + const providerId = createProviderId('username', args.username) + // sanitizeAndSerializeProviderData hashes the password and returns a JSON string + const providerData = await sanitizeAndSerializeProviderData({ + hashedPassword: args.password, + }) + + await createUser( + providerId, + providerData, + // Any additional data you want to store on the User entity + {}, + ) + + // This is equivalent to: + // await User.create({ + // data: { + // auth: { + // create: { + // identities: { + // create: { + // providerName: 'username', + // providerUserId: args.username + // providerData, + // }, + // }, + // } + // }, + // } + // }) + } catch (e) { + return { + success: false, + message: e.message, + } + } + + // Your custom code after sign-up. + // ... + + return { + success: true, + message: 'User created successfully', + } +} +``` + + + +```wasp title="main.wasp" +// ... + +action customSignup { + fn: import { signup } from "@server/auth/signup.js", + entities: [User] +} +``` + +```ts title="src/server/auth/signup.ts" +import { + createProviderId, + sanitizeAndSerializeProviderData, + createUser, +} from '@wasp/auth/utils.js' +import type { CustomSignup } from '@wasp/actions/types' + +type CustomSignupInput = { + username: string + password: string +} +type CustomSignupOutput = { + success: boolean + message: string +} + +export const signup: CustomSignup< + CustomSignupInput, + CustomSignupOutput +> = async (args, { entities: { User } }) => { + try { + // Provider ID is a combination of the provider name and the provider user ID + // And it is used to uniquely identify the user in your app + const providerId = createProviderId('username', args.username) + // sanitizeAndSerializeProviderData hashes the password and returns a JSON string + const providerData = await sanitizeAndSerializeProviderData<'username'>({ + hashedPassword: args.password, + }) + + await createUser( + providerId, + providerData, + // Any additional data you want to store on the User entity + {}, + ) + + // This is equivalent to: + // await User.create({ + // data: { + // auth: { + // create: { + // identities: { + // create: { + // providerName: 'username', + // providerUserId: args.username + // providerData, + // }, + // }, + // } + // }, + // } + // }) + } catch (e) { + return { + success: false, + message: e.message, + } + } + + // Your custom code after sign-up. + // ... + + return { + success: true, + message: 'User created successfully', + } +} +``` + + + +You can use whichever method suits your needs better: either the `createUser` function or Prisma's `User.create` method. The `createUser` function is a bit more convenient to use because it hides some of the complexity. On the other hand, the `User.create` method gives you more control over the data that is stored in the `Auth` and `AuthIdentity` entities. \ No newline at end of file diff --git a/web/docs/auth/overview.md b/web/docs/auth/overview.md index a4df59b54b..f3071de837 100644 --- a/web/docs/auth/overview.md +++ b/web/docs/auth/overview.md @@ -1,13 +1,20 @@ --- -title: Using Auth +title: Overview --- import { AuthMethodsGrid } from "@site/src/components/AuthMethodsGrid"; -import { Required } from "@site/src/components/Required"; +import { Required } from '@site/src/components/Tag'; +import ReadMoreAboutAuthEntities from './\_read-more-about-auth-entities.md'; -Auth is an essential piece of any serious application. Coincidentally, Wasp provides authentication and authorization support out of the box 🙃. +Auth is an essential piece of any serious application. That's why Wasp provides authentication and authorization support out of the box. -Enabling auth for your app is optional and can be done by configuring the `auth` field of the `app` declaration. +Here's a 1-minute tour of how full-stack auth works in Wasp: + +
    + +
    + +Enabling auth for your app is optional and can be done by configuring the `auth` field of your `app` declaration: @@ -18,7 +25,6 @@ app MyApp { //... auth: { userEntity: User, - externalAuthEntity: SocialLogin, methods: { usernameAndPassword: {}, // use this or email, not both email: {}, // use this or usernameAndPassword, not both @@ -41,7 +47,6 @@ app MyApp { //... auth: { userEntity: User, - externalAuthEntity: SocialLogin, methods: { usernameAndPassword: {}, // use this or email, not both email: {}, // use this or usernameAndPassword, not both @@ -72,11 +77,11 @@ Wasp supports the following auth methods: -Let's say we enabled the [Username & password](/docs/auth/username-and-pass) authentication. +Let's say we enabled the [Username & password](../auth/username-and-pass) authentication. -We get an auth backend with signup and login endpoints. We also get the `user` object in our [Operations](/docs/data-model/operations/overview) and we can decide what to do based on whether the user is logged in or not. +We get an auth backend with signup and login endpoints. We also get the `user` object in our [Operations](../data-model/operations/overview) and we can decide what to do based on whether the user is logged in or not. -We would also get the [Auth UI](/docs/auth/ui) generated for us. We can set up our login and signup pages where our users can **create their account** and **login**. We can then protect certain pages by setting `authRequired: true` for them. This will make sure that only logged-in users can access them. +We would also get the [Auth UI](../auth/ui) generated for us. We can set up our login and signup pages where our users can **create their account** and **login**. We can then protect certain pages by setting `authRequired: true` for them. This will make sure that only logged-in users can access them. We will also have access to the `user` object in our frontend code, so we can show different UI to logged-in and logged-out users. For example, we can show the user's name in the header alongside a **logout button** or a login button if the user is not logged in. @@ -146,7 +151,33 @@ const LogoutButton = () => { ## Accessing the logged-in user -You can get access to the `user` object both in the backend and on the frontend. +You can get access to the `user` object both on the server and on the client. The `user` object contains the logged-in user's data. + +The `user` object has all the fields that you defined in your `User` entity, plus the `auth` field which contains the auth identities connected to the user. For example, if the user signed up with their email, the `user` object might look something like this: + +```js +const user = { + id: "19c7d164-b5cb-4dde-a0cc-0daea77cf854", + + // Your entity's fields. + address: "My address", + // ... + + // Auth identities connected to the user. + auth: { + id: "26ab6f96-ed76-4ee5-9ac3-2fd0bf19711f", + identities: [ + { + providerName: "email", + providerUserId: "some@email.com", + providerData: { ... }, + }, + ] + }, +} +``` + + ### On the client @@ -200,11 +231,11 @@ page AccountPage { ``` ```tsx title="client/pages/Account.tsx" -import type { User } from '@wasp/entities' +import { User as AuthenticatedUser } from '@wasp/auth/types' import Button from './Button' import logout from '@wasp/auth/logout' -const AccountPage = ({ user }: { user: User }) => { +const AccountPage = ({ user }: { user: AuthenticatedUser }) => { return (
    @@ -295,7 +326,7 @@ Since the `user` prop is only available in a page's React component: use the `us #### Using the `context.user` object -When authentication is enabled, all [queries and actions](/docs/data-model/operations/overview) have access to the `user` object through the `context` argument. `context.user` contains all User entity's fields, except for the password. +When authentication is enabled, all [queries and actions](../data-model/operations/overview) have access to the `user` object through the `context` argument. `context.user` contains all User entity's fields and the auth identities connected to the user. We strip out the `hashedPassword` field from the identities for security reasons. @@ -355,25 +386,51 @@ export const createTask: CreateTask = async ( To implement access control in your app, each operation must check `context.user` and decide what to do. For example, if `context.user` is `undefined` inside a private operation, the user's access should be denied. -When using WebSockets, the `user` object is also available on the `socket.data` object. Read more in the [WebSockets section](/docs/advanced/web-sockets#websocketfn-function). +When using WebSockets, the `user` object is also available on the `socket.data` object. Read more in the [WebSockets section](../advanced/web-sockets#websocketfn-function). + +## Sessions + +Wasp's auth uses sessions to keep track of the logged-in user. The session is stored in `localStorage` on the client and in the database on the server. Under the hood, Wasp uses the excellent [Lucia Auth v3](https://v3.lucia-auth.com/) library for session management. + +When users log in, Wasp creates a session for them and stores it in the database. The session is then sent to the client and stored in `localStorage`. When users log out, Wasp deletes the session from the database and from `localStorage`. + +## User Entity -## User entity +### Password Hashing -### Password hashing +If you are saving a user's password in the database, you should **never** save it as plain text. You can use Wasp's helper functions for serializing and deserializing provider data which will automatically hash the password for you: -You don't need to worry about hashing the password yourself. Even when directly using the Prisma client and calling `create()` with a plain-text password, Wasp's middleware makes sure to hash the password before storing it in the database. -For example, if you need to update a user's password, you can safely use the Prisma client to do so, e.g., inside an Action: +```wasp title="main.wasp" +// ... + +action updatePassword { + fn: import { updatePassword } from "@server/auth.js", +} +``` ```js title="src/server/actions.js" +import { + createProviderId, + findAuthIdentity, + updateAuthIdentityProviderData, + deserializeAndSanitizeProviderData, +} from '@wasp/auth/utils.js'; + export const updatePassword = async (args, context) => { - return context.entities.User.update({ - where: { id: args.userId }, - data: { - password: 'New pwd which will be hashed automatically!', - }, + const providerId = createProviderId('email', args.email) + const authIdentity = await findAuthIdentity(providerId) + if (!authIdentity) { + throw new HttpError(400, "Unknown user") + } + + const providerData = deserializeAndSanitizeProviderData(authIdentity.providerData) + + // Updates the password and hashes it automatically. + await updateAuthIdentityProviderData(providerId, providerData, { + hashedPassword: args.password, }) } ``` @@ -382,22 +439,29 @@ export const updatePassword = async (args, context) => { ```ts title="src/server/actions.ts" +import { + createProviderId, + findAuthIdentity, + updateAuthIdentityProviderData, + deserializeAndSanitizeProviderData, +} from '@wasp/auth/utils.js'; import type { UpdatePassword } from '@wasp/actions/types' -import type { User } from '@wasp/entities' - -type UpdatePasswordPayload = { - userId: User['id'] -} export const updatePassword: UpdatePassword< - UpdatePasswordPayload, - User + { email: string; password: string }, + void, > = async (args, context) => { - return context.entities.User.update({ - where: { id: args.userId }, - data: { - password: 'New pwd which will be hashed automatically!', - }, + const providerId = createProviderId('email', args.email) + const authIdentity = await findAuthIdentity(providerId) + if (!authIdentity) { + throw new HttpError(400, "Unknown user") + } + + const providerData = deserializeAndSanitizeProviderData<'email'>(authIdentity.providerData) + + // Updates the password and hashes it automatically. + await updateAuthIdentityProviderData(providerId, providerData, { + hashedPassword: args.password, }) } ``` @@ -405,26 +469,26 @@ export const updatePassword: UpdatePassword< -### Default validations +### Default Validations -When you are using the default authentication flow, Wasp validates the fields with some default validations. These validations run if you use Wasp's built-in [Auth UI](/docs/auth/ui) or if you use the provided auth actions. +When you are using the default authentication flow, Wasp validates the fields with some default validations. These validations run if you use Wasp's built-in [Auth UI](./ui) or if you use the provided auth actions. -If you decide to create your [custom auth actions](/docs/auth/username-and-pass#2-creating-your-custom-sign-up-action), you'll need to run the validations yourself. +If you decide to create your [custom auth actions](./username-and-pass#2-creating-your-custom-sign-up-action), you'll need to run the validations yourself. Default validations depend on the auth method you use. -#### Username & password +#### Username & Password -If you use [Username & password](/docs/auth/username-and-pass) authentication, the default validations are: +If you use [Username & password](./username-and-pass) authentication, the default validations are: - The `username` must not be empty - The `password` must not be empty, have at least 8 characters, and contain a number -Note that `username`s are stored in a **case-sensitive** manner. +Note that `username`s are stored in a **case-insensitive** manner. #### Email -If you use [Email](/docs/auth/email) authentication, the default validations are: +If you use [Email](./email) authentication, the default validations are: - The `email` must not be empty and a valid email address - The `password` must not be empty, have at least 8 characters, and contain a number @@ -471,12 +535,13 @@ try { ## Customizing the Signup Process -Sometimes you want to include **extra fields** in your signup process, like first name and last name. +Sometimes you want to include **extra fields** in your signup process, like first name and last name and save them in the `User` entity. -In Wasp, in this case: +For this to happen: - you need to define the fields that you want saved in the database, -- you need to customize the `SignupForm`. +- you need to customize the `SignupForm` (in the case of [Email](./email) or [Username & Password](./username-and-pass) auth) + Other times, you might need to just add some **extra UI** elements to the form, like a checkbox for terms of service. In this case, customizing only the UI components is enough. @@ -493,40 +558,39 @@ We do that by defining an object where the keys represent the field name, and th \* We exclude the `password` field from this object to prevent it from being saved as plain-text in the database. The `password` field is handled by Wasp's auth backend. -First, we add the `auth.signup.additionalFields` field in our `main.wasp` file: +First, we add the `auth.methods.{authMethod}.userSignupFields` field in our `main.wasp` file. The `{authMethod}` depends on the auth method you are using. + +For example, if you are using [Username & Password](./username-and-pass), you would add the `auth.methods.usernameAndPassword.userSignupFields` field: -```wasp title="main.wasp" {9-11} +```wasp title="main.wasp" app crudTesting { // ... auth: { userEntity: User, methods: { - usernameAndPassword: {}, + usernameAndPassword: { + userSignupFields: import { userSignupFields } from "@server/auth/signup.js", + }, }, onAuthFailedRedirectTo: "/login", - signup: { - additionalFields: import { fields } from "@server/auth/signup.js", - }, }, } entity User {=psl id Int @id @default(autoincrement()) - username String @unique - password String address String? psl=} ``` -Then we'll define and export the `fields` object from the `server/auth/signup.js` file: +Then we'll define the `userSignupFields` object in the `server/auth/signup.js` file: ```ts title="server/auth/signup.js" -import { defineAdditionalSignupFields } from '@wasp/auth/index.js' +import { defineUserSignupFields } from '@wasp/auth/index.js' -export const fields = defineAdditionalSignupFields({ +export const userSignupFields = defineUserSignupFields({ address: async (data) => { const address = data.address if (typeof address !== 'string') { @@ -543,35 +607,32 @@ export const fields = defineAdditionalSignupFields({ -```wasp title="main.wasp" {9-11} +```wasp title="main.wasp" app crudTesting { // ... auth: { userEntity: User, methods: { - usernameAndPassword: {}, + usernameAndPassword: { + userSignupFields: import { userSignupFields } from "@server/auth/signup.js", + }, }, onAuthFailedRedirectTo: "/login", - signup: { - additionalFields: import { fields } from "@server/auth/signup.js", - }, }, } entity User {=psl id Int @id @default(autoincrement()) - username String @unique - password String address String? psl=} ``` -Then we'll export the `fields` object from the `server/auth/signup.ts` file: +Then we'll define the `userSignupFields` object in the `server/auth/signup.js` file: ```ts title="server/auth/signup.ts" -import { defineAdditionalSignupFields } from '@wasp/auth/index.js' +import { defineUserSignupFields } from '@wasp/auth/index.js' -export const fields = defineAdditionalSignupFields({ +export const userSignupFields = defineUserSignupFields({ address: async (data) => { const address = data.address if (typeof address !== 'string') { @@ -590,7 +651,7 @@ export const fields = defineAdditionalSignupFields({ -Read more about the `fields` object in the [API Reference](#signup-fields-customization). +Read more about the `userSignupFields` object in the [API Reference](#signup-fields-customization). Keep in mind, that these field names need to exist on the `userEntity` you defined in your `main.wasp` file e.g. `address` needs to be a field on the `User` entity. @@ -608,10 +669,10 @@ You can use any validation library you want to validate the fields. For example, ```js title="server/auth/signup.js" -import { defineAdditionalSignupFields } from '@wasp/auth/index.js' +import { defineUserSignupFields } from '@wasp/auth/index.js' import * as z from 'zod' -export const fields = defineAdditionalSignupFields({ +export const userSignupFields = defineUserSignupFields({ address: (data) => { const AddressSchema = z .string({ @@ -632,10 +693,10 @@ export const fields = defineAdditionalSignupFields({ ```ts title="server/auth/signup.ts" -import { defineAdditionalSignupFields } from '@wasp/auth/index.js' +import { defineUserSignupFields } from '@wasp/auth/index.js' import * as z from 'zod' -export const fields = defineAdditionalSignupFields({ +export const userSignupFields = defineUserSignupFields({ address: (data) => { const AddressSchema = z .string({ @@ -663,7 +724,7 @@ Now that we defined the fields, Wasp knows how to: 1. Validate the data sent from the client 2. Save the data to the database -Next, let's see how to customize [Auth UI](/docs/auth/ui) to include those fields. +Next, let's see how to customize [Auth UI](../auth/ui) to include those fields. ### 2. Customizing the Signup Component @@ -673,8 +734,8 @@ If you are not using Wasp's Auth UI, you can skip this section. Just make sure t Read more about using the signup actions for: -- email auth [here](/docs/auth/email#fields-in-the-email-dict) -- username & password auth [here](/docs/auth/username-and-pass#customizing-the-auth-flow) +- email auth [here](../auth/email#fields-in-the-email-dict) +- username & password auth [here](../auth/username-and-pass#customizing-the-auth-flow) ::: If you are using Wasp's Auth UI, you can customize the `SignupForm` component by passing the `additionalFields` prop to it. It can be either a list of extra fields or a render function. @@ -870,7 +931,6 @@ Read more about the render function in the [API Reference](#signupform-customiza //... auth: { userEntity: User, - externalAuthEntity: SocialLogin, methods: { usernameAndPassword: {}, // use this or email, not both email: {}, // use this or usernameAndPassword, not both @@ -894,7 +954,6 @@ app MyApp { //... auth: { userEntity: User, - externalAuthEntity: SocialLogin, methods: { usernameAndPassword: {}, // use this or email, not both email: {}, // use this or usernameAndPassword, not both @@ -916,74 +975,9 @@ app MyApp { #### `userEntity: entity` -The entity representing the user. Its mandatory fields depend on your chosen auth method. +The entity representing the user connected to your business logic. -#### `externalAuthEntity: entity` - -Wasp requires you to set the field `auth.externalAuthEntity` for all authentication methods relying on an external authorizatino provider (e.g., Google). You also need to tweak the Entity referenced by `auth.userEntity`, as shown below. - - - - -```wasp {4,14} title="main.wasp" -//... - auth: { - userEntity: User, - externalAuthEntity: SocialLogin, -//... - -entity User {=psl - id Int @id @default(autoincrement()) - //... - externalAuthAssociations SocialLogin[] -psl=} - -entity SocialLogin {=psl - id Int @id @default(autoincrement()) - provider String - providerId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int - createdAt DateTime @default(now()) - @@unique([provider, providerId, userId]) -psl=} -``` - - - - -```wasp {4,14} title="main.wasp" -//... - auth: { - userEntity: User, - externalAuthEntity: SocialLogin, -//... - -entity User {=psl - id Int @id @default(autoincrement()) - //... - externalAuthAssociations SocialLogin[] -psl=} - -entity SocialLogin {=psl - id Int @id @default(autoincrement()) - provider String - providerId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int - createdAt DateTime @default(now()) - @@unique([provider, providerId, userId]) -psl=} -``` - - - - -:::note -The same `externalAuthEntity` can be used across different social login providers (e.g., both GitHub and Google can use the same entity). -::: - -See [Google docs](/docs/auth/social-auth/google) and [GitHub docs](/docs/auth/social-auth/github) for more details. + #### `methods: dict` @@ -994,7 +988,7 @@ A dictionary of auth methods enabled for the app. #### `onAuthFailedRedirectTo: String` The route to which Wasp should redirect unauthenticated user when they try to access a private page (i.e., a page that has `authRequired: true`). -Check out these [essentials docs on auth](/docs/tutorial/auth#adding-auth-to-the-project) to see an example of usage. +Check out these [essentials docs on auth](../tutorial/auth#adding-auth-to-the-project) to see an example of usage. #### `onAuthSucceededRedirectTo: String` @@ -1002,7 +996,7 @@ The route to which Wasp will send a successfully authenticated after a successfu The default value is `"/"`. :::note -Automatic redirect on successful login only works when using the Wasp-provided [Auth UI](/docs/auth/ui). +Automatic redirect on successful login only works when using the Wasp-provided [Auth UI](../auth/ui). ::: #### `signup: SignupOptions` @@ -1011,33 +1005,33 @@ Read more about the signup process customization API in the [Signup Fields Custo ### Signup Fields Customization -If you want to add extra fields to the signup process, the server needs to know how to save them to the database. You do that by defining the `auth.signup.additionalFields` field in your `main.wasp` file. +If you want to add extra fields to the signup process, the server needs to know how to save them to the database. You do that by defining the `auth.methods.{authMethod}.userSignupFields` field in your `main.wasp` file. -```wasp title="main.wasp" {9-11} +```wasp title="main.wasp" app crudTesting { // ... auth: { userEntity: User, methods: { - usernameAndPassword: {}, + usernameAndPassword: { + // highlight-next-line + userSignupFields: import { userSignupFields } from "@server/auth/signup.js", + }, }, onAuthFailedRedirectTo: "/login", - signup: { - additionalFields: import { fields } from "@server/auth/signup.js", - }, }, } ``` -Then we'll export the `fields` object from the `server/auth/signup.js` file: +Then we'll export the `userSignupFields` object from the `server/auth/signup.js` file: ```ts title="server/auth/signup.js" -import { defineAdditionalSignupFields } from '@wasp/auth/index.js' +import { defineUserSignupFields } from '@wasp/auth/index.js' -export const fields = defineAdditionalSignupFields({ +export const userSignupFields = defineUserSignupFields({ address: async (data) => { const address = data.address if (typeof address !== 'string') { @@ -1054,28 +1048,28 @@ export const fields = defineAdditionalSignupFields({ -```wasp title="main.wasp" {9-11} +```wasp title="main.wasp" app crudTesting { // ... auth: { userEntity: User, methods: { - usernameAndPassword: {}, + usernameAndPassword: { + // highlight-next-line + userSignupFields: import { userSignupFields } from "@server/auth/signup.js", + }, }, onAuthFailedRedirectTo: "/login", - signup: { - additionalFields: import { fields } from "@server/auth/signup.js", - }, }, } ``` -Then we'll export the `fields` object from the `server/auth/signup.ts` file: +Then we'll export the `userSignupFields` object from the `server/auth/signup.ts` file: ```ts title="server/auth/signup.ts" -import { defineAdditionalSignupFields } from '@wasp/auth/index.js' +import { defineUserSignupFields } from '@wasp/auth/index.js' -export const fields = defineAdditionalSignupFields({ +export const userSignupFields = defineUserSignupFields({ address: async (data) => { const address = data.address if (typeof address !== 'string') { @@ -1092,13 +1086,13 @@ export const fields = defineAdditionalSignupFields({ -The `fields` object is an object where the keys represent the field name, and the values are functions which receive the data sent from the client\* and return the value of the field. +The `userSignupFields` object is an object where the keys represent the field name, and the values are functions that receive the data sent from the client\* and return the value of the field. -If the field value is invalid, the function should throw an error. +If the value that the function received is invalid, the function should throw an error. -\* We exclude the `password` field from this object to prevent it from being saved as plain-text in the database. The `password` field is handled by Wasp's auth backend. +\* We exclude the `password` field from this object to prevent it from being saved as plain text in the database. The `password` field is handled by Wasp's auth backend. ### `SignupForm` Customization diff --git a/web/docs/auth/social-auth/_api-reference-intro.md b/web/docs/auth/social-auth/_api-reference-intro.md index 3fb34065d4..f9b90f022b 100644 --- a/web/docs/auth/social-auth/_api-reference-intro.md +++ b/web/docs/auth/social-auth/_api-reference-intro.md @@ -1,10 +1,10 @@ Provider-specific behavior comes down to implementing two functions. - `configFn` -- `getUserFieldsFn` +- `userSignupFields` The reference shows how to define both. -For behavior common to all providers, check the general [API Reference](/docs/auth/overview.md#api-reference). +For behavior common to all providers, check the general [API Reference](../../auth/overview.md#api-reference). diff --git a/web/docs/auth/social-auth/_default-behaviour.md b/web/docs/auth/social-auth/_default-behaviour.md index 3ce6c65606..a5e2462374 100644 --- a/web/docs/auth/social-auth/_default-behaviour.md +++ b/web/docs/auth/social-auth/_default-behaviour.md @@ -1,10 +1,3 @@ When a user **signs in for the first time**, Wasp creates a new user account and links it to the chosen auth provider account for future logins. -Also, if the `userEntity` has: - -- A `username` field: Wasp sets it to a random username (e.g. `nice-blue-horse-14357`). -- A `password` field: Wasp sets it to a random string. - -This is a historical coupling between `auth` methods we plan to remove in the future. - diff --git a/web/docs/auth/social-auth/_getuserfields-type.md b/web/docs/auth/social-auth/_getuserfields-type.md index a630a3e562..091473d851 100644 --- a/web/docs/auth/social-auth/_getuserfields-type.md +++ b/web/docs/auth/social-auth/_getuserfields-type.md @@ -1,3 +1,3 @@ -Wasp automatically generates the type `GetUserFieldsFn` to help you correctly type your `getUserFields` function. +Wasp automatically generates the `defineUserSignupFields` function to help you correctly type your `userSignupFields` object. diff --git a/web/docs/auth/social-auth/_override-example-intro.md b/web/docs/auth/social-auth/_override-example-intro.md index 01fad70340..5b89b83a92 100644 --- a/web/docs/auth/social-auth/_override-example-intro.md +++ b/web/docs/auth/social-auth/_override-example-intro.md @@ -1,10 +1,10 @@ When a user logs in using a social login provider, the backend receives some data about the user. -Wasp lets you access this data inside the `getUserFieldsFn` function. +Wasp lets you access this data inside the `userSignupFields` getters. For example, the User entity can include a `displayName` field which you can set based on the details received from the provider. Wasp also lets you customize the configuration of the providers' settings using the `configFn` function. -Let's use this example to show both functions in action: +Let's use this example to show both fields in action: diff --git a/web/docs/auth/social-auth/_override-intro.md b/web/docs/auth/social-auth/_override-intro.md index 0c399d3ca0..ea438c6fc2 100644 --- a/web/docs/auth/social-auth/_override-intro.md +++ b/web/docs/auth/social-auth/_override-intro.md @@ -1,8 +1,8 @@ -Wasp lets you override the default behavior. You can create custom setups, such as allowing users to define a custom username rather instead of getting a randomly generated one. +By default, Wasp doesn't store any information it receives from the social login provider. It only stores the user's ID specific to the provider. -There are two mechanisms (functions) used for overriding the default behavior: +There are two mechanisms used for overriding the default behavior: -- `getUserFieldsFn` +- `userSignupFields` - `configFn` Let's explore them in more detail. diff --git a/web/docs/auth/social-auth/_using-auth-note.md b/web/docs/auth/social-auth/_using-auth-note.md index 8ddc130587..7fe740be18 100644 --- a/web/docs/auth/social-auth/_using-auth-note.md +++ b/web/docs/auth/social-auth/_using-auth-note.md @@ -1,3 +1,3 @@ -To read more about how to set up the logout button and get access to the logged-in user in both client and server code, read the docs on [using auth](/docs/auth/overview). +To read more about how to set up the logout button and get access to the logged-in user in both client and server code, read the docs on [using auth](../../auth/overview). diff --git a/web/docs/auth/social-auth/_wasp-file-structure-note.md b/web/docs/auth/social-auth/_wasp-file-structure-note.md index ea441c1309..b30c2c2daa 100644 --- a/web/docs/auth/social-auth/_wasp-file-structure-note.md +++ b/web/docs/auth/social-auth/_wasp-file-structure-note.md @@ -8,7 +8,6 @@ app myApp { // Defining entities entity User { ... } -entity SocialLogin { ... } // Defining routes and pages route LoginRoute { ... } diff --git a/web/docs/auth/social-auth/github.md b/web/docs/auth/social-auth/github.md index 67c067688c..2f2f6fe93e 100644 --- a/web/docs/auth/social-auth/github.md +++ b/web/docs/auth/social-auth/github.md @@ -8,9 +8,9 @@ import OverrideIntro from './\_override-intro.md'; import OverrideExampleIntro from './\_override-example-intro.md'; import UsingAuthNote from './\_using-auth-note.md'; import WaspFileStructureNote from './\_wasp-file-structure-note.md'; -import UsernameGenerateExplanation from './\_username-generate-explanation.md'; import GetUserFieldsType from './\_getuserfields-type.md'; import ApiReferenceIntro from './\_api-reference-intro.md'; +import UserSignupFieldsExplainer from '../\_user-signup-fields-explainer.md'; Wasp supports Github Authentication out of the box. GitHub is a great external auth choice when you're building apps for developers, as most of them already have a GitHub account. @@ -24,7 +24,7 @@ Let's walk through enabling Github Authentication, explain some of the default s Enabling GitHub Authentication comes down to a series of steps: 1. Enabling GitHub authentication in the Wasp file. -1. Adding the necessary Entities. +1. Adding the `User` entity. 1. Creating a GitHub OAuth app. 1. Adding the neccessary Routes and Pages 1. Using Auth UI components in our Pages. @@ -49,13 +49,9 @@ app myApp { // 1. Specify the User entity (we'll define it next) // highlight-next-line userEntity: User, - // highlight-next-line - // 2. Specify the SocialLogin entity (we'll define it next) - // highlight-next-line - externalAuthEntity: SocialLogin, methods: { // highlight-next-line - // 3. Enable Github Auth + // 2. Enable Github Auth // highlight-next-line gitHub: {} }, @@ -78,13 +74,9 @@ app myApp { // 1. Specify the User entity (we'll define it next) // highlight-next-line userEntity: User, - // highlight-next-line - // 2. Specify the SocialLogin entity (we'll define it next) - // highlight-next-line - externalAuthEntity: SocialLogin, methods: { // highlight-next-line - // 3. Enable Github Auth + // 2. Enable Github Auth // highlight-next-line gitHub: {} }, @@ -96,35 +88,20 @@ app myApp { -### 2. Add the Entities +### 2. Add the User Entity -Let's now define the entities acting as `app.auth.userEntity` and `app.auth.externalAuthEntity`: +Let's now define the `app.auth.userEntity` entity: ```wasp title="main.wasp" // ... -// highlight-next-line -// 4. Define the User entity +// 3. Define the User entity // highlight-next-line entity User {=psl id Int @id @default(autoincrement()) // ... - externalAuthAssociations SocialLogin[] -psl=} - -// highlight-next-line -// 5. Define the SocialLogin entity -// highlight-next-line -entity SocialLogin {=psl - id Int @id @default(autoincrement()) - provider String - providerId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int - createdAt DateTime @default(now()) - @@unique([provider, providerId, userId]) psl=} ``` @@ -133,34 +110,17 @@ psl=} ```wasp title="main.wasp" // ... -// highlight-next-line -// 4. Define the User entity +// 3. Define the User entity // highlight-next-line entity User {=psl id Int @id @default(autoincrement()) // ... - externalAuthAssociations SocialLogin[] -psl=} - -// highlight-next-line -// 5. Define the SocialLogin entity -// highlight-next-line -entity SocialLogin {=psl - id Int @id @default(autoincrement()) - provider String - providerId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int - createdAt DateTime @default(now()) - @@unique([provider, providerId, userId]) psl=} ``` -`externalAuthEntity` and `userEntity` are explained in [the social auth overview](/docs/auth/social-auth/overview#social-login-entity). - ### 3. Creating a GitHub OAuth App To use GitHub as an authentication method, you'll first need to create a GitHub OAuth App and provide Wasp with your client key and secret. Here's how you do it: @@ -231,7 +191,7 @@ We'll define the React components for these pages in the `client/pages/auth.{jsx ### 6. Creating the Client Pages :::info -We are using [Tailwind CSS](https://tailwindcss.com/) to style the pages. Read more about how to add it [here](/docs/project/css-frameworks). +We are using [Tailwind CSS](https://tailwindcss.com/) to style the pages. Read more about how to add it [here](../../project/css-frameworks). ::: Let's create a `auth.{jsx,tsx}` file in the `client/pages` folder and add the following to it: @@ -295,7 +255,7 @@ export function Layout({ children }: { children: React.ReactNode }) { -We imported the generated Auth UI component and used them in our pages. Read more about the Auth UI components [here](/docs/auth/ui). +We imported the generated Auth UI component and used them in our pages. Read more about the Auth UI components [here](../../auth/ui). ### Conclusion @@ -304,7 +264,7 @@ Yay, we've successfully set up Github Auth! 🎉 ![Github Auth](/img/auth/github.png) Running `wasp db migrate-dev` and `wasp start` should now give you a working app with authentication. -To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on [using auth](/docs/auth/overview). +To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on [using auth](../../auth/overview). ## Default Behaviour @@ -313,7 +273,7 @@ Add `gitHub: {}` to the `auth.methods` dictionary to use it with default setting -```wasp title=main.wasp {10} +```wasp title=main.wasp app myApp { wasp: { version: "^0.11.0" @@ -321,8 +281,8 @@ app myApp { title: "My App", auth: { userEntity: User, - externalAuthEntity: SocialLogin, methods: { + // highlight-next-line gitHub: {} }, onAuthFailedRedirectTo: "/login" @@ -333,7 +293,7 @@ app myApp { -```wasp title=main.wasp {10} +```wasp title=main.wasp app myApp { wasp: { version: "^0.11.0" @@ -341,8 +301,8 @@ app myApp { title: "My App", auth: { userEntity: User, - externalAuthEntity: SocialLogin, methods: { + // highlight-next-line gitHub: {} }, onAuthFailedRedirectTo: "/login" @@ -366,7 +326,7 @@ app myApp { -```wasp title="main.wasp" {11-12,22} +```wasp title="main.wasp" app myApp { wasp: { version: "^0.11.0" @@ -374,11 +334,12 @@ app myApp { title: "My App", auth: { userEntity: User, - externalAuthEntity: SocialLogin, methods: { gitHub: { + // highlight-next-line configFn: import { getConfig } from "@server/auth/github.js", - getUserFieldsFn: import { getUserFields } from "@server/auth/github.js" + // highlight-next-line + userSignupFields: import { userSignupFields } from "@server/auth/github.js" } }, onAuthFailedRedirectTo: "/login" @@ -389,19 +350,15 @@ entity User {=psl id Int @id @default(autoincrement()) username String @unique displayName String - externalAuthAssociations SocialLogin[] psl=} // ... ``` ```js title=src/server/auth/github.js -import { generateAvailableDictionaryUsername } from "@wasp/core/auth.js"; - -export const getUserFields = async (_context, args) => { - const username = await generateAvailableDictionaryUsername(); - const displayName = args.profile.displayName; - return { username, displayName }; +export const userSignupFields = { + username: () => "hardcoded-username", + displayName: (data) => data.profile.displayName, }; export function getConfig() { @@ -416,7 +373,7 @@ export function getConfig() { -```wasp title="main.wasp" {11-12,22} +```wasp title="main.wasp" app myApp { wasp: { version: "^0.11.0" @@ -424,11 +381,12 @@ app myApp { title: "My App", auth: { userEntity: User, - externalAuthEntity: SocialLogin, methods: { gitHub: { + // highlight-next-line configFn: import { getConfig } from "@server/auth/github.js", - getUserFieldsFn: import { getUserFields } from "@server/auth/github.js" + // highlight-next-line + userSignupFields: import { userSignupFields } from "@server/auth/github.js" } }, onAuthFailedRedirectTo: "/login" @@ -439,21 +397,18 @@ entity User {=psl id Int @id @default(autoincrement()) username String @unique displayName String - externalAuthAssociations SocialLogin[] psl=} // ... ``` ```ts title=src/server/auth/github.ts -import type { GetUserFieldsFn } from '@wasp/types' -import { generateAvailableDictionaryUsername } from '@wasp/core/auth.js' +import { defineUserSignupFields } from '@wasp/auth/index.js' -export const getUserFields: GetUserFieldsFn = async (_context, args) => { - const username = await generateAvailableDictionaryUsername() - const displayName = args.profile.displayName - return { username, displayName } -} +export const userSignupFields = defineUserSignupFields({ + username: () => "hardcoded-username", + displayName: (data) => data.profile.displayName, +}) export function getConfig() { return { @@ -480,7 +435,7 @@ export function getConfig() { -```wasp title="main.wasp" {11-12} +```wasp title="main.wasp" app myApp { wasp: { version: "^0.11.0" @@ -488,11 +443,12 @@ app myApp { title: "My App", auth: { userEntity: User, - externalAuthEntity: SocialLogin, methods: { gitHub: { + // highlight-next-line configFn: import { getConfig } from "@server/auth/github.js", - getUserFieldsFn: import { getUserFields } from "@server/auth/github.js" + // highlight-next-line + userSignupFields: import { userSignupFields } from "@server/auth/github.js" } }, onAuthFailedRedirectTo: "/login" @@ -503,7 +459,7 @@ app myApp { -```wasp title="main.wasp" {11-12} +```wasp title="main.wasp" app myApp { wasp: { version: "^0.11.0" @@ -511,11 +467,12 @@ app myApp { title: "My App", auth: { userEntity: User, - externalAuthEntity: SocialLogin, methods: { gitHub: { + // highlight-next-line configFn: import { getConfig } from "@server/auth/github.js", - getUserFieldsFn: import { getUserFields } from "@server/auth/github.js" + // highlight-next-line + userSignupFields: import { userSignupFields } from "@server/auth/github.js" } }, onAuthFailedRedirectTo: "/login" @@ -561,46 +518,6 @@ The `gitHub` dict has the following properties: -- #### `getUserFieldsFn: ServerImport` - - This function should return the user fields to use when creating a new user. - - The `context` contains the `User` entity, and the `args` object contains GitHub profile information. - You can do whatever you want with this information (e.g., generate a username). - - Here is how you could generate a username based on the Github display name: - - - - ```js title=src/server/auth/github.js - import { generateAvailableUsername } from '@wasp/core/auth.js' - - export const getUserFields = async (_context, args) => { - const username = await generateAvailableUsername( - args.profile.displayName.split(' '), - { separator: '.' } - ) - return { username } - } - ``` - - - - - ```ts title=src/server/auth/github.ts - import type { GetUserFieldsFn } from '@wasp/types' - import { generateAvailableUsername } from '@wasp/core/auth.js' - - export const getUserFields: GetUserFieldsFn = async (_context, args) => { - const username = await generateAvailableUsername( - args.profile.displayName.split(' '), - { separator: '.' } - ) - return { username } - } - ``` - - - +- #### `userSignupFields: ServerImport` - + \ No newline at end of file diff --git a/web/docs/auth/social-auth/google.md b/web/docs/auth/social-auth/google.md index 0d38c1d839..1beebf4471 100644 --- a/web/docs/auth/social-auth/google.md +++ b/web/docs/auth/social-auth/google.md @@ -8,9 +8,9 @@ import OverrideIntro from './\_override-intro.md'; import OverrideExampleIntro from './\_override-example-intro.md'; import UsingAuthNote from './\_using-auth-note.md'; import WaspFileStructureNote from './\_wasp-file-structure-note.md'; -import UsernameGenerateExplanation from './\_username-generate-explanation.md'; import GetUserFieldsType from './\_getuserfields-type.md'; import ApiReferenceIntro from './\_api-reference-intro.md'; +import UserSignupFieldsExplainer from '../\_user-signup-fields-explainer.md'; Wasp supports Google Authentication out of the box. Google Auth is arguably the best external auth option, as most users on the web already have Google accounts. @@ -24,7 +24,7 @@ Let's walk through enabling Google authentication, explain some of the default s Enabling Google Authentication comes down to a series of steps: 1. Enabling Google authentication in the Wasp file. -1. Adding the necessary Entities. +1. Adding the `User` entity. 1. Creating a Google OAuth app. 1. Adding the neccessary Routes and Pages 1. Using Auth UI components in our Pages. @@ -45,17 +45,11 @@ app myApp { }, title: "My App", auth: { - // highlight-next-line // 1. Specify the User entity (we'll define it next) // highlight-next-line userEntity: User, - // highlight-next-line - // 2. Specify the SocialLogin entity (we'll define it next) - // highlight-next-line - externalAuthEntity: SocialLogin, methods: { - // highlight-next-line - // 3. Enable Google Auth + // 2. Enable Google Auth // highlight-next-line google: {} }, @@ -74,17 +68,11 @@ app myApp { }, title: "My App", auth: { - // highlight-next-line // 1. Specify the User entity (we'll define it next) // highlight-next-line userEntity: User, - // highlight-next-line - // 2. Specify the SocialLogin entity (we'll define it next) - // highlight-next-line - externalAuthEntity: SocialLogin, methods: { - // highlight-next-line - // 3. Enable Google Auth + // 2. Enable Google Auth // highlight-next-line google: {} }, @@ -96,37 +84,22 @@ app myApp { -`externalAuthEntity` and `userEntity` are explained in [the social auth overview](/docs/auth/social-auth/overview#social-login-entity). +`userEntity` is explained in [the social auth overview](../../auth/social-auth/overview#social-login-entity). -### 2. Adding the Entities +### 2. Adding the User Entity -Let's now define the entities acting as `app.auth.userEntity` and `app.auth.externalAuthEntity`: +Let's now define the `app.auth.userEntity` entity: ```wasp title="main.wasp" // ... -// highlight-next-line -// 4. Define the User entity +// 3. Define the User entity // highlight-next-line entity User {=psl id Int @id @default(autoincrement()) // ... - externalAuthAssociations SocialLogin[] -psl=} - -// highlight-next-line -// 5. Define the SocialLogin entity -// highlight-next-line -entity SocialLogin {=psl - id Int @id @default(autoincrement()) - provider String - providerId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int - createdAt DateTime @default(now()) - @@unique([provider, providerId, userId]) psl=} ``` @@ -135,26 +108,11 @@ psl=} ```wasp title="main.wasp" // ... -// highlight-next-line -// 4. Define the User entity +// 3. Define the User entity // highlight-next-line entity User {=psl id Int @id @default(autoincrement()) // ... - externalAuthAssociations SocialLogin[] -psl=} - -// highlight-next-line -// 5. Define the SocialLogin entity -// highlight-next-line -entity SocialLogin {=psl - id Int @id @default(autoincrement()) - provider String - providerId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int - createdAt DateTime @default(now()) - @@unique([provider, providerId, userId]) psl=} ``` @@ -271,7 +229,7 @@ We'll define the React components for these pages in the `client/pages/auth.{jsx ### 6. Create the Client Pages :::info -We are using [Tailwind CSS](https://tailwindcss.com/) to style the pages. Read more about how to add it [here](/docs/project/css-frameworks). +We are using [Tailwind CSS](https://tailwindcss.com/) to style the pages. Read more about how to add it [here](../../project/css-frameworks). ::: Let's now create a `auth.{jsx,tsx}` file in the `client/pages`. @@ -337,7 +295,7 @@ export function Layout({ children }: { children: React.ReactNode }) { :::info Auth UI -Our pages use an automatically-generated Auth UI component. Read more about Auth UI components [here](/docs/auth/ui). +Our pages use an automatically-generated Auth UI component. Read more about Auth UI components [here](../../auth/ui). ::: ### Conclusion @@ -347,7 +305,7 @@ Yay, we've successfully set up Google Auth! 🎉 ![Google Auth](/img/auth/google.png) Running `wasp db migrate-dev` and `wasp start` should now give you a working app with authentication. -To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on [using auth](/docs/auth/overview). +To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on [using auth](../../auth/overview). ## Default Behaviour @@ -356,7 +314,7 @@ Add `google: {}` to the `auth.methods` dictionary to use it with default setting -```wasp title=main.wasp {10} +```wasp title=main.wasp app myApp { wasp: { version: "^0.11.0" @@ -364,8 +322,8 @@ app myApp { title: "My App", auth: { userEntity: User, - externalAuthEntity: SocialLogin, methods: { + // highlight-next-line google: {} }, onAuthFailedRedirectTo: "/login" @@ -376,7 +334,7 @@ app myApp { -```wasp title=main.wasp {10} +```wasp title=main.wasp app myApp { wasp: { version: "^0.11.0" @@ -384,8 +342,8 @@ app myApp { title: "My App", auth: { userEntity: User, - externalAuthEntity: SocialLogin, methods: { + // highlight-next-line google: {} }, onAuthFailedRedirectTo: "/login" @@ -409,7 +367,7 @@ app myApp { -```wasp title="main.wasp" {11-12,22} +```wasp title="main.wasp" app myApp { wasp: { version: "^0.11.0" @@ -417,11 +375,12 @@ app myApp { title: "My App", auth: { userEntity: User, - externalAuthEntity: SocialLogin, methods: { google: { + // highlight-next-line configFn: import { getConfig } from "@server/auth/google.js", - getUserFieldsFn: import { getUserFields } from "@server/auth/google.js" + // highlight-next-line + userSignupFields: import { userSignupFields } from "@server/auth/google.js" } }, onAuthFailedRedirectTo: "/login" @@ -432,19 +391,15 @@ entity User {=psl id Int @id @default(autoincrement()) username String @unique displayName String - externalAuthAssociations SocialLogin[] psl=} // ... ``` ```js title=src/server/auth/google.js -import { generateAvailableDictionaryUsername } from '@wasp/core/auth.js' - -export const getUserFields = async (_context, args) => { - const username = await generateAvailableDictionaryUsername() - const displayName = args.profile.displayName - return { username, displayName } +export const userSignupFields = { + username: () => "hardcoded-username", + displayName: (data) => data.profile.displayName, } export function getConfig() { @@ -459,7 +414,7 @@ export function getConfig() { -```wasp title="main.wasp" {11-12,22} +```wasp title="main.wasp" app myApp { wasp: { version: "^0.11.0" @@ -467,11 +422,12 @@ app myApp { title: "My App", auth: { userEntity: User, - externalAuthEntity: SocialLogin, methods: { google: { + // highlight-next-line configFn: import { getConfig } from "@server/auth/google.js", - getUserFieldsFn: import { getUserFields } from "@server/auth/google.js" + // highlight-next-line + userSignupFields: import { userSignupFields } from "@server/auth/google.js" } }, onAuthFailedRedirectTo: "/login" @@ -482,21 +438,18 @@ entity User {=psl id Int @id @default(autoincrement()) username String @unique displayName String - externalAuthAssociations SocialLogin[] psl=} // ... ``` ```ts title=src/server/auth/google.ts -import type { GetUserFieldsFn } from '@wasp/types' -import { generateAvailableDictionaryUsername } from '@wasp/core/auth.js' +import { defineUserSignupFields } from '@wasp/auth/index.js' -export const getUserFields: GetUserFieldsFn = async (_context, args) => { - const username = await generateAvailableDictionaryUsername() - const displayName = args.profile.displayName - return { username, displayName } -} +export const userSignupFields = defineUserSignupFields({ + username: () => "hardcoded-username", + displayName: (data) => data.profile.displayName, +}) export function getConfig() { return { @@ -523,7 +476,7 @@ export function getConfig() { -```wasp title="main.wasp" {11-12} +```wasp title="main.wasp" app myApp { wasp: { version: "^0.11.0" @@ -531,11 +484,12 @@ app myApp { title: "My App", auth: { userEntity: User, - externalAuthEntity: SocialLogin, methods: { google: { + // highlight-next-line configFn: import { getConfig } from "@server/auth/google.js", - getUserFieldsFn: import { getUserFields } from "@server/auth/google.js" + // highlight-next-line + userSignupFields: import { userSignupFields } from "@server/auth/google.js" } }, onAuthFailedRedirectTo: "/login" @@ -546,7 +500,7 @@ app myApp { -```wasp title="main.wasp" {11-12} +```wasp title="main.wasp" app myApp { wasp: { version: "^0.11.0" @@ -554,11 +508,12 @@ app myApp { title: "My App", auth: { userEntity: User, - externalAuthEntity: SocialLogin, methods: { google: { + // highlight-next-line configFn: import { getConfig } from "@server/auth/google.js", - getUserFieldsFn: import { getUserFields } from "@server/auth/google.js" + // highlight-next-line + userSignupFields: import { userSignupFields } from "@server/auth/google.js" } }, onAuthFailedRedirectTo: "/login" @@ -604,48 +559,6 @@ The `google` dict has the following properties: -- #### `getUserFieldsFn: ServerImport` - - This function must return the user fields to use when creating a new user. - - The `context` contains the `User` entity, and the `args` object contains Google profile information. - You can do whatever you want with this information (e.g., generate a username). - - Here is how to generate a username based on the Google display name: - - - - ```js title=src/server/auth/google.js - import { generateAvailableUsername } from '@wasp/core/auth.js' - - export const getUserFields = async (_context, args) => { - const username = await generateAvailableUsername( - args.profile.displayName.split(' '), - { separator: '.' } - ) - return { username } - } - ``` - - - - - ```ts title=src/server/auth/google.ts - import type { GetUserFieldsFn } from '@wasp/types' - import { generateAvailableUsername } from '@wasp/core/auth.js' - - export const getUserFields: GetUserFieldsFn = async (_context, args) => { - const username = await generateAvailableUsername( - args.profile.displayName.split(' '), - { separator: '.' } - ) - return { username } - } - ``` - - - - - +- #### `userSignupFields: ServerImport` - + \ No newline at end of file diff --git a/web/docs/auth/social-auth/overview.md b/web/docs/auth/social-auth/overview.md index 9a89101e5b..c69a29ee11 100644 --- a/web/docs/auth/social-auth/overview.md +++ b/web/docs/auth/social-auth/overview.md @@ -23,15 +23,12 @@ Wasp currently supports the following social login providers: -## Social Login Entity +## User Entity Wasp requires you to declare a `userEntity` for all `auth` methods (social or otherwise). This field tells Wasp which Entity represents the user. -Additionally, when using `auth` methods that rely on external providers(e.g., _Google_), you must also declare an `externalAuthEntity`. -This tells Wasp which Entity represents the user's link with the social provider. - -Both fields fall under `app.auth`. Here's what the full setup looks like: +Here's what the full setup looks like: @@ -45,8 +42,6 @@ app myApp { auth: { // highlight-next-line userEntity: User, - // highlight-next-line - externalAuthEntity: SocialLogin, methods: { google: {} }, @@ -58,18 +53,6 @@ app myApp { entity User {=psl id Int @id @default(autoincrement()) //... - externalAuthAssociations SocialLogin[] -psl=} - -// highlight-next-line -entity SocialLogin {=psl - id Int @id @default(autoincrement()) - provider String - providerId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int - createdAt DateTime @default(now()) - @@unique([provider, providerId, userId]) psl=} ``` @@ -85,8 +68,6 @@ app myApp { auth: { // highlight-next-line userEntity: User, - // highlight-next-line - externalAuthEntity: SocialLogin, methods: { google: {} }, @@ -98,18 +79,6 @@ app myApp { entity User {=psl id Int @id @default(autoincrement()) //... - externalAuthAssociations SocialLogin[] -psl=} - -// highlight-next-line -entity SocialLogin {=psl - id Int @id @default(autoincrement()) - provider String - providerId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int - createdAt DateTime @default(now()) - @@unique([provider, providerId, userId]) psl=} ``` @@ -122,23 +91,23 @@ To learn more about what the fields on these entities represent, look at the [AP -:::note -Wasp uses the same `externalAuthEntity` for all social login providers (e.g. both GitHub and Google use the same entity). -::: - ## Default Behavior ## Overrides -Wasp lets you override the default behavior. You can create custom setups, such as allowing users to define a custom username rather instead of getting a randomly generated one. +By default, Wasp doesn't store any information it receives from the social login provider. It only stores the user's ID specific to the provider. + +If you wish to store more information about the user, you can override the default behavior. You can do this by defining the `userSignupFields` and `configFn` fields in `main.wasp` for each provider. + +You can create custom signup setups, such as allowing users to define a custom username after they sign up with a social provider. -### Allowing User to Set Their Username +### Example: Allowing User to Set Their Username If you want to modify the signup flow (e.g., let users choose their own usernames), you will need to go through three steps: -1. The first step is adding a `isSignupComplete` property to your `User` Entity. This field will signals whether the user has completed the signup process. +1. The first step is adding a `isSignupComplete` property to your `User` Entity. This field will signal whether the user has completed the signup process. 2. The second step is overriding the default signup behavior. 3. The third step is implementing the rest of your signup flow and redirecting users where appropriate. @@ -155,7 +124,6 @@ entity User {=psl username String? @unique // highlight-next-line isSignupComplete Boolean @default(false) - externalAuthAssociations SocialLogin[] psl=} ``` @@ -168,7 +136,6 @@ entity User {=psl username String? @unique // highlight-next-line isSignupComplete Boolean @default(false) - externalAuthAssociations SocialLogin[] psl=} ``` @@ -177,7 +144,7 @@ psl=} #### 2. Overriding the Default Behavior -Declare an import under `app.auth.methods.google.getUserFieldsFn` (the example assumes you're using Google): +Declare an import under `app.auth.methods.google.userSignupFields` (the example assumes you're using Google): @@ -190,11 +157,10 @@ app myApp { title: "My App", auth: { userEntity: User, - externalAuthEntity: SocialLogin, methods: { google: { // highlight-next-line - getUserFieldsFn: import { getUserFields } from "@server/auth/google.js" + userSignupFields: import { userSignupFields } from "@server/auth/google.js" } }, onAuthFailedRedirectTo: "/login" @@ -207,10 +173,8 @@ app myApp { And implement the imported function. ```js title=src/server/auth/google.js -export const getUserFields = async (_context, _args) => { - return { - isSignupComplete: false, - } +export const userSignupFields = { + isSignupComplete: () => false, } ``` @@ -225,11 +189,10 @@ app myApp { title: "My App", auth: { userEntity: User, - externalAuthEntity: SocialLogin, methods: { google: { // highlight-next-line - getUserFieldsFn: import { getUserFields } from "@server/auth/google.js" + userSignupFields: import { userSignupFields } from "@server/auth/google.js" } }, onAuthFailedRedirectTo: "/login" @@ -242,13 +205,11 @@ app myApp { And implement the imported function: ```ts title=src/server/auth/google.ts -import { GetUserFieldsFn } from '@wasp/types' +import { defineUserSignupFields } from '@wasp/auth/index.js' -export const getUserFields: GetUserFieldsFn = async (_context, _args) => { - return { - isSignupComplete: false, - } -} +export const userSignupFields = defineUserSignupFields({ + isSignupComplete: () => false, +}) ``` @@ -258,7 +219,7 @@ export const getUserFields: GetUserFieldsFn = async (_context, _args) => { #### 3. Showing the Correct State on the Client -You can query the user's `isSignupComplete` flag on the client with the [`useAuth()`](/docs/auth/overview) hook. +You can query the user's `isSignupComplete` flag on the client with the [`useAuth()`](../../auth/overview) hook. Depending on the flag's value, you can redirect users to the appropriate signup step. For example: @@ -310,14 +271,14 @@ The same general principle applies to more complex signup procedures, just chang ### Using the User's Provider Account Details Account details are provider-specific. -Each provider has their own rules for defining the `getUserFieldsFn` and `configFn` functions: +Each provider has their own rules for defining the `userSignupFields` and `configFn` fields: ## UI Helpers :::tip Use Auth UI -[Auth UI](/docs/auth/ui) is a common name for all high-level auth forms that come with Wasp. +[Auth UI](../../auth/ui) is a common name for all high-level auth forms that come with Wasp. These include fully functional auto-generated login and signup forms with working social login buttons. If you're looking for the fastest way to get your auth up and running, that's where you should look. @@ -391,107 +352,8 @@ If you need even more customization, you can create your custom components using For more information on: - Allowed fields in `app.auth` -- `getUserFields` and `configFn` functions +- `userSignupFields` and `configFn` functions Check the provider-specific API References: - - -### The `externalAuthEntity` and Its Fields - -Using social login providers requires you to define _an External Auth Entity_ and declare it with the `app.auth.externalAuthEntity` field. -This Entity holds the data relevant to the social provider. -All social providers share the same Entity. - - - - -```wasp title=main.wasp {4-10} -// ... - -entity SocialLogin {=psl - id Int @id @default(autoincrement()) - provider String - providerId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int - createdAt DateTime @default(now()) - @@unique([provider, providerId, userId]) -psl=} - -// ... -``` - - - - -```wasp title=main.wasp {4-10} -// ... - -entity SocialLogin {=psl - id Int @id @default(autoincrement()) - provider String - providerId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int - createdAt DateTime @default(now()) - @@unique([provider, providerId, userId]) -psl=} - -// ... -``` - - - - -:::info -You don't need to know these details, you can just copy and paste the entity definition above and you are good to go. -::: - -The Entity acting as `app.auth.externalAuthEntity` must include the following fields: - -- `provider` - The provider's name (e.g. `google`, `github`, etc.). -- `providerId` - The user's ID on the provider's platform. -- `userId` - The user's ID on your platform (this references the `id` field from the Entity acting as `app.auth.userEntity`). -- `user` - A relation to the `userEntity` (see [the `userEntity` section](#expected-fields-on-the-userentity)) for more details. -- `createdAt` - A timestamp of when the association was created. -- `@@unique([provider, providerId, userId])` - A unique constraint on the combination of `provider`, `providerId` and `userId`. - -### Expected Fields on the `userEntity` - -Using Social login providers requires you to add one extra field to the Entity acting as `app.auth.userEntity`: - -- `externalAuthAssociations` - A relation to the `externalAuthEntity` (see [the `externalAuthEntity` section](#the-externalauthentity-and-its-fields) for more details). - - - - -```wasp title=main.wasp {6} -// ... - -entity User {=psl - id Int @id @default(autoincrement()) - //... - externalAuthAssociations SocialLogin[] -psl=} - -// ... -``` - - - - -```wasp title=main.wasp {6} -// ... - -entity User {=psl - id Int @id @default(autoincrement()) - //... - externalAuthAssociations SocialLogin[] -psl=} - -// ... -``` - - - + \ No newline at end of file diff --git a/web/docs/auth/ui.md b/web/docs/auth/ui.md index 2070a9ed3f..bb54cff98d 100644 --- a/web/docs/auth/ui.md +++ b/web/docs/auth/ui.md @@ -218,7 +218,7 @@ export function SignupPage() { It will automatically show the correct authentication providers based on your `main.wasp` file. -Read more about customizing the signup process like adding additional fields or extra UI in the [Using Auth](/docs/auth/overview#customizing-the-signup-process) section. +Read more about customizing the signup process like adding additional fields or extra UI in the [Auth Overview](../auth/overview#customizing-the-signup-process) section. ### Forgot Password Form diff --git a/web/docs/auth/username-and-pass.md b/web/docs/auth/username-and-pass.md index 4d7339293d..3d9efd8764 100644 --- a/web/docs/auth/username-and-pass.md +++ b/web/docs/auth/username-and-pass.md @@ -2,7 +2,12 @@ title: Username & Password --- -import { Required } from '@site/src/components/Required'; +import { Required } from '@site/src/components/Tag'; +import MultipleIdentitiesWarning from './\_multiple-identities-warning.md'; +import ReadMoreAboutAuthEntities from './\_read-more-about-auth-entities.md'; +import GetUsername from './entities/\_get-username.md'; +import UserSignupFieldsExplainer from './\_user-signup-fields-explainer.md'; +import UserFieldsExplainer from './\_user-fields.md'; Wasp supports username & password authentication out of the box with login and signup flows. It provides you with the server-side implementation and the UI components for the client-side. @@ -10,8 +15,8 @@ Wasp supports username & password authentication out of the box with login and s To set up username authentication we need to: 1. Enable username authentication in the Wasp file -1. Add the user entity -1. Add the routes and pages +1. Add the `User` entity +1. Add the auth routes and pages 1. Use Auth UI components in our pages Structure of the `main.wasp` file we will end up with: @@ -80,17 +85,16 @@ Read more about the `usernameAndPassword` auth method options [here](#fields-in- ### 2. Add the User Entity -When username authentication is enabled, Wasp expects certain fields in your `userEntity`. Let's add these fields to our `main.wasp` file: +The `User` entity can be as simple as including only the `id` field: -```wasp title="main.wasp" {4-5} +```wasp title="main.wasp" // 3. Define the user entity entity User {=psl + // highlight-next-line id Int @id @default(autoincrement()) - username String @unique - password String // Add your own fields below // ... psl=} @@ -98,12 +102,11 @@ psl=} -```wasp title="main.wasp" {4-5} +```wasp title="main.wasp" // 3. Define the user entity entity User {=psl + // highlight-next-line id Int @id @default(autoincrement()) - username String @unique - password String // Add your own fields below // ... psl=} @@ -111,7 +114,7 @@ psl=} -Read more about the `userEntity` fields [here](#userentity-fields). + ### 3. Add the Routes and Pages @@ -157,7 +160,7 @@ We'll define the React components for these pages in the `client/pages/auth.{jsx ### 4. Create the Client Pages :::info -We are using [Tailwind CSS](https://tailwindcss.com/) to style the pages. Read more about how to add it [here](/docs/project/css-frameworks). +We are using [Tailwind CSS](https://tailwindcss.com/) to style the pages. Read more about how to add it [here](../project/css-frameworks). ::: Let's create a `auth.{jsx,tsx}` file in the `client/pages` folder and add the following to it: @@ -255,23 +258,25 @@ export function Layout({ children }: { children: React.ReactNode }) { -We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components [here](/docs/auth/ui). +We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components [here](../auth/ui). ### Conclusion That's it! We have set up username authentication in our app. 🎉 -Running `wasp db migrate-dev` and then `wasp start` should give you a working app with username authentication. If you want to put some of the pages behind authentication, read the [using auth docs](/docs/auth/overview). +Running `wasp db migrate-dev` and then `wasp start` should give you a working app with username authentication. If you want to put some of the pages behind authentication, read the [auth overview docs](../auth/overview). + + ## Customizing the Auth Flow The login and signup flows are pretty standard: they allow the user to sign up and then log in with their username and password. The signup flow validates the username and password and then creates a new user entity in the database. -Read more about the default username and password validation rules in the [using auth docs](/docs/auth/overview#default-validations). +Read more about the default username and password validation rules in the [auth overview docs](../auth/overview#default-validations). If you require more control in your authentication flow, you can achieve that in the following ways: 1. Create your UI and use `signup` and `login` actions. -1. Create your custom sign-up action which use the Prisma client, along with your custom code. +1. Create your custom sign-up action which uses the lower-level API, along with your custom code. ### 1. Using the `signup` and `login` actions @@ -375,7 +380,7 @@ It takes one argument: - `password: string` :::info - By default, Wasp will only save the `username` and `password` fields. If you want to add extra fields to your signup process, read about [defining extra signup fields](/docs/auth/overview#customizing-the-signup-process). + By default, Wasp will only save the `username` and `password` fields. If you want to add extra fields to your signup process, read about [defining extra signup fields](../auth/overview#customizing-the-signup-process). ::: You can use it like this: @@ -471,7 +476,6 @@ The code of your custom sign-up action can look like this: action customSignup { fn: import { signup } from "@server/auth/signup.js", - entities: [User] } ``` @@ -482,19 +486,29 @@ import { ensureValidPassword, ensureValidUsername, } from '@wasp/auth/validation.js' +import { + createProviderId, + sanitizeAndSerializeProviderData, + createUser, +} from '@wasp/auth/utils.js' -export const signup = async (args, { entities: { User } }) => { +export const signup = async (args, _context) => { ensureValidUsername(args) ensurePasswordIsPresent(args) ensureValidPassword(args) try { - await User.create({ - data: { - username: args.username, - password: args.password, // Password is hashed automatically by Wasp - }, + const providerId = createProviderId('username', args.username) + const providerData = await sanitizeAndSerializeProviderData({ + hashedPassword: args.password, }) + + await createUser( + providerId, + providerData, + // Any additional data you want to store on the User entity + {}, + ) } catch (e) { return { success: false, @@ -519,7 +533,6 @@ export const signup = async (args, { entities: { User } }) => { action customSignup { fn: import { signup } from "@server/auth/signup.js", - entities: [User] } ``` @@ -529,6 +542,11 @@ import { ensureValidPassword, ensureValidUsername, } from '@wasp/auth/validation.js' +import { + createProviderId, + sanitizeAndSerializeProviderData, + createUser, +} from '@wasp/auth/utils.js' import type { CustomSignup } from '@wasp/actions/types' type CustomSignupInput = { @@ -543,19 +561,24 @@ type CustomSignupOutput = { export const signup: CustomSignup< CustomSignupInput, CustomSignupOutput -> = async (args, { entities: { User } }) => { +> = async (args, _context) => { ensureValidUsername(args) ensurePasswordIsPresent(args) ensureValidPassword(args) try { - await User.create({ - data: { - username: args.username, - password: args.password, // Password is hashed automatically by Wasp - }, + const providerId = createProviderId('username', args.username) + const providerData = await sanitizeAndSerializeProviderData<'username'>({ + hashedPassword: args.password, }) - } catch (e: any) { + + await createUser( + providerId, + providerData, + // Any additional data you want to store on the User entity + {}, + ) + } catch (e) { return { success: false, message: e.message, @@ -580,7 +603,7 @@ We suggest using the built-in field validators for your authentication flow. You - `ensureValidUsername(args)` - Checks if the username is valid and throws an error if it's not. Read more about the validation rules [here](/docs/auth/overview#default-validations). + Checks if the username is valid and throws an error if it's not. Read more about the validation rules [here](../auth/overview#default-validations). #### Password @@ -590,11 +613,19 @@ We suggest using the built-in field validators for your authentication flow. You - `ensureValidPassword(args)` - Checks if the password is valid and throws an error if it's not. Read more about the validation rules [here](/docs/auth/overview#default-validations). + Checks if the password is valid and throws an error if it's not. Read more about the validation rules [here](../auth/overview#default-validations). ## Using Auth -To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the [using auth docs](/docs/auth/overview). +To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the [auth overview docs](../auth/overview). + +### `getUsername` + +If you are looking to access the user's username in your code, you can do that by accessing the info about the user that is stored in the `user.auth.identities` array. + +To make things a bit easier for you, Wasp offers the `getUsername` helper. + + ## API Reference @@ -618,11 +649,8 @@ app myApp { } } -// Wasp requires the `userEntity` to have at least the following fields entity User {=psl id Int @id @default(autoincrement()) - username String @unique - password String psl=} ``` @@ -643,20 +671,14 @@ app myApp { } } -// Wasp requires the `userEntity` to have at least the following fields entity User {=psl id Int @id @default(autoincrement()) - username String @unique - password String psl=} ``` -Username & password auth requires that `userEntity` specified in `auth` contains: - -- `username` field of type `String` -- `password` field of type `String` + ### Fields in the `usernameAndPassword` dict @@ -672,7 +694,9 @@ app myApp { auth: { userEntity: User, methods: { - usernameAndPassword: {}, + usernameAndPassword: { + userSignupFields: import { userSignupFields } from "@server/auth/email.js", + }, }, onAuthFailedRedirectTo: "/login" } @@ -691,7 +715,9 @@ app myApp { auth: { userEntity: User, methods: { - usernameAndPassword: {}, + usernameAndPassword: { + userSignupFields: import { userSignupFields } from "@server/auth/email.js", + }, }, onAuthFailedRedirectTo: "/login" } @@ -701,8 +727,6 @@ app myApp { -:::info -`usernameAndPassword` dict doesn't have any options at the moment. -::: +#### `userSignupFields: ServerImport` -You can read about the rest of the `auth` options in the [using auth](/docs/auth/overview) section of the docs. + \ No newline at end of file diff --git a/web/docs/contributing.md b/web/docs/contributing.md index 4dc0a25c56..2325a17861 100644 --- a/web/docs/contributing.md +++ b/web/docs/contributing.md @@ -4,7 +4,7 @@ sidebar_label: Contributing slug: /contributing --- -import DiscordLink from '../blog/components/DiscordLink'; +import DiscordLink from '@site/blog/components/DiscordLink'; Any way you want to contribute is a good way, and we'd be happy to meet you! A single entry point for all contributors is the [CONTRIBUTING.md](https://github.com/wasp-lang/wasp/blob/main/CONTRIBUTING.md) file in our Github repo. All the requirements and instructions are there, so please check [CONTRIBUTING.md](https://github.com/wasp-lang/wasp/blob/main/CONTRIBUTING.md) for more details. @@ -16,4 +16,4 @@ Some side notes to make your journey easier: 3. If there's something you'd like to bring to our attention, go to [docs GitHub repo](https://github.com/wasp-lang/wasp) and make an issue/PR! -Happy hacking! \ No newline at end of file +Happy hacking! diff --git a/web/docs/data-model/backends.md b/web/docs/data-model/backends.md index acfb036a2f..672f2998cc 100644 --- a/web/docs/data-model/backends.md +++ b/web/docs/data-model/backends.md @@ -2,9 +2,9 @@ title: Databases --- -import { Required } from '@site/src/components/Required' +import { Required } from '@site/src/components/Tag' -[Entities](/docs/data-model/entities.md), [Operations](/docs/data-model/operations/overview) and [Automatic CRUD](/docs/data-model/crud.md) together make a high-level interface for working with your app's data. Still, all that data has to live somewhere, so let's see how Wasp deals with databases. +[Entities](../data-model/entities.md), [Operations](../data-model/operations/overview) and [Automatic CRUD](../data-model/crud.md) together make a high-level interface for working with your app's data. Still, all that data has to live somewhere, so let's see how Wasp deals with databases. ## Supported Database Backends @@ -77,7 +77,7 @@ Also, make sure that: If you want to spin up your own dev database (or connect to an external one), you can tell Wasp about it using the `DATABASE_URL` environment variable. Wasp will use the value of `DATABASE_URL` as a connection string. -The easiest way to set the necessary `DATABASE_URL` environment variable is by adding it to the [.env.server](/docs/project/env-vars) file in the root dir of your Wasp project (if that file doesn't yet exist, create it). +The easiest way to set the necessary `DATABASE_URL` environment variable is by adding it to the [.env.server](../project/env-vars) file in the root dir of your Wasp project (if that file doesn't yet exist, create it). Alternatively, you can set it inline when running `wasp` (this applies to all environment variables): diff --git a/web/docs/data-model/crud.md b/web/docs/data-model/crud.md index 41c0b61c19..cc83ef5a92 100644 --- a/web/docs/data-model/crud.md +++ b/web/docs/data-model/crud.md @@ -2,15 +2,15 @@ title: Automatic CRUD --- -import { Required } from '@site/src/components/Required'; +import { Required } from '@site/src/components/Tag'; import { ShowForTs } from '@site/src/components/TsJsHelpers'; -import ImgWithCaption from '../../blog/components/ImgWithCaption' +import ImgWithCaption from '@site/blog/components/ImgWithCaption' If you have a lot of experience writing full-stack apps, you probably ended up doing some of the same things many times: listing data, adding data, editing it, and deleting it. Wasp makes handling these boring bits easy by offering a higher-level concept called Automatic CRUD. -With a single declaration, you can tell Wasp to automatically generate server-side logic (i.e., Queries and Actions) for creating, reading, updating and deleting [Entities](/docs/data-model/entities). As you update definitions for your Entities, Wasp automatically regenerates the backend logic. +With a single declaration, you can tell Wasp to automatically generate server-side logic (i.e., Queries and Actions) for creating, reading, updating and deleting [Entities](../data-model/entities). As you update definitions for your Entities, Wasp automatically regenerates the backend logic. :::caution Early preview This feature is currently in early preview and we are actively working on it. Read more about [our plans](#future-of-crud-operations-in-wasp) for CRUD operations. @@ -62,7 +62,7 @@ Keep reading for an example of Automatic CRUD in action, or skip ahead for the [ ## Example: A Simple TODO App -Let's create a full-app example that uses automatic CRUD. We'll stick to using the `Task` entity from the previous example, but we'll add a `User` entity and enable [username and password](/docs/auth/username-and-pass) based auth. +Let's create a full-app example that uses automatic CRUD. We'll stick to using the `Task` entity from the previous example, but we'll add a `User` entity and enable [username and password](../auth/username-and-pass) based auth. @@ -89,8 +89,6 @@ app tasksCrudApp { entity User {=psl id Int @id @default(autoincrement()) - username String @unique - password String tasks Task[] psl=} @@ -328,7 +326,7 @@ export const MainPage = () => { -And here are the login and signup pages, where we are using Wasp's [Auth UI](/docs/auth/ui) components: +And here are the login and signup pages, where we are using Wasp's [Auth UI](../auth/ui) components: @@ -692,7 +690,7 @@ export const getAllOverride: GetAllQuery = async ( -For a usage example, check the [example guide](/docs/data-model/crud#adding-crud-to-the-task-entity-). +For a usage example, check the [example guide](../data-model/crud#adding-crud-to-the-task-entity-). #### Using the CRUD operations in client code @@ -742,7 +740,7 @@ const deleteAction = Tasks.delete.useAction() -All CRUD operations are implemented with [Queries and Actions](/docs/data-model/operations/overview) under the hood, which means they come with all the features you'd expect (e.g., automatic SuperJSON serialization, full-stack type safety when using TypeScript) +All CRUD operations are implemented with [Queries and Actions](../data-model/operations/overview) under the hood, which means they come with all the features you'd expect (e.g., automatic SuperJSON serialization, full-stack type safety when using TypeScript) --- diff --git a/web/docs/data-model/entities.md b/web/docs/data-model/entities.md index 78a15c8bac..23843da178 100644 --- a/web/docs/data-model/entities.md +++ b/web/docs/data-model/entities.md @@ -62,11 +62,11 @@ Let's see how you can define and work with Wasp Entities: 1. Create/update some Entities in your `.wasp` file. 2. Run `wasp db migrate-dev`. This command syncs the database model with the Entity definitions in your `.wasp` file. It does this by creating migration scripts. 3. Migration scripts are automatically placed in the `migrations/` folder. Make sure to commit this folder into version control. -4. Use Wasp's JavasScript API to work with the database when implementing Operations (we'll cover this in detail when we talk about [operations](/docs/data-model/operations/overview)). +4. Use Wasp's JavasScript API to work with the database when implementing Operations (we'll cover this in detail when we talk about [operations](../data-model/operations/overview)). #### Using Entities in Operations -Most of the time, you will be working with Entities within the context of [Operations (Queries & Actions)](/docs/data-model/operations/overview). We'll see how that's done on the next page. +Most of the time, you will be working with Entities within the context of [Operations (Queries & Actions)](../data-model/operations/overview). We'll see how that's done on the next page. #### Using Entities directly diff --git a/web/docs/data-model/operations/actions.md b/web/docs/data-model/operations/actions.md index 13f9cffe92..fb0b378fd4 100644 --- a/web/docs/data-model/operations/actions.md +++ b/web/docs/data-model/operations/actions.md @@ -2,13 +2,13 @@ title: Actions --- -import { Required } from '@site/src/components/Required'; +import { Required } from '@site/src/components/Tag'; import { ShowForTs } from '@site/src/components/TsJsHelpers'; import SuperjsonNote from './\_superjson-note.md'; We'll explain what Actions are and how to use them. If you're looking for a detailed API specification, skip ahead to the [API Reference](#api-reference). -Actions are quite similar to [Queries](/docs/data-model/operations/queries.md), but with a key distinction: Actions are designed to modify and add data, while Queries are solely for reading data. Examples of Actions include adding a comment to a blog post, liking a video, or updating a product's price. +Actions are quite similar to [Queries](../../data-model/operations/queries.md), but with a key distinction: Actions are designed to modify and add data, while Queries are solely for reading data. Examples of Actions include adding a comment to a blog post, liking a video, or updating a product's price. Actions and Queries work together to keep data caches up-to-date. @@ -373,7 +373,7 @@ export const createTask: CreateTask = async (args, context) => { ### Using Entities in Actions -In most cases, resources used in Actions will be [Entities](/docs/data-model/entities.md). +In most cases, resources used in Actions will be [Entities](../../data-model/entities.md). To use an Entity in your Action, add it to the `action` declaration in Wasp: @@ -626,7 +626,7 @@ Since both arguments are positional, you can name the parameters however you wan 2. `context` (type depends on the Action) - An additional context object **passed into the Action by Wasp**. This object contains user session information, as well as information about entities. Check the [section about using entities in Actions](#using-entities-in-actions) to see how to use the entities field on the `context` object, or the [auth section](/docs/auth/overview#using-the-contextuser-object) to see how to use the `user` object. + An additional context object **passed into the Action by Wasp**. This object contains user session information, as well as information about entities. Check the [section about using entities in Actions](#using-entities-in-actions) to see how to use the entities field on the `context` object, or the [auth section](../../auth/overview#using-the-contextuser-object) to see how to use the `user` object. @@ -704,7 +704,7 @@ In this case, the Action expects to receive an object with a `bar` field of type ### The `useAction` Hook and Optimistic Updates -Make sure you understand how [Queries](/docs/data-model/operations/queries.md) and [Cache Invalidation](#cache-invalidation) work before reading this chapter. +Make sure you understand how [Queries](../../data-model/operations/queries.md) and [Cache Invalidation](#cache-invalidation) work before reading this chapter. When using Actions in components, you can enhance them with the help of the `useAction` hook. This hook comes bundled with Wasp, and is used for decorating Wasp Actions. In other words, the hook returns a function whose API matches the original Action while also doing something extra under the hood (depending on how you configure it). diff --git a/web/docs/data-model/operations/overview.md b/web/docs/data-model/operations/overview.md index 42d933a5a1..2bf1fa44d5 100644 --- a/web/docs/data-model/operations/overview.md +++ b/web/docs/data-model/operations/overview.md @@ -2,11 +2,11 @@ title: Overview --- -import { Required } from '@site/src/components/Required'; +import { Required } from '@site/src/components/Tag'; While Entities enable help you define your app's data model and relationships, Operations are all about working with this data. -There are two kinds of Operations: [Queries](/docs/data-model/operations/queries.md) and [Actions](/docs/data-model/operations/actions.md). As their names suggest, +There are two kinds of Operations: [Queries](../../data-model/operations/queries.md) and [Actions](../../data-model/operations/actions.md). As their names suggest, Queries are meant for reading data, and Actions are meant for changing it (either by updating existing entries or creating new ones). Keep reading to find out all there is to know about Operations in Wasp. diff --git a/web/docs/data-model/operations/queries.md b/web/docs/data-model/operations/queries.md index 8d25f6aa61..7987d032d2 100644 --- a/web/docs/data-model/operations/queries.md +++ b/web/docs/data-model/operations/queries.md @@ -2,7 +2,7 @@ title: Queries --- -import { Required } from '@site/src/components/Required'; +import { Required } from '@site/src/components/Tag'; import { ShowForTs } from '@site/src/components/TsJsHelpers'; import SuperjsonNote from './\_superjson-note.md'; @@ -15,7 +15,7 @@ Fetching all comments on a blog post, a list of users that liked a video, inform Queries are fairly similar to Actions in terms of their API. Therefore, if you're already familiar with Actions, you might find reading the entire guide repetitive. -We instead recommend skipping ahead and only reading [the differences between Queries and Actions](/docs/data-model/operations/actions#differences-between-queries-and-actions), and consulting the [API Reference](#api-reference) as needed. +We instead recommend skipping ahead and only reading [the differences between Queries and Actions](../../data-model/operations/actions#differences-between-queries-and-actions), and consulting the [API Reference](#api-reference) as needed. ::: ## Working with Queries @@ -395,7 +395,7 @@ To prevent information leakage, the server won't forward these fields for any ot ### Using Entities in Queries -In most cases, resources used in Queries will be [Entities](/docs/data-model/entities.md). +In most cases, resources used in Queries will be [Entities](../../data-model/entities.md). To use an Entity in your Query, add it to the `query` declaration in Wasp: @@ -552,7 +552,7 @@ Since both arguments are positional, you can name the parameters however you wan 2. `context` (type depends on the Query) - An additional context object **passed into the Query by Wasp**. This object contains user session information, as well as information about entities. Check the [section about using entities in Queries](#using-entities-in-queries) to see how to use the entities field on the `context` object, or the [auth section](/docs/auth/overview#using-the-contextuser-object) to see how to use the `user` object. + An additional context object **passed into the Query by Wasp**. This object contains user session information, as well as information about entities. Check the [section about using entities in Queries](#using-entities-in-queries) to see how to use the entities field on the `context` object, or the [auth section](../../auth/overview#using-the-contextuser-object) to see how to use the `user` object. @@ -651,6 +651,6 @@ Wasp's `useQuery` hook accepts three arguments: [the default behavior](https://react-query.tanstack.com/guides/important-defaults) for this particular Query. If you want to change the global defaults, you can do - so in the [client setup function](/docs/project/client-config.md#overriding-default-behaviour-for-queries). + so in the [client setup function](../../project/client-config.md#overriding-default-behaviour-for-queries). For an example of usage, check [this section](#the-usequery-hook). diff --git a/web/docs/examples.md b/web/docs/examples.md deleted file mode 100644 index 8bd07753c1..0000000000 --- a/web/docs/examples.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Examples ---- - -import useBaseUrl from '@docusaurus/useBaseUrl'; - -We have a constantly growing collection of fully-functioning example apps, which you can use to learn more about Wasp's features. - -The full list of examples can be found [here](https://github.com/wasp-lang/wasp/tree/release/examples/). Here is a few of them: - -## Todo App - - **Features**: Auth ([username/password](language/features#authentication--authorization)), [Queries & Actions](language/features#queries-and-actions-aka-operations), [Entities](language/features#entity), [Routes](language/features#route) - - JS source code: [GitHub](https://github.com/wasp-lang/wasp/tree/release/examples/tutorials/TodoApp) - - TS source code: [GitHub](https://github.com/wasp-lang/wasp/tree/release/examples/todo-typescript) - - in-browser dev environment: [GitPod](https://gitpod.io/#https://github.com/wasp-lang/gitpod-template) - -## Waspello (Trello Clone) - - **Features**: Auth ([Google](language/features#social-login-providers-oauth-20), [username/password](language/features#authentication--authorization)), [Optimistic Updates](language/features#the-useaction-hook), [Tailwind CSS integration](/docs/project/css-frameworks) - - Source code: [GitHub](https://github.com/wasp-lang/wasp/tree/main/examples/waspello) - - Hosted at [https://waspello-demo.netlify.app](https://waspello-demo.netlify.app/login) -

    - -

    - -## Waspleau (Realtime Statistics Dashboard) - - **Features**: Cron [Jobs](language/features#jobs), [Server Setup](language/features#server-configuration) - - Source code: [GitHub](https://github.com/wasp-lang/wasp/tree/main/examples/waspleau) - - Hosted at [https://waspleau-app-client.fly.dev/](https://waspleau-app-client.fly.dev/) -

    - -

    \ No newline at end of file diff --git a/web/docs/general/cli.md b/web/docs/general/cli.md index cf0c94ed2b..d7b8db6d15 100644 --- a/web/docs/general/cli.md +++ b/web/docs/general/cli.md @@ -5,7 +5,7 @@ This guide provides an overview of the Wasp CLI commands, arguments, and options ## Overview -Once [installed](/docs/quick-start), you can use the wasp command from your command line. +Once [installed](../quick-start), you can use the wasp command from your command line. If you run the `wasp` command without any arguments, it will show you a list of available commands and their descriptions: @@ -101,13 +101,13 @@ Newsletter: https://wasp-lang.dev/#signup Deleted .wasp/ directory. ``` - - `wasp build` generates the complete web app code, which is ready for [deployment](/docs/advanced/deployment/overview). Use this command when you're deploying or ejecting. The generated code is stored in the `.wasp/build` folder. + - `wasp build` generates the complete web app code, which is ready for [deployment](../advanced/deployment/overview). Use this command when you're deploying or ejecting. The generated code is stored in the `.wasp/build` folder. - `wasp deploy` makes it easy to get your app hosted on the web. Currently, Wasp offers support for [Fly.io](https://fly.io). If you prefer a different hosting provider, feel free to let us know on Discord or submit a PR by updating [this TypeScript app](https://github.com/wasp-lang/wasp/tree/main/waspc/packages/deploy). - Read more about automatic deployment [here](/docs/advanced/deployment/cli). + Read more about automatic deployment [here](../advanced/deployment/cli). - `wasp telemetry` displays the status of [telemetry](https://wasp-lang.dev/docs/telemetry). diff --git a/web/docs/introduction/editor-setup.md b/web/docs/introduction/editor-setup.md index 15e49ef011..f025eccd32 100644 --- a/web/docs/introduction/editor-setup.md +++ b/web/docs/introduction/editor-setup.md @@ -4,7 +4,7 @@ slug: /editor-setup --- :::note -This page assumes you have already installed Wasp. If you do not have Wasp installed yet, check out the [Quick Start](/docs/quick-start) guide. +This page assumes you have already installed Wasp. If you do not have Wasp installed yet, check out the [Quick Start](./quick-start.md) guide. ::: Wasp comes with the Wasp language server, which gives supported editors powerful support and integration with the language. diff --git a/web/docs/introduction/introduction.md b/web/docs/introduction/introduction.md new file mode 100644 index 0000000000..29e2829f77 --- /dev/null +++ b/web/docs/introduction/introduction.md @@ -0,0 +1,204 @@ +--- +title: Introduction +slug: / +--- + +import ImgWithCaption from '@site/blog/components/ImgWithCaption' + +:::note +If you are looking for the installation instructions, check out the [Quick Start](./quick-start.md) section. +::: + +We will give a brief overview of what Wasp is, how it works on a high level and when to use it. + +## Wasp is a tool to build modern web applications + +It is an opinionated way of building **full-stack web applications**. It takes care of all three +major parts of a web application: **client** (front-end), **server** (back-end) and **database**. + +### Works well with your existing stack +Wasp is not trying to do everything at once but rather focuses on the complexity +which arises from connecting all the parts of the stack (client, server, database, deployment) together. + +Wasp is using **React**, **Node.js** and **Prisma** under the hood and relies on them to define web components and server queries and actions. + +### Wasp's secret sauce + +At the core is the Wasp compiler which takes the Wasp config and your Javascript code and outputs the client app, server app and deployment code. + + + + + +The cool thing about having a compiler that understands your code is that it can do a lot of things for you. + +Define your app in the Wasp config and get: +- login and signup with Auth UI components, +- full-stack type safety, +- e-mail sending, +- async processing jobs, +- React Query powered data fetching, +- security best practices, +- and more. + +You don't need to write any code for these features, Wasp will take care of it for you 🤯 And what's even better, Wasp also maintains the code for you, so you don't have to worry about keeping up with the latest security best practices. As Wasp updates, so does your app. + +## So what does the code look like? + +Let's say you want to build a web app that allows users to **create and share their favorite recipes**. + +Let's start with the main.wasp file: it is the central file of your app, where you describe the app from the high level. + +Let's give our app a title and let's immediately turn on the full-stack authentication via username and password: +```wasp title="main.wasp" +app RecipeApp { + title: "My Recipes", + wasp: { version: "^0.11.0" }, + auth: { + methods: { usernameAndPassword: {} }, + onAuthFailedRedirectTo: "/login", + userEntity: User + } +} +``` + +Let's then add the data models for your recipes. We will want to have Users and Users can own Recipes: + +```wasp title="main.wasp" +... + +entity User {=psl // Data models are defined using Prisma Schema Language. + id Int @id @default(autoincrement()) + recipes Recipe[] +psl=} + +entity Recipe {=psl + id Int @id @default(autoincrement()) + title String + description String? + userId Int + user User @relation(fields: [userId], references: [id]) +psl=} +``` + +Next, let's define how to do something with these data models! + +We do that by defining Operations, in this case a Query `getRecipes` and Action `addRecipe`, +which are in their essence a Node.js functions that execute on server and can, thanks to Wasp, very easily be called from the client. + +First, we define these Operations in our main.wasp file, so Wasp knows about them and can "beef them up": +```wasp title="main.wasp" +// Queries have automatic cache invalidation and are type-safe. +query getRecipes { + fn: import { getRecipes } from "@server/recipe.js", + entities: [Recipe], +} + +// Actions are type-safe and can be used to perform side-effects. +action addRecipe { + fn: import { addRecipe } from "@server/recipe.js", + entities: [Recipe], +} +``` + +... and then implement them in our Javascript (or TypeScript) code (we show just the query here, using TypeScript): + +```ts title="src/server/recipe.ts" +// Wasp generates types for you. +import type { GetRecipes } from "@wasp/queries/types"; +import type { Recipe } from "@wasp/entities"; + +export const getRecipes: GetRecipes<{}, Recipe[]> = async (_args, context) => { + return context.entities.Recipe.findMany( // Prisma query + { where: { user: { id: context.user.id } } } + ); +}; + +export const addRecipe ... +``` + +Now we can very easily use these in our React components! + +For the end, let's create a home page of our app. + +First we define it in main.wasp: +```wasp title="main.wasp" +... + +route HomeRoute { path: "/", to: HomePage } +page HomePage { + component: import { HomePage } from "@client/pages/HomePage", + authRequired: true // Will send user to /login if not authenticated. +} +``` + +and then implement it as a React component in JS/TS (that calls the Operations we previously defined): + +```tsx title="src/client/pages/HomePage.tsx" +import getRecipes from "@wasp/queries/getRecipes"; +import { useQuery } from "@wasp/queries"; +import type { User } from "@wasp/entities"; + +export function HomePage({ user }: { user: User }) { + // Due to full-stack type safety, `recipes` will be of type `Recipe[]` here. + const { data: recipes, isLoading } = useQuery(getRecipes); // Calling our query here! + + if (isLoading) { + return
    Loading...
    ; + } + + return ( +
    +

    Recipes

    +
      + {recipes ? recipes.map((recipe) => ( +
    • +
      {recipe.title}
      +
      {recipe.description}
      +
    • + )) : 'No recipes defined yet!'} +
    +
    + ); +} +``` + +And voila! We are listing all the recipes in our app 🎉 + +This was just a quick example to give you a taste of what Wasp is. For step by step tour through the most important Wasp features, check out the [Todo app tutorial](../tutorial/01-create.md). + +:::note +Above we skipped defining /login and /signup pages to keep the example a bit shorter, but those are very simple to do by using Wasp's Auth UI feature. +::: + +## When to use Wasp +Wasp is addressing the same core problems that typical web app frameworks are addressing, and it in big part [looks, swims and quacks](https://en.wikipedia.org/wiki/Duck_test) like a web app framework. + +### Best used for +- building full-stack web apps (like e.g. Airbnb or Asana) +- quickly starting a web app with industry best practices +- to be used alongside modern web dev stack (currently supported React and Node) + +### Avoid using Wasp for +- building static/presentational websites +- to be used as a no-code solution +- to be a solve-it-all tool in a single language + +## Wasp is a DSL + +:::note +You don't need to know what a DSL is to use Wasp, but if you are curious, you can read more about it below. +::: + +Wasp does not match typical expectations of a web app framework: it is not a set of libraries, it is instead a programming language that understands your code and can do a lot of things for you. + +Wasp is a programming language, but a specific kind: it is specialized for a single purpose: **building modern web applications**. We call such languages *DSL*s (Domain Specific Language). + +Other examples of *DSL*s that are often used today are e.g. *SQL* for databases and *HTML* for web page layouts. +The main advantage and reason why *DSL*s exist is that they need to do only one task (e.g. database queries) +so they can do it well and provide the best possible experience for the developer. + +The same idea stands behind Wasp - a language that will allow developers to **build modern web applications with 10x less code and less stack-specific knowledge**. diff --git a/web/docs/introduction/getting-started.md b/web/docs/introduction/quick-start.md similarity index 75% rename from web/docs/introduction/getting-started.md rename to web/docs/introduction/quick-start.md index be3c96872a..df6a3cdc42 100644 --- a/web/docs/introduction/getting-started.md +++ b/web/docs/introduction/quick-start.md @@ -45,8 +45,8 @@ Check [More Details](#more-details) section below if anything went wrong with th ### What next? - - [ ] 👉 **Check out the [Todo App tutorial](/docs/tutorial/create), which will take you through all the core features of Wasp!** 👈 - - [ ] [Setup your editor](/docs/editor-setup) for working with Wasp. + - [ ] 👉 **Check out the [Todo App tutorial](../tutorial/01-create.md), which will take you through all the core features of Wasp!** 👈 + - [ ] [Setup your editor](./editor-setup.md) for working with Wasp. - [ ] Join us on [Discord](https://discord.gg/rzdnErX)! Any feedback or questions you have, we are there for you. - [ ] Follow Wasp development by subscribing to our newsletter: https://wasp-lang.dev/#signup . We usually send 1 per month, and [Matija](https://github.com/matijaSos) does his best to unleash his creativity to make them engaging and fun to read :D! @@ -110,14 +110,22 @@ Open your terminal and run: curl -sSL https://get.wasp-lang.dev/installer.sh | sh ``` +:::note Running Wasp on Mac with Mx chip (arm64) +**Experiencing the 'Bad CPU type in executable' issue on a device with arm64 (Apple Silicon)?** +Given that the wasp binary is built for x86 and not for arm64 (Apple Silicon), you'll need to install [Rosetta on your Mac](https://support.apple.com/en-us/HT211861) if you are using a Mac with Mx (M1, M2, ...). Rosetta is a translation process that enables users to run applications designed for x86 on arm64 (Apple Silicon). To install Rosetta, run the following command in your terminal +```bash +softwareupdate --install-rosetta +``` +Once Rosetta is installed, you should be able to run Wasp without any issues. +::: + With Wasp for Windows, we are almost there: Wasp is successfully compiling and running on Windows but there is a bug or two stopping it from fully working. Check it out [here](https://github.com/wasp-lang/wasp/issues/48) if you are interested in helping. -In the meantime, the best way to start using Wasp on Windows is by using [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10). Once you set up Ubuntu on WSL, just follow Linux instructions for installing Wasp. If you need further help, reach out to us on [Discord](https://discord.gg/rzdnErX) - we have some community members using WSL that might be able to help you. - +In the meantime, the best way to start using Wasp on Windows is by using [WSL](https://learn.microsoft.com/en-us/windows/wsl/install). Once you set up Ubuntu on WSL, just follow Linux instructions for installing Wasp. You can refer to this [article](https://wasp-lang.dev/blog/2023/11/21/guide-windows-development-wasp-wsl) if you prefer a step by step guide to using Wasp in WSL environment. If you need further help, reach out to us on [Discord](https://discord.gg/rzdnErX) - we have some community members using WSL that might be able to help you. :::caution If you are using WSL2, make sure that your Wasp project is not on the Windows file system, but instead on the Linux file system. Otherwise, Wasp won't be able to detect file changes, due to the [issue in WSL2](https://github.com/microsoft/WSL/issues/4739). ::: diff --git a/web/docs/language/features.md b/web/docs/language/features.md deleted file mode 100644 index 43a2046cca..0000000000 --- a/web/docs/language/features.md +++ /dev/null @@ -1,2372 +0,0 @@ ---- -title: Features ---- - -import SendingEmailsInDevelopment from '../_sendingEmailsInDevelopment.md' -import OldDocsNote from '@site/docs/OldDocsNote' - - - -## App - -There can be only one declaration of `app` type per Wasp project. -It serves as a starting point and defines global properties of your app. - -```wasp -app todoApp { - wasp: { - version: "^0.6.0" - }, - title: "ToDo App", - head: [ // optional - "" - ] -} -``` - -### Fields - -#### `wasp: dict` (required) -Wasp compiler configuration. It is a dictionary with a single field: -- `version: string` (required) - version declares the compatible Wasp versions for the app. It should contain a valid [SemVer range](https://github.com/npm/node-semver#ranges). - -:::info -For now, the version field only supports caret ranges (i.e., `^x.y.z`). Support for the full specification will come in a future version of Wasp -::: - -#### `title: string` (required) -Title of your app. It will be displayed in the browser tab, next to the favicon. - -#### `head: [string]` (optional) -Head of your HTML Document. Your app's metadata (styles, links, etc) can be added here. - -#### `auth: dict` (optional) -Authentication and authorization configuration. -Check [`app.auth`](/docs/language/features#authentication--authorization) for more details. - -#### `client: dict` (optional) -Client configuration. -Check [`app.client`](/docs/language/features#client-configuration) for more details. - -#### `server: dict` (optional) -Server configuration. -Check [`app.server`](/docs/language/features#server-configuration) for more details. - -#### `db: dict` (optional) -Database configuration. -Check [`app.db`](/docs/language/features#database-configuration) for more details. - -#### `dependencies: [(string, string)]` (optional) -List of dependencies (external libraries). -Check [`app.dependencies`](/docs/language/features#dependencies) for more details. - -#### `emailSender: dict` (optional) -Email sender configuration. -Check [`app.emailSender`](/docs/language/features#email-sender) for more details. - -#### `webSocket: dict` (optional) -WebSocket configuration. -Check out the [`WebSocket guide`](/docs/guides/websockets) for more details. - -## Page - -`page` declaration is the top-level layout abstraction. Your app can have multiple pages. - -```wasp -page MainPage { - component: import Main from "@client/pages/Main", - authRequired: false // optional -} -``` - -Normally you will also want to associate `page` with a `route`, otherwise it won't be accessible in the app. - -### Fields - -#### `component: ClientImport` (required) -Import statement of the React element that implements the page component. - -#### `authRequired: bool` (optional) -Can be specified only if [`app.auth`](/docs/language/features#authentication--authorization) is defined. - -If set to `true`, only authenticated users will be able to access this page. Unauthenticated users will be redirected to a route defined by `onAuthFailedRedirectTo` property within `app.auth`. - -If `authRequired` is set to `true`, the React component of a page (specified by `component` property) will be provided `user` object as a prop. - -Check out this [section of our Todo app tutorial](/docs/tutorial/auth#update-the-main-page-to-require-auth) for an example of usage. - -## Route - -`route` declaration provides top-level routing functionality in Wasp. - -```wasp -route AboutRoute { path: "/about", to: AboutPage } -``` - -### Fields - -#### `path: string` (required) -URL path of the route. Route path can be parametrised and follows the same conventions as -[React Router](https://reactrouter.com/web/). - -#### `to: page` (required) -Name of the `page` to which the path will lead. -Referenced page must be defined somewhere in `.wasp` file. - -### Example - parametrised URL path -```wasp -route TaskRoute { path: "/task/:id", to: TaskPage } -``` -For details on URL path format check [React Router](https://reactrouter.com/web/) -documentation. - -### Accessing route parameters in a page component - -Since Wasp under the hood generates code with [React Router](https://reactrouter.com/web/), -the same rules apply when accessing URL params in your React components. Here is an example just to get you -started: - -```wasp title="todoApp.wasp" -// ... -route TaskRoute { path: "/task/:id", to: TaskPage } -page TaskPage { - component: import Task from "@client/pages/Task" -} -``` - -```jsx title="pages/Task.js" -import React from 'react' - -const Task = (props) => { - return ( -
    - I am showing a task with id: {props.match.params.id}. -
    - ) -} - -export default Task -``` -### Navigating between routes - -Navigation can be performed from the React code via `` component, also using the functionality of -[React Router](https://reactrouter.com/web/): - -```wasp title="todoApp.wasp" -// ... -route HomeRoute { path: "/home", to: HomePage } -page HomePage { - component: import Home from "@client/pages/Home" -} -``` - -```jsx title="src/client/pages/OtherPage.js" -import React from 'react' -import { Link } from "react-router-dom" - -const OtherPage = (props) => { - return ( - Go to homepage - ) -} -``` - -## Entity - -`entity` declaration represents a database model. -Wasp uses [Prisma](https://www.prisma.io/) to implement database functionality and currently provides only a thin layer above it. - -Each `Entity` declaration corresponds 1-to-1 to Prisma data model and is defined in a following way: - -```wasp -entity Task {=psl - id Int @id @default(autoincrement()) - description String - isDone Boolean @default(false) -psl=} -``` - -### `{=psl ... psl=}: PSL` -Definition of entity fields in *Prisma Schema Language* (PSL). See -[here for intro and examples](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema) -and [here for a more exhaustive language specification](https://github.com/prisma/specs/tree/master/schema). - -### Using Entities - -Entity-system in Wasp is based on [Prisma](http://www.prisma.io), and currently Wasp provides only a thin layer -on top of it. The workflow is as follows: - -1. Wasp developer creates/updates some of the entities in `.wasp` file. -2. Wasp developer runs `wasp db migrate-dev`. -3. Migration data is generated in `migrations/` folder (and should be committed). -4. Wasp developer uses Prisma JS API to work with the database when in Operations. - -#### Using Entities in Operations - -Most of the time in Wasp you will be working with entities in the context of Operations (Queries & Actions), so check their part of docs for more info on how to use entities in Operations. - -#### Using Entities directly - -If needed, you can also interact with entities directly via [Prisma Client(https://www.prisma.io/docs/concepts/components/prisma-client/crud) (although we recommend using them via injected `entities` when in Operations). - -To import Prisma Client in your Wasp server code, do `import prismaClient from '@wasp/dbClient'`. - -## Queries and Actions (aka Operations) - -In Wasp, the client and the server interact with each other through Operations. -Wasp currently supports two kinds of Operations: **Queries** and **Actions**. - -### Query - -Queries are used to fetch data from the server. They do not modify the server's state. - -Queries are implemented in NodeJS and executed within the server's context. -Wasp generates the code that lets you call the Query from anywhere in your code (client or server) using the same interface. -In other words, you won't have to worry about building an HTTP API for the Query, handling the request on the server, or even handling and caching the responses on the client. -Instead, simply focus on the business logic inside your Query and let Wasp take care of the rest! - -To create a Wasp Query, you must: -1. Define the Query's NodeJS implementation -2. Declare the Query in Wasp using the `query` declaration - -After completing these two steps, you'll be able to use the Query from any point in your code. - - -#### Defining the Query's NodeJS implementation -The Query's implementation is a NodeJS function that takes two arguments (it can be an `async` function but doesn't have to). -Since both arguments are positional, you can name the parameters however you want, but we'll stick with `args` and `context`: -1. `args`: An object containing all the arguments (i.e., payload) **passed to the Query by the caller** (e.g., filtering conditions). -Take a look at [the examples of usage](#using-the-query) to see how to pass this object to the Query. -3. `context`: An additional context object **injected into the Query by Wasp**. This object contains user session information, as well as information about entities. The examples here won't use the context for simplicity purposes. You can read more about it in the [section about using entities in queries](#using-entities-in-queries). - -Here's an example of three simple Queries: -```js title="src/server/queries.js" -// our "database" -const tasks = [ - { id: 1, description: "Buy some eggs", isDone: true }, - { id: 2, description: "Make an omelette", isDone: false }, - { id: 3, description: "Eat breakfast", isDone: false } -] - -// You don't need to use the arguments if you don't need them -export const getAllTasks = () => { - return tasks; -} - -// The 'args' object is something sent by the caller (most often from the client) -export const getFilteredTasks = (args) => { - const { isDone } = args; - return tasks.filter(task => task.isDone === isDone) -} - -// Query implementations can be async functions and use await. -export const getTasksWithDelay = async () => { - const result = await sleep(1000) - return tasks -} -``` - -#### Declaring a Query in Wasp -After implementing your Queries in NodeJS, all that's left to do before using them is tell Wasp about it! -You can easily do this with the `query` declaration, which supports the following fields: -- `fn: ServerImport` (required) - The import statement of the Query's NodeJs implementation. -- `entities: [Entity]` (optional) - A list of entities you wish to use inside your Query. -We'll leave this option aside for now. You can read more about it [here](#using-entities-in-queries). - -Wasp Queries and their implementations don't need to (but can) have the same name, so we will keep the names different to avoid confusion. -With that in mind, this is how you might declare the Queries that use the implementations from the previous step: -```wasp title="pages/main.wasp" -// ... - -// Again, it most likely makes sense to name the Wasp Query after -// its implementation. We're changing the name to emphasize the difference. - -query fetchAllTasks { - fn: import { getAllTasks } from "@server/queries.js" -} - -query fetchFilteredTasks { - fn: import { getFilteredTasks } from "@server/queries.js" -} -``` - -After declaring a NodeJS function as a Wasp Query, two crucial things happen: -- Wasp **generates a client-side JavaScript function** that shares its name with the Query (e.g., `fetchFilteredTasks`). -This function takes a single optional argument - an object containing any serializable data you wish to use inside the Query. -Wasp will pass this object to the Query's implementation as its first positional argument (i.e., `args` from the previous step). -Such an abstraction works thanks to an HTTP API route handler Wasp generates on the server, which calls the Query's NodeJS implementation under the hood. -- Wasp **generates a server-side NodeJS function** that shares its name with the Query. This function's interface is identical to the client-side function from the previous point. - -Generating two such functions ensures a uniform calling interface across the entire app (both client and server). - - -#### Using the Query -To use the Query, you can import it from `@wasp` and call it directly. As mentioned, the usage is the same regardless of whether you're on the server or the client: -```javascript -import fetchAllTasks from '@wasp/queries/fetchAllTasks.js' -import fetchFilteredTasks from '@wasp/queries/fetchFilteredTasks.js' - -// ... - -const allTasks = await fetchAllTasks(); -const doneTasks = await fetchFilteredTasks({isDone: true}) -``` - -**NOTE**: Wasp will not stop you from importing a Query's NodeJS implementation from `./queries.js` and calling it directly. However, we advise against this, as you'll lose all the useful features a Wasp Query provides (e.g., entity injection). - -#### The `useQuery` hook -When using Queries on the client, you can make them reactive with the help of the `useQuery` hook. -This hook comes bundled with Wasp and is a thin wrapper around the `useQuery` hook from [_react-query_](https://github.com/tannerlinsley/react-query). - -Wasp's `useQuery` hook accepts three arguments: -- `queryFn` (required): A Wasp query declared in the previous step or, in other words, the client-side query function generated by Wasp based on a `query` declaration. -- `queryFnArgs` (optional): The arguments object (payload) you wish to pass into the Query. The Query's NodeJS implementation will receive this object as its first positional argument. -- `options` (optional): A _react-query_ `options` object. Use this to change - [the default - behaviour](https://react-query.tanstack.com/guides/important-defaults) for - this particular query. If you want to change the global defaults, you can do - so in the [client setup function](#overriding-default-behaviour-for-queries). - -Wasp's `useQuery` hook behaves mostly the same as [_react-query_'s `useQuery` hook](https://react-query.tanstack.com/docs/api#usequery), the only difference being in not having to supply the key (Wasp does this automatically under the hood). - -Here's an example of calling the Queries using the `useQuery` hook: -```jsx -import React from 'react' -import { useQuery } from '@wasp/queries' - -import fetchAllTasks from '@wasp/queries/fetchAllTasks' -import fetchFilteredTasks from '@wasp/queries/fetchFilteredTasks' - -const MainPage = () => { - const { - data: allTasks, - error: error1 - } = useQuery(fetchAllTasks) - - const { - data: doneTasks, - error: error2 - } = useQuery(fetchFilteredTasks, { isDone: true }) - - return ( -
    -

    All Tasks

    - {allTasks ? allTasks.map(task => ) : error1} - -

    Finished Tasks

    - {doneTasks ? doneTasks.map(task => ) : error2} -
    - ) -} - -const Task = ({ description, isDone }) => { - return ( -
    -

    Description: { description }

    -

    Is done: { isDone ? 'Yes' : 'No' }

    -
    - ) -} - - -export default MainPage -``` - -#### Error Handling -For security reasons, all exceptions thrown in the Query's NodeJS implementation are sent to the client as responses with the HTTP status code `500`, with all other details removed. -Hiding error details by default helps against accidentally leaking possibly sensitive information over the network. - -If you do want to pass additional error information to the client, you can construct and throw an appropriate `HttpError` in your NodeJS Query function: -```js title=src/server/queries.js -import HttpError from '@wasp/core/HttpError.js' - -export const getTasks = async (args, context) => { - const statusCode = 403 - const message = 'You can\'t do this!' - const data = { foo: 'bar' } - throw new HttpError(statusCode, message, data) -} -``` - -If the status code is `4xx`, the client will receive a response object with the corresponding `.message` and `.data` fields and rethrow the error (with these fields included). -To prevent information leakage, the server won't forward these fields for any other HTTP status codes. - -#### Using Entities in Queries -In most cases, resources used in Queries will be [Entities](#entity). -To use an Entity in your Query, add it to the query declaration in Wasp: - -```wasp {4,9} title="main.wasp" - -query fetchAllTasks { - fn: import { getAllTasks } from "@server/queries.js", - entities: [Task] -} - -query fetchFilteredTasks { - fn: import { getFilteredTasks } from "@server/queries.js", - entities: [Task] -} -``` - -Wasp will inject the specified Entity into the Query's `context` argument, giving you access to the Entity's Prisma API: -```js title="src/server/queries.js" -export const getAllTasks = async (args, context) => { - return context.entities.Task.findMany({}) -} - -export const getFilteredTasks = async (args, context) => { - return context.entities.Task.findMany({ - where: { isDone: args.isDone } - }) -} -``` - -The object `context.entities.Task` exposes `prisma.task` from [Prisma's CRUD API](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/crud). - - -### Action - -Actions are very similar to Queries. So similar, in fact, we will only list the differences: -1. They can (and most often should) modify the server's state, while Queries are only allowed to read it. -2. Actions don't need to be reactive so you can call them directly. Still, Wasp does provide a `useAction` React hook for adding extra behavior to the Action (e.g., optimistic updates). -Read more about the [`useAction` hook](#the-useaction-hook) below. -3. `action` declarations in Wasp are mostly identical to `query` declarations. The only difference is in the declaration's name. - -Here's an implementation of a simple Action: - -```js title=src/server/actions.js -export const sayHi = async () => { - console.log('The client said Hi!') -} -``` -Its corresponding declaration in Wasp: - -```wasp title="main.wasp" -// ... - -action sayHi { - fn: import { sayHi } from "@server/actions.js" -} -``` -And an example of how to import and call the declared Action: - -```js -import sayHi from '@wasp/actions/sayHi' - -// ... - -sayHi() -``` - -Here's an example on how you might define a less contrived Action. -```js title=src/server/actions.js -// ... -export const updateTaskIsDone = ({ id, isDone }, context) => { - return context.entities.Task.update({ - where: { id }, - data: { isDone } - }) -} -``` -```wasp title=main.wasp -action updateTaskIsDone { - fn: import { updateTaskIsDone } from "@server/actions.js", - entities: [Task] -} -``` - -And here is how you might use it: -```jsx {4,18} title=src/client/pages/Task.js -import React from 'react' -import { useQuery } from '@wasp/queries' -import fetchTask from '@wasp/queries/fetchTask' -import updateTaskIsDone from '@wasp/actions/updateTaskIsDone' - -const TaskPage = ({ id }) => { - const { data: task } = useQuery(fetchTask, { id }) - - if (!task) { - return

    "Loading"

    - } - - const { description, isDone } = task - return ( -
    -

    Description: {description}

    -

    Is done: {isDone ? 'Yes' : 'No'}

    - -
    - ) -} -``` - -#### The `useAction` hook -When using Actions in components, you can enhance them with the help of the `useAction` hook. This hook comes bundled with Wasp and decorates Wasp Actions. -In other words, the hook returns a function whose API matches the original Action while also doing something extra under the hood (depending on how you configure it). - -The `useAction` hook accepts two arguments: -- `actionFn` (required) - The Wasp Action (i.e., the client-side query function generated by Wasp based on a query declaration) you wish to enhance. -- `actionOptions` (optional) - An object configuring the extra features you want to add to the given Action. While this argument is technically optional, there is no point in using the `useAction` hook without providing it (it would be the same as using the Action directly). The Action options object supports the following fields: - - `optimisticUpdates` (optional) - An array of objects where each object defines an [optimistic update](https://stackoverflow.com/a/33009713) to perform on the query cache. To define an optimistic update, you must specify the following properties: - - `getQuerySpecifier` (required) - A function returning the query specifier (i.e., a value used to address the query you want to update). A query specifier is an array specifying the query function and arguments. For example, to optimistically update the query used with `useQuery(fetchFilteredTasks, {isDone: true }]`, your `getQuerySpecifier` function would have to return the array `[fetchFilteredTasks, { isDone: true}]`. Wasp will forward the argument you pass into the decorated Action to this function (i.e., you can use the properties of the added/change item to address the query). - - `updateQuery` (required) - The function used to perform the optimistic update. It should return the desired state of the cache. Wasp will call it with the following arguments: - - `item` - The argument you pass into the decorated Action. - - `oldData` - The currently cached value for the query identified by the specifier. - -**NOTE:** The `updateQuery` function must be a pure function. It must return the desired cache value identified by the `getQuerySpecifier` function and _must not_ perform any side effects. Also, make sure you only update the query caches affected by your action causing the optimistic update (Wasp cannot yet verify this). Finally, your implementation of the `updateQuery` function should work correctly regardless of the state of `oldData` (e.g., don't rely on array positioning). If you need to do something else during your optimistic update, you can directly use _react-query_'s lower-level API (read more about it [here](#advanced-usage)). - -Here's an example showing how to configure the Action from the previous example to perform an optimistic update: -```jsx {3,9,10,11,12,13,14,15,16,27} title=src/client/pages/Task.js -import React from 'react' -import { useQuery } from '@wasp/queries' -import { useAction } from '@wasp/actions' -import fetchTask from '@wasp/queries/fetchTask' -import updateTaskIsDone from '@wasp/actions/updateTaskIsDone' - -const TaskPage = ({ id }) => { - const { data: task } = useQuery(fetchTask, { id }) - const updateTaskIsDoneOptimistically = useAction(updateTaskIsDone, { - optimisticUpdates: [ - { - getQuerySpecifier: ({ id }) => [fetchTask, { id }], - updateQuery: ({ isDone }, oldData) => ({ ...oldData, isDone }) - } - ] - }) - - if (!task) { - return

    "Loading"

    - } - - const { description, isDone } = task - return ( -
    -

    Description: {description}

    -

    Is done: {isDone ? 'Yes' : 'No'}

    - -
    - Back to main page -
    -
    - ) -} - -export default TaskPage -``` -#### Advanced usage -The `useAction` hook currently only supports specifying optimistic updates. You can expect more features in future versions of Wasp. - -Wasp's optimistic update API is deliberately small and focuses exclusively on updating Query caches (as that's the most common use case). You might need an API that offers more options or a higher level of control. If that's the case, instead of using Wasp's `useAction` hook, you can use _react-query_'s `useMutation` hook and directly work with [their low-level API](https://tanstack.com/query/v4/docs/guides/optimistic-updates?from=reactQueryV3&original=https://react-query-v3.tanstack.com/guides/optimistic-updates). - -If you decide to use _react-query_'s API directly, you will need access to the Query's cache key. Wasp internally uses this key but abstracts it from the programmer. Still, you can easily obtain it by accessing the `queryCacheKey` property on a Query: -```js -import { fetchTasks } from '@wasp/queries' - -const queryKey = fetchTasks.queryCacheKey -``` - -### Cache Invalidation -One of the trickiest parts of managing a web app's state is making sure the data returned by the queries is up to date. -Since Wasp uses _react-query_ for Query management, we must make sure to invalidate Queries (more specifically, their cached results managed by _react-query_) whenever they become stale. - -It's possible to invalidate the caches manually through several mechanisms _react-query_ provides (e.g., refetch, direct invalidation). -However, since manual cache invalidation quickly becomes complex and error-prone, Wasp offers a quicker and a more effective solution to get you started: **automatic Entity-based Query cache invalidation**. -Because Actions can (and most often do) modify the state while Queries read it, Wasp invalidates a Query's cache whenever an Action that uses the same Entity is executed. - -For example, let's assume that Action `createTask` and Query `getTasks` both use Entity `Task`. If `createTask` is executed, `getTasks`'s cached result may no longer be up-to-date. -Wasp will therefore invalidate it, making `getTasks` refetch data from the server, bringing it up to date again. - -In practice, this means that Wasp keeps the queries "fresh" without requiring you to think about cache invalidation. - -On the other hand, this kind of automatic cache invalidation can become wasteful (some updates might not be necessary) and will only work for Entities. If that's an issue, you can use the mechanisms provided by _react-query_ for now, and expect more direct support in Wasp for handling those use cases in a nice, elegant way. - -If you wish to optimistically set cache values after performing an action, you can do so using [optimistic updates](https://stackoverflow.com/a/33009713). Configure them using Wasp's [useAction hook](#the-useaction-hook). This is currently the only manual cache invalidation mechanism Wasps supports natively. For everything else, you can always rely on _react-query_. - -### Prisma Error Helpers -In your Operations, you may wish to handle general Prisma errors with HTTP-friendly responses. We have exposed two helper functions, `isPrismaError`, and `prismaErrorToHttpError`, for this purpose. As of now, we convert two specific Prisma errors (which we will continue to expand), with the rest being `500`. See the [source here](https://github.com/wasp-lang/wasp/blob/main/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/utils.js). - -#### `import statement`: -```js -import { isPrismaError, prismaErrorToHttpError } from '@wasp/utils.js' -``` - -##### Example of usage: -```js -try { - await context.entities.Task.create({...}) -} catch (e) { - if (isPrismaError(e)) { - throw prismaErrorToHttpError(e) - } else { - throw e - } -} -``` - -### CRUD operations on top of entities - -:::caution Early preview -This feature is currently in early preview. It doesn't contain all the planned features. - -In the future iterations of Wasp we plan on supporting: -- **authorization** that will allow you to specify which users can perform which operations -- **validation** of input data (e.g. using Zod schema validation) -::: - -For a specific [Entity](/docs/language/features#entity), you can tell Wasp to automatically instantiate server-side logic ([Queries](/docs/language/features#query) and [Actions](/docs/language/features#action)) for creating, reading, updating and deleting such entities. - -#### Which operations are supported? - -If we create CRUD operations for an entity named `Task`, - -```wasp title="main.wasp" -crud Tasks { // crud name here is "Tasks" - entity: Task, - operations: { - getAll: { - isPublic: true, // optional, defaults to false - }, - get: {}, - create: { - overrideFn: import { createTask } from "@server/tasks.js", // optional - }, - update: {}, - }, -} -``` - -Wasp will give you the following default implementations: - -**getAll** - returns all entities - -```js -// ... - -// If the operation is not public, Wasp checks if an authenticated user -// is making the request. - -return Task.findMany() -``` - -**get** - returns one entity by id field - -```js -// ... -// Wasp uses the field marked with `@id` in Prisma schema as the id field. -return Task.findUnique({ where: { id: args.id } }) -``` - -**create** - creates a new entity - -```js -// ... -return Task.create({ data: args.data }) -``` - -**update** - updates an existing entity - -```js -// ... -// Wasp uses the field marked with `@id` in Prisma schema as the id field. -return Task.update({ where: { id: args.id }, data: args.data }) -``` - -**delete** - deletes an existing entity - -```js -// ... -// Wasp uses the field marked with `@id` in Prisma schema as the id field. -return Task.delete({ where: { id: args.id } }) -``` - -:::info Current Limitations -In the default `create` and `update` implementations, we are saving all of the data that the client sends to the server. This is not always desirable, i.e. in the case when the client should not be able to modify all of the data in the entity. - -[In the future](#/docs/guides/crud#future-of-crud-operations-in-wasp), we are planning to add validation of action input, where only the data that the user is allowed to change will be saved. - -For now, the solution is to provide an override function. You can override the default implementation by using the `overrideFn` option and implementing the validation logic yourself. - -::: - -#### CRUD declaration - -The CRUD declaration works on top of an existing entity declaration. It is declared as follows: - -```wasp title="main.wasp" -crud Tasks { // crud name here is "Tasks" - entity: Task, - operations: { - getAll: { - isPublic: true, // optional, defaults to false - }, - get: {}, - create: { - overrideFn: import { createTask } from "@server/tasks.js", // optional - }, - update: {}, - }, -} -``` - -It has the following fields: -- `entity: Entity` - the entity to which the CRUD operations will be applied. -- `operations: { [operationName]: CrudOperationOptions }` - the operations to be generated. The key is the name of the operation, and the value is the operation configuration. - - The possible values for `operationName` are: - - `getAll` - - `get` - - `create` - - `update` - - `delete` - - `CrudOperationOptions` can have the following fields: - - `isPublic: bool` - Whether the operation is public or not. If it is public, no auth is required to access it. If it is not public, it will be available only to authenticated users. Defaults to `false`. - - `overrideFn: ServerImport` - The import statement of the optional override implementation in Node.js. - -#### Defining the overrides - -Like with actions and queries, you can define the implementation in a Javascript/Typescript file. The overrides are functions that take the following arguments: -- `args` - The arguments of the operation i.e. the data that's sent from the client. -- `context` - Context contains the `user` making the request and the `entities` object containing the entity that's being operated on. - -You can also import types for each of the functions you want to override from `@wasp/crud/{crud name}`. The available types are: -- `GetAllQuery` -- `GetQuery` -- `CreateAction` -- `UpdateAction` -- `DeleteAction` - -If you have a CRUD named `Tasks`, you would import the types like this: -```ts -import type { GetAllQuery, GetQuery, CreateAction, UpdateAction, DeleteAction } from '@wasp/crud/Tasks' - -// Each of the types is a generic type, so you can use it like this: -export const getAllOverride: GetAllQuery = async (args, context) => { - // ... -} -``` - -We are showing an example of an override in the [CRUD guide](/docs/guides/crud). - -#### Using the CRUD operations in client code - -On the client, you import the CRUD operations from `@wasp/crud/{crud name}`. The names of the imports are the same as the names of the operations. For example, if you have a CRUD called `Tasks`, you would import the operations like this: - -```jsx title="SomePage.jsx" -import { Tasks } from '@wasp/crud/Tasks' -``` - -You can then access the operations like this: -```jsx title="SomePage.jsx" -const { data } = Tasks.getAll.useQuery() -const { data } = Tasks.get.useQuery({ id: 1 }) -const createAction = Tasks.create.useAction() -const updateAction = Tasks.update.useAction() -const deleteAction = Tasks.delete.useAction() - -// The CRUD operations are using the existing actions and queries -// under the hood, so all the options are available as before. -``` - -Check out the [CRUD guide](/docs/guides/crud) to see how to use the CRUD operations in client code. - -## APIs - -In Wasp, the default client-server interaction mechanism is through [Operations](#queries-and-actions-aka-operations). However, if you need a specific URL method/path, or a specific response, Operations may not be suitable for you. For these cases, you can use an `api`! Best of all, they should look and feel very familiar. - -### API - -APIs are used to tie a JS function to an HTTP (method, path) pair. They are distinct from Operations and have no client-side helpers (like `useQuery`). - -To create a Wasp API, you must: -1. Define the APIs NodeJS implementation -2. Declare the API in Wasp using the `api` declaration - -After completing these two steps, you'll be able to call the API from client code (via our Axios wrapper), or from the outside world. - -:::note -In order to leverage the benefits of TypeScript and use types in your NodeJS implementation (step 1), you must add your `api` declarations to your `.wasp` file (step 2) _and_ compile the Wasp project. This will enable the Wasp compiler to generate any new types based on your `.wasp`file definitions for use in your implementation files. -::: - -#### Defining the APIs NodeJS implementation -An API should be implemented as a NodeJS function that takes three arguments. -1. `req`: Express Request object -2. `res`: Express Response object -3. `context`: An additional context object **injected into the API by Wasp**. This object contains user session information, as well as information about entities. The examples here won't use the context for simplicity purposes. You can read more about it in the [section about using entities in APIs](#using-entities-in-apis). - -##### Simple API example -```ts title="src/server/apis.ts" -import { FooBar } from '@wasp/apis/types' - -export const fooBar : FooBar = (req, res, context) => { - res.set('Access-Control-Allow-Origin', '*') // Example of modifying headers to override Wasp default CORS middleware. - res.json({ msg: `Hello, ${context.user?.username || "stranger"}!` }) -} -``` - -##### More complicated TypeScript example -Let's say you wanted to create some `GET` route that would take an email address as a param, and provide them the answer to "Life, the Universe and Everything." :) What would this look like in TypeScript? - -```wasp title="main.wasp" -api fooBar { - fn: import { fooBar } from "@server/apis.js", - entities: [Task], - httpRoute: (GET, "/foo/bar/:email") -} -``` - -```ts title="src/server/apis.ts" -import { FooBar } from '@wasp/apis/types' - -export const fooBar: FooBar< -{ email: string }, // params -{ answer: number } // response -> = (req, res, _context) => { - console.log(req.params.email) - res.json({ answer: 42 }) -} -``` - -#### Declaring an API in Wasp -After implementing your APIs in NodeJS, all that's left to do before using them is tell Wasp about it! -You can easily do this with the `api` declaration, which supports the following fields: -- `fn: ServerImport` (required) - The import statement of the APIs NodeJs implementation. -- `httpRoute: (HttpMethod, string)` (required) - The HTTP (method, path) pair, where the method can be one of: - - `ALL`, `GET`, `POST`, `PUT` or `DELETE` - - and path is an Express path `string`. -- `entities: [Entity]` (optional) - A list of entities you wish to use inside your API. -We'll leave this option aside for now. You can read more about it [here](#using-entities-in-apis). -- `auth: bool` (optional) - If auth is enabled, this will default to `true` and provide a `context.user` object. If you do not wish to attempt to parse the JWT in the Authorization Header, you may set this to `false`. -- `middlewareConfigFn: ServerImport` (optional) - The import statement to an Express middleware config function for this API. See [the guide here](/docs/guides/middleware-customization#2-customize-api-specific-middleware). - -Wasp APIs and their implementations don't need to (but can) have the same name. With that in mind, this is how you might declare the API that uses the implementations from the previous step: -```wasp title="pages/main.wasp" -// ... - -api fooBar { - fn: import { fooBar } from "@server/apis.js", - httpRoute: (GET, "/foo/bar") -} -``` - -#### Using the API -To use the API externally, you simply call the endpoint using the method and path you used. For example, if your app is running at `https://example.com` then from the above you could issue a `GET` to `https://example/com/foo/callback` (in your browser, Postman, `curl`, another web service, etc.). - -To use the API from your client, including with auth support, you can import the Axios wrapper from `@wasp/api` and invoke a call. For example: -```ts -import React, { useEffect } from 'react' -import api from '@wasp/api' - -async function fetchCustomRoute() { - const res = await api.get('/foo/bar') - console.log(res.data) -} - -export const Foo = () => { - useEffect(() => { - fetchCustomRoute() - }, []); - - return ( - <> - // ... - - ) -} -``` - -#### Using Entities in APIs -In many cases, resources used in APIs will be [Entities](#entity). -To use an Entity in your API, add it to the `api` declaration in Wasp: - -```wasp {3} title="main.wasp" -api fooBar { - fn: import { fooBar } from "@server/apis.js", - entities: [Task], - httpRoute: (GET, "/foo/bar") -} -``` - -Wasp will inject the specified Entity into the APIs `context` argument, giving you access to the Entity's Prisma API: -```ts title="src/server/apis.ts" -import { FooBar } from '@wasp/apis/types' - -export const fooBar : FooBar = (req, res, context) => { - res.json({ count: await context.entities.Task.count() }) -} - -``` - -The object `context.entities.Task` exposes `prisma.task` from [Prisma's CRUD API](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/crud). - -### apiNamespace - -An `apiNamespace` is a simple declaration used to apply some `middlewareConfigFn` to all APIs under some specific path. For example: - -```wasp title="main.wasp" -apiNamespace fooBar { - middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@server/apis.js", - path: "/foo/bar" -} -``` - -For more information about middleware configuration, please see: [Middleware Configuration](/docs/guides/middleware-customization) - -## Jobs - -If you have server tasks that you do not want to handle as part of the normal request-response cycle, Wasp allows you to make that function a `job` and it will gain some "superpowers." Jobs will: - * persist between server restarts - * can be retried if they fail - * can be delayed until the future - * can have a recurring schedule! - -Some examples where you may want to use a `job` on the server include sending an email, making an HTTP request to some external API, or doing some nightly calculations. - -### Job Executors - -Job executors handle the scheduling, monitoring, and execution of our jobs. - -Wasp allows you to choose which job executor will be used to execute a specific job that you define, which affects some of the finer details of how jobs will behave and how they can be further configured. Each job executor has its pros and cons, which we will explain in more detail below, so you can pick the one that best suits your needs. - -Currently, Wasp supports only one type of job executor, which is `PgBoss`, but in the future, it will likely support more. - -#### pg-boss - -We have selected [pg-boss](https://github.com/timgit/pg-boss/) as our first job executor to handle the low-volume, basic job queue workloads many web applications have. By using PostgreSQL (and [SKIP LOCKED](https://www.2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5/)) as its storage and synchronization mechanism, it allows us to provide many job queue pros without any additional infrastructure or complex management. - -:::info -Keep in mind that pg-boss jobs run alongside your other server-side code, so they are not appropriate for CPU-heavy workloads. Additionally, some care is required if you modify scheduled jobs. Please see pg-boss details below for more information. - -
    - pg-boss details - - pg-boss provides many useful features, which can be found [here](https://github.com/timgit/pg-boss/blob/8.4.2/README.md). - - When you add pg-boss to a Wasp project, it will automatically add a new schema to your database called `pgboss` with some internal tracking tables, including `job` and `schedule`. pg-boss tables have a `name` column in most tables that will correspond to your `job` identifier. Additionally, these tables maintain arguments, states, return values, retry information, start and expiration times, and other metadata required by pg-boss. - - If you need to customize the creation of the pg-boss instance, you can set an environment variable called `PG_BOSS_NEW_OPTIONS` to a stringified JSON object containing [these initialization parameters](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md#newoptions). **NOTE**: Setting this overwrites all Wasp defaults, so you must include database connection information as well. - - ##### pg-boss considerations - - Wasp starts pg-boss alongside your web server's application, where both are simultaneously operational. This means that jobs running via pg-boss and the rest of the server logic (like Operations) share the CPU, therefore you should avoid running CPU-intensive tasks via jobs. - - Wasp does not (yet) support independent, horizontal scaling of pg-boss-only applications, nor starting them as separate workers/processes/threads. - - The job name/identifier in your `.wasp` file is the same name that will be used in the `name` column of pg-boss tables. If you change a name that had a `schedule` associated with it, pg-boss will continue scheduling those jobs but they will have no handlers associated, and will thus become stale and expire. To resolve this, you can remove the applicable row from the `schedule` table in the `pgboss` schema of your database. - - If you remove a `schedule` from a job, you will need to do the above as well. - - If you wish to deploy to Heroku, you need to set an additional environment variable called `PG_BOSS_NEW_OPTIONS` to `{"connectionString":"","ssl":{"rejectUnauthorized":false}}`. This is because pg-boss uses the `pg` extension, which does not seem to connect to Heroku over SSL by default, which Heroku requires. Additionally, Heroku uses a self-signed cert, so we must handle that as well. -- https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js - -
    -::: - -### Basic job definition and usage - -To declare a `job` in Wasp, simply add a declaration with a reference to an `async` function, like the following: - -```wasp title="main.wasp" -job mySpecialJob { - executor: PgBoss, - perform: { - fn: import { foo } from "@server/workers/bar.js" - } -} -``` - -Then, in your [Operations](/docs/language/features#queries-and-actions-aka-operations) or [setupFn](/docs/language/features#setupfn-serverimport-optional) (or any other NodeJS code), you can submit work to be done: -```js -import { mySpecialJob } from '@wasp/jobs/mySpecialJob.js' - -const submittedJob = await mySpecialJob.submit({ job: "args" }) -console.log(await submittedJob.pgBoss.details()) - -// Or, if you'd prefer it to execute in the future, just add a .delay(). -// It takes a number of seconds, Date, or ISO date string. -await mySpecialJob.delay(10).submit({ job: "args" }) -``` - -And that is it! Your job will be executed by the job executor (pg-boss, in this case) as if you called `foo({ job: "args" })`. - -Note that in our example, `foo` takes an argument, but this does not always have to be the case. It all depends on how you've implemented your worker function. - -### Recurring jobs - -If you have work that needs to be done on some recurring basis, you can add a `schedule` to your job declaration: - -```wasp {6-9} title="main.wasp" -job mySpecialJob { - executor: PgBoss, - perform: { - fn: import { foo } from "@server/workers/bar.js" - }, - schedule: { - cron: "0 * * * *", - args: {=json { "job": "args" } json=} // optional - } -} -``` - -In this example, you do _not_ need to invoke anything in JavaScript. You can imagine `foo({ job: "args" })` getting automatically scheduled and invoked for you every hour. - -### Fully specified example -Both `perform` and `schedule` accept `executorOptions`, which we pass directly to the named job executor when you submit jobs. In this example, the scheduled job will have a `retryLimit` set to 0, as `schedule` overrides any similar property from `perform`. Lastly, we add an entity to pass in via the context argument to `perform.fn`. - -```wasp -job mySpecialJob { - executor: PgBoss, - perform: { - fn: import { foo } from "@server/workers/bar.js", - executorOptions: { - pgBoss: {=json { "retryLimit": 1 } json=} - } - }, - schedule: { - cron: "*/5 * * * *", - args: {=json { "foo": "bar" } json=}, - executorOptions: { - pgBoss: {=json { "retryLimit": 0 } json=} - } - }, - entities: [Task], -} -``` - -### Fields - -#### `executor: JobExecutor` (required) -`PgBoss` is currently our only job executor, and is recommended for low-volume production use cases. It requires your `app.db.system` to be `PostgreSQL`. - -#### `perform: dict` (required) - - - ##### `fn: ServerImport` (required) - An `async` JavaScript function of work to be performed. Since Wasp executes jobs on the server, you must import it from `@server`. The function receives a first argument which may be passed when the job is called, as well as the context containing any declared entities as the second (this is passed automatically by Wasp). Here is a sample signature: - - ```js - export async function foo(args, context) { - // Can reference context.entities.Task, for example. - } - ``` - - - ##### `executorOptions: dict` (optional) - Executor-specific default options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. These can be overridden during invocation with `submit()` or in a `schedule`. - - - ##### `pgBoss: JSON` (optional) - See the docs for [pg-boss](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md#sendname-data-options). - -#### `schedule: dict` (optional) - - - ##### `cron: string` (required) - A 5-placeholder format cron expression string. See rationale for minute-level precision [here](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md#scheduling). - - _If you need help building cron expressions, Check out_ [Crontab guru](https://crontab.guru/#0_*_*_*_*). - - - ##### `args: JSON` (optional) - The arguments to pass to the `perform.fn` function when invoked. - - - ##### `executorOptions: dict` (optional) - Executor-specific options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. The `perform.executorOptions` are the default options, and `schedule.executorOptions` can override/extend those. - - - ##### `pgBoss: JSON` (optional) - See the docs for [pg-boss](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md#sendname-data-options). - -#### `entities: [Entity]` (optional) -A list of entities you wish to use inside your Job (similar to Queries and Actions). - -### JavaScript API - -#### Invocation -##### `import` - -```js -import { mySpecialJob } from '@wasp/jobs/mySpecialJob.js' -``` - -##### `submit(jobArgs, executorOptions)` -- ###### `jobArgs: JSON` (optional) -- ###### `executorOptions: JSON` (optional) - -Submits a `job` to be executed by an executor, optionally passing in a JSON job argument your job handler function will receive, and executor-specific submit options. - -```js -const submittedJob = await mySpecialJob.submit({ job: "args" }) -``` - -##### `delay(startAfter)` (optional) -- ###### `startAfter: int | string | Date` (required) - -Delaying the invocation of the job handler. The delay can be one of: -- Integer: number of seconds to delay. [Default 0] -- String: ISO date string to run at. -- Date: Date to run at. - -```js -const submittedJob = await mySpecialJob.delay(10).submit({ job: "args" }, { "retryLimit": 2 }) -``` - -#### Tracking -The return value of `submit()` is an instance of `SubmittedJob`, which minimally contains: -- `jobId`: A getter returning the UUID String ID for the job in that executor. -- `jobName`: A getter returning the name of the job you used in your `.wasp` file. -- `executorName`: A getter returning a Symbol of the name of the job executor. - - For pg-boss, you can import a Symbol from: `import { PG_BOSS_EXECUTOR_NAME } from '@wasp/jobs/core/pgBoss/pgBossJob.js'` if you wish to compare against `executorName`. - -There will also be namespaced, job executor-specific objects. - -- For pg-boss, you may access: `pgBoss` - - **NOTE**: no arguments are necessary, as we already applied the `jobId` in the available functions. - - `details()`: pg-boss specific job detail information. [Reference](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md#getjobbyidid) - - `cancel()`: attempts to cancel a job. [Reference](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md#cancelid) - - `resume()`: attempts to resume a canceled job. [Reference](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md#resumeid) - -## Dependencies - -You can specify additional npm dependencies via `dependencies` field in `app` declaration, in following way: - -```wasp -app MyApp { - title: "My app", - // ... - dependencies: [ - ("redux", "^4.0.5"), - ("react-redux", "^7.1.3") - ] -} -``` - -You will need to re-run `wasp start` after adding a dependency for Wasp to pick it up. - -**NOTE**: In current implementation of Wasp, if Wasp is already internally using certain npm dependency with certain version specified, you are not allowed to define that same npm dependency yourself while specifying different version. -If you do that, you will get an error message telling you which exact version you have to use for that dependency. -This means Wasp dictates exact versions of certain packages, so for example you can't choose version of React you want to use. -In the future, we will add support for picking any version you like, but we have not implemented that yet. Check [issue #59](https://github.com/wasp-lang/wasp/issues/59) to check out the progress or contribute. - - -## Authentication & Authorization - -Wasp provides authentication and authorization support out-of-the-box. Enabling it for your app is optional and can be done by configuring the `auth` field of the `app` declaration: - -```wasp -app MyApp { - title: "My app", - //... - auth: { - userEntity: User, - externalAuthEntity: SocialLogin, - methods: { - usernameAndPassword: {}, // use this or email, not both - email: {}, // use this or usernameAndPassword, not both - google: {}, - gitHub: {}, - }, - onAuthFailedRedirectTo: "/someRoute" - } -} - -//... -``` - -`app.auth` is a dictionary with following fields: - -#### `userEntity: entity` (required) -Entity which represents the user. - -#### `externalAuthEntity: entity` (optional) -Entity which associates a user with some external authentication provider. We currently offer support for Google and GitHub. See the sections on [Social Login Providers](#social-login-providers-oauth-20) for more info. - -#### `methods: dict` (required) -List of authentication methods that Wasp app supports. Currently supported methods are: -* `usernameAndPassword`: authentication with a username and password. See [here](#username-and-password) for more. -* `email`: authentication with a email and password. See [here](#email-authentication) for more. -* `google`: authentication via Google accounts. See [here](#social-login-providers-oauth-20) for more. -* `gitHub`: authentication via GitHub accounts. See [here](#social-login-providers-oauth-20) for more. - -#### `onAuthFailedRedirectTo: String` (required) -Path where an unauthenticated user will be redirected to if they try to access a private page (which is declared by setting `authRequired: true` for a specific page). -Check out this [section of our Todo app tutorial](/docs/tutorial/auth#update-the-main-page-to-require-auth) to see an example of usage. - -#### `onAuthSucceededRedirectTo: String` (optional) -Path where a successfully authenticated user will be sent upon successful login/signup. -Default value is "/". - -:::note -Automatic redirect on successful login only works when using the Wasp provided [`Signup` and `Login` forms](#high-level-api) -::: - -### Username and Password - -`usernameAndPassword` authentication method makes it possible to signup/login into the app by using a username and password. -This method requires that `userEntity` specified in `auth` contains `username: string` and `password: string` fields: - -```wasp -app MyApp { - title: "My app", - //... - - auth: { - userEntity: User, - methods: { - usernameAndPassword: {}, - }, - onAuthFailedRedirectTo: "/someRoute" - } -} - -// Wasp requires the userEntity to have at least the following fields -entity User {=psl - id Int @id @default(autoincrement()) - username String @unique - password String -psl=} -``` - -We provide basic validations out of the box, which you can customize as shown below. Default validations are: -- `username`: non-empty -- `password`: non-empty, at least 8 characters, and contains a number - -Note that `username`s are stored in a case-sensitive manner. - -#### High-level API - -The quickest way to get started is by using the following API generated by Wasp: -- Signup and Login forms at `@wasp/auth/forms/Signup` and `@wasp/auth/forms/Login` routes - - For styling, these default authentication components have form classes associated for both login (`login-form`) and signup (`signup-form`). Additionally, they both share a common class (`auth-form`). -- `logout` function -- `useAuth()` React hook -**NOTE:** If the signup is successful, the Signup form will automatically log in the user. - -Check our [Todo app tutorial](/docs/tutorial/auth) to see how it works. See below for detailed specification of each of these methods. - -#### Lower-level API - -If you require more control in your authentication flow, you can achieve that in the following ways: -- If you don't want to use already generated Signup and Login forms and want to create your own, you can use `signup` and `login` function by invoking them from the client. -- If you want to execute custom code on the server during sign up, create your own sign up action which invokes Prisma client as `context.entities.[USER_ENTITY].create()` function, along with your custom code. - -The code of your custom sign-up action would look like this (your user entity being `User` in this instance): -```js title="src/server/auth/signup.js" -export const signUp = async (args, context) => { - // Your custom code before sign-up. - // ... - - const newUser = context.entities.User.create({ - data: { - username: args.username, - password: args.password // password hashed automatically by Wasp! 🐝 - } - }) - - // Your custom code after sign-up. - // ... - return newUser -} -``` - -:::info -You don't need to worry about hashing the password yourself! Even when you are using Prisma's client directly and calling `create()` with a plain-text password, Wasp's middleware takes care of hashing it before storing it in the database. An additional middleware also performs field validation. -::: - -##### Customizing user entity validations - -To disable/enable default validations, or add your own, you can modify your custom signUp function like so: -```js -const newUser = context.entities.User.create({ - data: { - username: args.username, - password: args.password // password hashed automatically by Wasp! 🐝 - }, - _waspSkipDefaultValidations: false, // can be omitted if false (default), or explicitly set to true - _waspCustomValidations: [ - { - validates: 'password', - message: 'password must contain an uppercase letter', - validator: password => /[A-Z]/.test(password) - }, - ] -}) -``` - -:::info -Validations always run on `create()`, but only when the field mentioned in `validates` is present for `update()`. The validation process stops on the first `validator` to return false. If enabled, default validations run first and validate basic properties of both the `'username'` or `'password'` fields. -::: - -#### Specification - -#### `login()` -An action for logging in the user. -```js -login(username, password) -``` -:::info -When using the exposed `login()` function, make sure to implement your own redirect on successful login logic -::: - -#### `username: string` -Username of the user logging in. - -#### `password: string` -Password of the user logging in. - -#### `import statement`: -```js -import login from '@wasp/auth/login' -``` -Login is a regular action and can be used directly from the frontend. - - -#### `signup()` -An action for signing up the user. This action does not log in the user, you still need to call `login()`. - -```js -signup(userFields) -``` -#### `userFields: object` -Auth-related fields (either `username` or `email` and `password`) of the user entity which was declared in `auth`. - -:::info -Wasp only stores the auth-related fields of the user entity. Adding extra fields to `userFields` will not have any effect. - -If you need to add extra fields to the user entity, we suggest doing it in a separate step after the user logs in for the first time. -::: - -#### `import statement`: -```js -import signup from '@wasp/auth/signup' -``` -Signup is a regular action and can be used directly from the frontend. - -#### `logout()` -An action for logging out the user. -```js -logout() -``` - -#### `import statement`: -```js -import logout from '@wasp/auth/logout' -``` - -##### Example of usage: -```jsx -import logout from '@wasp/auth/logout' - -const SignOut = () => { - return ( - - ) -} -``` - -#### Updating a user's password -If you need to update user's password, you can do it safely via Prisma client, e.g. within an action: -```js -export const updatePassword = async (args, context) => { - return context.entities.User.update({ - where: { id: args.userId }, - data: { - password: 'New pwd which will be hashed automatically!' - } - }) -} -``` -You don't need to worry about hashing the password yourself - if you have an `auth` declaration -in your `.wasp` file, Wasp already set a middleware on Prisma that makes sure whenever password -is created or updated on the user entity, it is also hashed before it is stored to the database. - -### Email authentication - -:::info -We have written a step-by-step guide on how to set up the e-mail authentication with Wasp's included Auth UI. - -Read more in the [email authentication guide](/docs/guides/email-auth). -::: - -:::warning -If a user signs up with Google or Github (and you set it up to save their social provider e-mail info on the `User` entity), they'll be able to reset their password and login with e-mail and password. - -If a user signs up with the e-mail and password and then tries to login with a social provider (Google or Github), they won't be able to do that. - -In the future, we will lift this limitation and enable smarter merging of accounts. -::: - -`email` authentication method makes it possible to signup/login into the app by using an e-mail and a password. - -```wasp title="main.wasp" -app MyApp { - title: "My app", - // ... - - auth: { - userEntity: User, - methods: { - email: { - // we'll deal with `email` below - }, - }, - onAuthFailedRedirectTo: "/someRoute" - }, - // ... -} - -// Wasp requires the userEntity to have at least the following fields -entity User {=psl - id Int @id @default(autoincrement()) - email String? @unique - password String? - isEmailVerified Boolean @default(false) - emailVerificationSentAt DateTime? - passwordResetSentAt DateTime? -psl=} -``` - -This method requires that `userEntity` specified in `auth` contains: - -- optional `email` field of type `String` -- optional `password` field of type `String` -- `isEmailVerified` field of type `Boolean` with a default value of `false` -- optional `emailVerificationSentAt` field of type `DateTime` -- optional `passwordResetSentAt` field of type `DateTime` - -#### Fields in the `email` dict - -```wasp title="main.wasp" -app MyApp { - title: "My app", - // ... - - auth: { - userEntity: User, - methods: { - email: { - fromField: { - name: "My App", - email: "hello@itsme.com" - }, - emailVerification: { - clientRoute: EmailVerificationRoute, - getEmailContentFn: import { getVerificationEmailContent } from "@server/auth/email.js", - }, - passwordReset: { - clientRoute: PasswordResetRoute, - getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js", - }, - allowUnverifiedLogin: false, - }, - }, - onAuthFailedRedirectTo: "/someRoute" - }, - // ... -} -``` - -##### `fromField: EmailFromField` (required) -`fromField` is a dict that specifies the name and e-mail address of the sender of the e-mails sent by Wasp. It is required to be defined. The object has the following fields: -- `name`: name of the sender (optional) -- `email`: e-mail address of the sender - -##### `emailVerification: EmailVerificationConfig` (required) -`emailVerification` is a dict that specifies the e-mail verification process. It is required to be defined. - -The object has the following fields: -- `clientRoute: Route`: a route that is used for the user to verify their e-mail address. (required) - -Client route should handle the process of taking a token from the URL and sending it to the server to verify the e-mail address. You can use our `verifyEmail` action for that. - -```js title="src/pages/EmailVerificationPage.jsx" -import { verifyEmail } from '@wasp/auth/email/actions'; -... -await verifyEmail({ token }); -``` - -Read on how to do it the easiest way with Auth UI in the [email authentication guide](/docs/guides/email-auth). - -- `getEmailContentFn: ServerImport`: a function that returns the content of the e-mail that is sent to the user. (optional) - -Defining `getEmailContentFn` can be done by defining a Javscript or Typescript file in the `server` directory. - -```ts title="server/email.ts" -import { GetVerificationEmailContentFn } from '@wasp/types' - -export const getVerificationEmailContent: GetVerificationEmailContentFn = ({ - verificationLink, -}) => ({ - subject: 'Verify your email', - text: `Click the link below to verify your email: ${verificationLink}`, - html: ` -

    Click the link below to verify your email

    - Verify email - `, -}) -``` - -##### `passwordReset: PasswordResetConfig` (required) -`passwordReset` is a dict that specifies the password reset process. It is required to be defined. The object has the following fields: -- `clientRoute: Route`: a route that is used for the user to reset their password. (required) - -Client route should handle the process of taking a token from the URL and a new password from the user and sending it to the server. You can use our `requestPasswordReset` and `resetPassword` actions to do that. - -```js title="src/pages/ForgotPasswordPage.jsx" -import { requestPasswordReset } from '@wasp/auth/email/actions'; -... -await requestPasswordReset({ email }); -``` - -```js title="src/pages/PasswordResetPage.jsx" -import { resetPassword } from '@wasp/auth/email/actions'; -... -await resetPassword({ password, token }) -``` - -##### `allowUnverifiedLogin: bool`: a boolean that specifies whether the user can login without verifying their e-mail address. (optional) - -It defaults to `false`. If `allowUnverifiedLogin` is set to `true`, the user can login without verifying their e-mail address, otherwise users will receive a `401` error when trying to login without verifying their e-mail address. - - -Read on how to do it the easiest way with Auth UI in the [email authentication guide](/docs/guides/email-auth). - -- `getEmailContentFn: ServerImport`: a function that returns the content of the e-mail that is sent to the user. (optional) - -Defining `getEmailContentFn` is done by defining a function that looks like this: - -```ts title="server/email.ts" -import { GetPasswordResetEmailContentFn } from '@wasp/types' - -export const getPasswordResetEmailContent: GetPasswordResetEmailContentFn = ({ - passwordResetLink, -}) => ({ - subject: 'Password reset', - text: `Click the link below to reset your password: ${passwordResetLink}`, - html: ` -

    Click the link below to reset your password

    - Reset password - `, -}) -``` - -#### Email sender for email authentication - -We require that you define an `emailSender`, so that Wasp knows how to send e-mails. Read more about that [here](#email-sender). - -#### Validations - -We provide basic validations out of the box. The validations are: -- `email`: non-empty, valid e-mail address -- `password`: non-empty, at least 8 characters, and contains a number - -Note that `email`s are stored in a case-insensitive manner. - -:::info -You don't need to worry about hashing the password yourself! Even when you are using Prisma's client directly and calling `create()` with a plain-text password, Wasp's middleware takes care of hashing it before storing it in the database. An additional middleware also performs field validation. -::: - - - -### Social Login Providers (OAuth 2.0) -Wasp allows you to easily add social login providers to your app. - -The following is a list of links to guides that will help you get started with the currently supported providers: -- [GitHub](/docs/integrations/github) -- [Google](/docs/integrations/google) - -When using Social Login Providers, Wasp gives you the following options: -- Default settings to get you started quickly -- UI Helpers to make it easy to add social login buttons and actions -- Override settings to customize the behavior of the providers - -#### Default Settings - - - - - -```wasp - auth: { - userEntity: User, - externalAuthEntity: SocialLogin, - methods: { - google: {}, - }, - } -``` - -

    Add google: {} to your auth.methods dictionary to use it with default settings

    -

    By default, Wasp expects you to set two environment variables in order to use Google authentication:

    -
      -
    • GOOGLE_CLIENT_ID
    • -
    • GOOGLE_CLIENT_SECRET
    • -
    -

    These can be obtained in your Google Cloud Console project dashboard. See here for a detailed guide.

    - -
    - - -```wasp - auth: { - userEntity: User, - externalAuthEntity: SocialLogin, - methods: { - gitHub: {}, - }, - } -``` - -

    Add gitHub: {} to your auth.methods dictionary to use it with default settings

    -

    By default, Wasp expects you to set two environment variables in order to use GitHub authentication:

    -
      -
    • GITHUB_CLIENT_ID
    • -
    • GITHUB_CLIENT_SECRET
    • -
    -

    These can be obtained in your GitHub project dashboard. See here for a detailed guide.

    - -
    -
    - -When a user signs in for the first time, if the `userEntity` has `username` and/or `password` fields Wasp assigns generated values to those fields by default (e.g. `username: nice-blue-horse-14357` and a strong random `password`). This is a historical coupling between auth methods that will be removed over time. If you'd like to change this behavior, these values can be overridden as described below. - -:::tip Overriding Defaults -It is also possible to [override the default](features#overrides-for-social-login-providers) login behaviors that Wasp provides for you. This allows you to create custom setups, such as allowing Users to define a username rather than the default random username assigned by Wasp on initial Login. -::: - -#### `externalAuthEntity` -Anytime an authentication method is used that relies on an external authorization provider, for example, Google, we require an `externalAuthEntity` specified in `auth`, in addition to the `userEntity`, that contains the following configuration: - -```wasp {4,14} -//... - auth: { - userEntity: User, - externalAuthEntity: SocialLogin, -//... - -entity User {=psl - id Int @id @default(autoincrement()) - //... - externalAuthAssociations SocialLogin[] -psl=} - -entity SocialLogin {=psl - id Int @id @default(autoincrement()) - provider String - providerId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int - createdAt DateTime @default(now()) - @@unique([provider, providerId, userId]) -psl=} -``` -:::note -the same `externalAuthEntity` can be used across different social login providers (e.g., both GitHub and Google can use the same entity). -::: -#### UI helpers - -Wasp provides sign-in buttons, logos and URLs for your login page: - -```jsx -... -import { SignInButton as GoogleSignInButton, signInUrl as googleSignInUrl, logoUrl as googleLogoUrl } from '@wasp/auth/helpers/Google' -import { SignInButton as GitHubSignInButton, signInUrl as gitHubSignInUrl, logoUrl as gitHubLogoUrl } from '@wasp/auth/helpers/GitHub' - -const Login = () => { - return ( - <> - ... - - - - {/* or */} - Sign in with Google - Sign in with GitHub - - ) -} - -export default Login -``` - -If you need more customization than what the buttons provide, you can create your own custom components using the `signInUrl`s. - -#### Overrides - -When a user signs in for the first time, Wasp will create a new User account and link it to the chosen Auth Provider account for future logins. If the `userEntity` contains a `username` field it will default to a random dictionary phrase that does not exist in the database, such as `nice-blue-horse-27160`. This is a historical coupling between auth methods that will be removed over time. - -If you would like to allow the user to select their own username, or some other sign up flow, you could add a boolean property to your `User` entity indicating the account setup is incomplete. You can then check this user's property on the client with the [`useAuth()`](#useauth) hook and redirect them when appropriate - - e.g. check on homepage if `user.isAuthSetup === false`, redirect them to `EditUserDetailsPage` where they can edit the `username` property. - -Alternatively, you could add a `displayName` property to your User entity and assign it using the details of their provider account. Below is an example of how to do this by using: - - the `getUserFieldsFn` function to configure the user's `username` or `displayName` from their provider account - -We also show you how to customize the configuration of the Provider's settings using: - - the `configFn` function - -```wasp title=main.wasp {9,10,13,14,26} -app Example { - //... - - auth: { - userEntity: User, - externalAuthEntity: SocialLogin, - methods: { - google: { - configFn: import { config } from "@server/auth/google.js", - getUserFieldsFn: import { getUserFields } from "@server/auth/google.js" - }, - gitHub: { - configFn: import { config } from "@server/auth/github.js", - getUserFieldsFn: import { getUserFields } from "@server/auth/github.js" - } - }, - - //... - } -} - -entity User {=psl - id Int @id @default(autoincrement()) - username String @unique - password String - displayName String? - externalAuthAssociations SocialLogin[] -psl=} - -//... - -``` - - -#### `configFn` - -This function should return an object with the following shape: - - - -```js title=src/server/auth/google.js -export function config() { - // ... - return { - clientID, // look up from env or elsewhere, - clientSecret, // look up from env or elsewhere, - scope: ['profile'] // must include at least 'profile' for Google - } -} - -// ... -``` - -

    Here is a link to the default implementations as a reference

    -
    - - -```js title=src/server/auth/github.js -export function config() { - // ... - return { - clientID, // look up from env or elsewhere, - clientSecret, // look up from env or elsewhere, - scope: [] // default is an empty array for GitHub - } -} - -// ... -``` - -

    Here is a link to the default implementations as a reference

    -
    -
    - -#### `getUserFieldsFn` - -This function should return the user fields to use when creating a new user upon their first time logging in with a Social Auth Provider. The context contains a User entity for DB access, and the args are what the OAuth provider responds with. Here is how you could generate a username based on the Google display name. In your model, you could choose to add more attributes and set additional information. - ```js title=src/server/auth/google.js - import { generateAvailableUsername } from '@wasp/core/auth.js' - - // ... - - export async function getUserFields(_context, args) { - const username = await generateAvailableUsername(args.profile.displayName.split(' '), { separator: '.' }) - return { username } - } - ``` - - Or you could set the optional `displayName` property on the `User` entity instead: - ```js title=src/server/auth/google.js - import { generateAvailableDictionaryUsername, generateAvailableUsername } from '@wasp/core/auth.js' - - // ... - - export async function getUserFields(_context, args) { - const username = await generateAvailableDictionaryUsername() - const displayName = await generateAvailableUsername(args.profile.displayName.split(' '), { separator: '.' }) - return { username, displayName } - } - ``` - - `generateAvailableUsername` takes an array of Strings and an optional separator and generates a string ending with a random number that is not yet in the database. For example, the above could produce something like "Jim.Smith.3984" for a Google user Jim Smith. - - `generateAvailableDictionaryUsername` generates a random dictionary phrase that is not yet in the database. For example, `nice-blue-horse-27160`. - - -### Validation Error Handling -When creating, updating, or deleting entities, you may wish to handle validation errors. We have exposed a class called `AuthError` for this purpose. This could also be combined with [Prisma Error Helpers](/docs/language/features#prisma-error-helpers). - -#### `import statement`: -```js -import AuthError from '@wasp/core/AuthError.js' -``` - -##### Example of usage: -```js -try { - await context.entities.User.update(...) -} catch (e) { - if (e instanceof AuthError) { - throw new HttpError(422, 'Validation failed', { message: e.message }) - } else { - throw e - } -} -``` - -## Accessing the currently logged in user -When authentication is enabled in a Wasp app, we need a way to tell whether a user is logged in and access its data. -With that, we can further implement access control and decide which content is private and which public. - -#### On the client -On the client, Wasp provides a React hook you can use in functional components - `useAuth`. -This hook is actually a thin wrapper over Wasp's [`useQuery` hook](http://localhost:3002/docs/language/features#the-usequery-hook) and returns data in the same format. - -### `useAuth()` -#### `import statement`: -```js -import useAuth from '@wasp/auth/useAuth' -``` - -##### Example of usage: -```js title="src/client/pages/MainPage.js" -import React from 'react' - -import { Link } from 'react-router-dom' -import useAuth from '@wasp/auth/useAuth' -import logout from '@wasp/auth/logout' -import Todo from '../Todo' -import '../Main.css' - -const Main = () => { - const { data: user } = useAuth() - - if (!user) { - return ( - - Please login or sign up. - - ) - } else { - return ( - <> - - - < /> - ) - } -} - -export default Main -``` - -#### On the server - -### `context.user` - -When authentication is enabled, all operations (actions and queries) will have access to the `user` through the `context` argument. `context.user` will contain all the fields from the user entity except for the password. - -##### Example of usage: -```js title="src/server/actions.js" -import HttpError from '@wasp/core/HttpError.js' - -export const createTask = async (task, context) => { - if (!context.user) { - throw new HttpError(403) - } - - const Task = context.entities.Task - return Task.create({ - data: { - description: task.description, - user: { - connect: { id: context.user.id } - } - } - }) -} -``` -In order to implement access control, each operation is responsible for checking `context.user` and -acting accordingly - e.g. if `context.user` is `undefined` and the operation is private then user -should be denied access to it. - -## Client configuration - -You can configure the client using the `client` field inside the `app` -declaration, - -```wasp -app MyApp { - title: "My app", - // ... - client: { - rootComponent: import Root from "@client/Root.jsx", - setupFn: import mySetupFunction from "@client/myClientSetupCode.js" - } -} -``` - -`app.client` is a dictionary with the following fields: - -#### `rootComponent: ClientImport` (optional) - -`rootComponent` defines the root component of your client application. It is -expected to be a React component, and Wasp will use it to wrap your entire app. -It must render its children, which are the actual pages of your application. - -You can use it to define a common layout for your application: - -```jsx title="src/client/Root.jsx" -export default async function Root({ children }) { - return ( -
    -
    -

    My App

    -
    - {children} -
    -

    My App footer

    -
    -
    - ) -} -``` - -You can use it to set up various providers that your application needs: - -```jsx title="src/client/Root.jsx" -import store from './store' -import { Provider } from 'react-redux' - -export default async function Root({ children }) { - return ( - - {children} - - ) -} -``` - -As long as you render the children, you can do whatever you want in your root -component. Here's an example of a root component both sets up a provider and -renders a custom layout: - -```jsx title="src/client/Root.jsx" -import store from './store' -import { Provider } from 'react-redux' - -export default function Root({ children }) { - return ( - - - {children} - - - ) -} - -function Layout({ children }) { - return ( -
    -
    -

    My App

    -
    - {children} -
    -

    My App footer

    -
    -
    - ) -} -``` - - -#### `setupFn: ClientImport` (optional) - -`setupFn` declares a JavaScript function that Wasp executes on the client -before everything else. It is expected to be asynchronous, and -Wasp will await its completion before rendering the page. The function takes no -arguments, and its return value is ignored. - -You can use this function to perform any custom setup (e.g., setting up -client-side periodic jobs). - -Here's a dummy example of such a function: - -```js title="src/client/myClientSetupCode.js" -export default async function mySetupFunction() { - let count = 1; - setInterval( - () => console.log(`You have been online for ${count++} hours.`), - 1000 * 60 * 60, - ) -} -``` - -##### Overriding default behaviour for Queries -As mentioned, our `useQuery` hook uses _react-query_'s hook of the same name. -Since _react-query_ comes configured with aggressive but sane default options, -you most likely won't have to change those defaults for all Queries (you can -change them for a single Query using the `options` object, as described -[here](#the-usequery-hook)). - -Still, if you do need the global defaults, you can do so inside client setup -function. Wasp exposes a `configureQueryClient` hook that lets you configure -_react-query_'s `QueryClient` object: - - -```js title="src/client/myClientSetupCode.js" -import { configureQueryClient } from '@wasp/queryClient' - -export default async function mySetupFunction() { - // ... some setup - configureQueryClient({ - defaultOptions: { - queries: { - staleTime: Infinity, - } - } - }) - // ... some more setup -} -``` - -Make sure to pass in an object expected by the `QueryClient`'s constructor, as -explained in -[_react-query_'s docs](https://tanstack.com/query/v4/docs/react/reference/QueryClient). - -## Public static files on the client - -If you wish to override the default `favicon.ico` file or expose any other static files to the client, you can do so by placing them in the `public` directory in the `src/client` folder. - -The contents of this directory will be copied to the `dist/public` directory during the build process. This makes these files available at the root of the domain. For example, if you have a file `favicon.ico` in the `public` directory, it will be available at `https://example.com/favicon.ico`. - -For example, doing this: -``` -src -└── client - ├── public - │ └── favicon.ico - └── ... -``` -will result in the following directory structure in the `build` folder: -``` -build -└── public - └── favicon.ico -``` - -:::warning Usage in client code -You **can't import these files** from your client code. They are only exposed at the root of the domain, e.g. `https://example.com/favicon.ico`. -::: - -## Server configuration - -Via `server` field of `app` declaration, you can configure the behavior of the Node.js server (one that is executing wasp operations). - -```wasp -app MyApp { - title: "My app", - // ... - server: { - setupFn: import mySetupFunction from "@server/myServerSetupCode.js" - } -} -``` - -`app.server` is a dictionary with the following fields: - -#### `middlewareConfigFn: ServerImport` (optional) - -The import statement to an Express middleware config function. This is a global modification affecting all operations and APIs. See [the guide here](/docs/guides/middleware-customization#1-customize-global-middleware). - -#### `setupFn: ServerImport` (optional) - -`setupFn` declares a JS function that will be executed on server start. This function is expected to be async and will be awaited before the server starts accepting any requests. - -It allows you to do any custom setup, e.g. setting up additional database/websockets or starting cron/scheduled jobs. - -The `setupFn` function receives the `express.Application` and the `http.Server` instances as part of its context. They can be useful for setting up any custom server routes or for example, setting up `socket.io`. -```ts -export type ServerSetupFn = (context: ServerSetupFnContext) => Promise - -export type ServerSetupFnContext = { - app: Application, // === express.Application - server: Server, // === http.Server -} -``` - -As an example, adding a custom route would look something like: -```ts title="src/server/myServerSetupCode.ts" -import { ServerSetupFn, Application } from '@wasp/types' - -const mySetupFunction: ServerSetupFn = async ({ app }) => { - addCustomRoute(app) -} - -function addCustomRoute(app: Application) { - app.get('/customRoute', (_req, res) => { - res.send('I am a custom route') - }) -} -``` - -In case you want to store some values for later use, or to be accessed by the Operations, recommended way is to store those in variables in the same module/file where you defined the javascript setup function and then expose additional functions for reading those values, which you can then import directly from Operations and use. This effectively turns your module into a singleton whose construction is performed on server start. - -Dummy example of such function and its usage: - -```js title="src/server/myServerSetupCode.js" -let someResource = undefined - -const mySetupFunction = async () => { - // Let's pretend functions setUpSomeResource and startSomeCronJob - // are implemented below or imported from another file. - someResource = await setUpSomeResource() - startSomeCronJob() -} - -export const getSomeResource = () => someResource - -export default mySetupFunction -``` - -```js title="src/server/queries.js" -import { getSomeResource } from './myServerSetupCode.js' - -... - -export const someQuery = async (args, context) => { - const someResource = getSomeResource() - return queryDataFromSomeResource(args, someResource) -} -``` - - -## .env - -Your project will likely be using environment variables for configuration, typically to define connection to the database, API keys for external services and similar. - -When in production, you will typically define environment variables through mechanisms provided by your hosting provider. - -However, when in development, you might also need to supply certain environment variables, and to avoid doing it "manually", Wasp supports `.env` (dotenv) files where you can define environment variables that will be used during development (they will not be used during production). - -Since environmental variables are usually different for server-side and client apps, in Wasp root directly you can create two files, `.env.server` for server-side of your Wasp project, and `.env.client` for client side (or web app) of Wasp project. - - -`.env.server` and `.env.client` files have to be defined in the root of your Wasp project. - -`.env.server` and `.env.client` files should not be committed to the version control - we already ignore it by default in the .gitignore file we generate when you create a new Wasp project via `wasp new` cli command. - -Variables are defined in `.env.server` or `.env.client` files in the form of `NAME=VALUE`, for example: -``` -DATABASE_URL=postgresql://localhost:5432 -MY_VAR=somevalue -``` - -Any env vars defined in the `.env.server` / `.env.client` files will be forwarded to the server-side / web app of your Wasp project and therefore accessible in your javascript code via `process.env`, for example: -```js -console.log(process.env.DATABASE_URL) -``` - -## Database configuration - -Via `db` field of `app` declaration, you can configure the database used by Wasp. - -```wasp -app MyApp { - title: "My app", - // ... - db: { - system: PostgreSQL, - seeds: [ - import devSeed from "@server/dbSeeds.js" - ], - prisma: { - clientPreviewFeatures: ["extendedWhereUnique"] - } - } -} -``` - -`app.db` is a dictionary with following fields: - -#### - `system: DbSystem` (Optional) -The database system Wasp will use. It can be either `PostgreSQL` or `SQLite`. -If not defined, or even if whole `db` field is not present, default value is `SQLite`. -If you add/remove/modify `db` field, run `wasp db migrate-dev` to apply the changes. - -#### - `seeds: [ServerImport]` (Optional) -Defines seed functions that you can use via `wasp db seed` to seed your database with initial data. -Check out [Seeding](#seeding) section for more details. - -#### - `prisma: [PrismaOptions]` (Optional) -Additional configuration for Prisma. -It currently only supports a single field: - - `clientPreviewFeatures : string` - allows you to define [Prisma client preview features](https://www.prisma.io/docs/concepts/components/preview-features/client-preview-features). - - -### SQLite -Default database is `SQLite`, since it is great for getting started with a new project (needs no configuring), but it can be used only in development - once you want to deploy Wasp to production you will need to switch to `PostgreSQL` and stick with it. -Check below for more details on how to migrate from SQLite to PostgreSQL. - -### PostgreSQL -When using `PostgreSQL` as your database (`app: { db: { system: PostgreSQL } }`), you will need to make sure you have a postgres database running during development (when running `wasp start` or doing `wasp db ...` commands). - -### Using Wasp provided dev database - -Wasp provides `wasp start db` command that starts the default dev db for you. - -Your Wasp app will automatically connect to it once you have it running via `wasp start db`, no additional configuration is needed. This command relies on Docker being installed on your machine. - -### Connecting to existing database - -If instead of using `wasp start db` you would rather spin up your own dev database or connect to some external database, you will need to provide Wasp with `DATABASE_URL` environment variable that Wasp will use to connect to it. - -The easiest way to provide the needed `DATABASE_URL` environment variable is by adding it to the [.env.server](https://wasp-lang.dev/docs/language/features#env) file in the root dir of your Wasp project (if that file doesn't yet exist, create it). - -You can also set it per command by doing `DATABASE_URL= wasp ...` -> this can be useful if you want to run specific `wasp` command on a specific database. -Example: you could do `DATABASE_URL= wasp db seed myProdSeed` to seed data for a fresh staging or production database. - -### Migrating from SQLite to PostgreSQL -To run Wasp app in production, you will need to switch from `SQLite` to `PostgreSQL`. - -1. Set `app.db.system` to `PostgreSQL`. -3. Delete old migrations, since they are SQLite migrations and can't be used with PostgreSQL: `rm -r migrations/`. -3. Run `wasp start db` to start your new db running (or check instructions above if you prefer using your own db). Leave it running, since we need it for the next step. -4. In a different terminal, run `wasp db migrate-dev` to apply new changes and create new, initial migration. -5. That is it, you are all done! - -### Seeding - -**Database seeding** is a term for populating database with some initial data. - -Seeding is most commonly used for two following scenarios: - 1. To put development database into a state convenient for testing / playing with it. - 2. To initialize dev/staging/prod database with some essential data needed for it to be useful, - for example default currencies in a Currency table. - -#### Writing a seed function - -Wasp enables you to define multiple **seed functions** via `app.db.seeds`: - -```wasp -app MyApp { - // ... - db: { - // ... - seeds: [ - import { devSeedSimple } from "@server/dbSeeds.js", - import { prodSeed } from "@server/dbSeeds.js" - ] - } -} -``` - -Each seed function is expected to be an async function that takes one argument, `prismaClient`, which is a [Prisma Client](https://www.prisma.io/docs/concepts/components/prisma-client/crud) instance that you can use to interact with the database. -This is the same instance of Prisma Client that Wasp uses internally, so you e.g. get password hashing for free. - -Since a seed function is part of the server-side code, it can also import other server-side code, so you can and will normally want to import and use Actions to perform the seeding. - -Example of a seed function that imports an Action (+ a helper function to create a user): - -```js -import { createTask } from './actions.js' - -export const devSeedSimple = async (prismaClient) => { - const user = await createUser(prismaClient, { - username: "RiuTheDog", - password: "bark1234" - }) - - await createTask( - { description: "Chase the cat" }, - { user, entities: { Task: prismaClient.task } } - ) -} - -async function createUser (prismaClient, data) { - const { password, ...newUser } = await prismaClient.user.create({ data }) - return newUser -} -``` - -#### Running seed functions - - - `wasp db seed`: If you have just one seed function, it will run it. If you have multiple, it will interactively ask you to choose one to run. - - - `wasp db seed `: It will run the seed function with the specified name, where the name is the identifier you used in its `import` expression in the `app.db.seeds` list. Example: `wasp db seed devSeedSimple`. - -:::tip - Often you will want to call `wasp db seed` right after you ran `wasp db reset`: first you empty your database, then you fill it with some initial data. -::: - - -## Email sender - -#### `provider: EmailProvider` (required) - -We support multiple different providers for sending e-mails: `SMTP`, `SendGrid` and `Mailgun`. - -### SMTP - -SMTP e-mail sender uses your SMTP server to send e-mails. - -Read [our guide](/docs/advanced/email#using-the-smtp-provider) for setting up SMTP for more details. - - -### SendGrid - -SendGrid is a popular service for sending e-mails that provides both API and SMTP methods of sending e-mails. We use their official SDK for sending e-mails. - -Check out [our guide](/docs/advanced/email#using-the-sendgrid-provider) for setting up Sendgrid for more details. - -### Mailgun - -Mailgun is a popular service for sending e-mails that provides both API and SMTP methods of sending e-mails. We use their official SDK for sending e-mails. - -Check out [our guide](/docs/advanced/email#using-the-mailgun-provider) for setting up Mailgun for more details. - -#### `defaultSender: EmailFromField` (optional) - -You can optionally provide a default sender info that will be used when you don't provide it explicitly when sending an e-mail. - -```wasp -app MyApp { - title: "My app", - // ... - emailSender: { - provider: SMTP, - defaultFrom: { - name: "Hello", - email: "hello@itsme.com" - }, - }, -} -``` - -After you set up the email sender, you can use it in your code to send e-mails. For example, you can send an e-mail when a user signs up, or when a user resets their password. - -### Sending e-mails - - - -To send an e-mail, you can use the `emailSender` that is provided by the `@wasp/email` module. - -```ts title="src/actions/sendEmail.js" -import { emailSender } from '@wasp/email/index.js' - -// In some action handler... -const info = await emailSender.send({ - to: 'user@domain.com', - subject: 'Saying hello', - text: 'Hello world', - html: 'Hello world' -}) -``` diff --git a/web/docs/project/client-config.md b/web/docs/project/client-config.md index e7ca198e05..9b9070ef9c 100644 --- a/web/docs/project/client-config.md +++ b/web/docs/project/client-config.md @@ -211,7 +211,7 @@ export default async function mySetupFunction(): Promise { ### Overriding Default Behaviour for Queries :::info -You can change the options for a **single** Query using the `options` object, as described [here](/docs/data-model/operations/queries#the-usequery-hook-1). +You can change the options for a **single** Query using the `options` object, as described [here](../data-model/operations/queries#the-usequery-hook-1). ::: Wasp's `useQuery` hook uses `react-query`'s `useQuery` hook under the hood. Since `react-query` comes configured with aggressive but sane default options, you most likely won't have to change those defaults for all Queries. diff --git a/web/docs/project/css-frameworks.md b/web/docs/project/css-frameworks.md index f5ca4ecdff..a1f9972298 100644 --- a/web/docs/project/css-frameworks.md +++ b/web/docs/project/css-frameworks.md @@ -85,19 +85,15 @@ Make sure to use the `.cjs` extension for these config files, if you name them w ### Adding Tailwind Plugins -To add Tailwind plugins, add it to [dependencies](/docs/project/dependencies) in your `main.wasp` file and to the plugins list in your `tailwind.config.cjs` file: +To add Tailwind plugins, install them as npm development [dependencies](../project/dependencies) and add them to the plugins list in your `tailwind.config.cjs` file: -```wasp title="./main.wasp" {4-5} -app todoApp { - // ... - dependencies: [ - ("@tailwindcss/forms", "^0.5.3"), - ("@tailwindcss/typographjy", "^0.5.7"), - ], - // ... -} +```shell +npm install -D @tailwindcss/forms +npm install -D @tailwindcss/typography ``` +and also + ```js title="./tailwind.config.cjs" {5-6} /** @type {import('tailwindcss').Config} */ module.exports = { diff --git a/web/docs/project/customizing-app.md b/web/docs/project/customizing-app.md index fe3bffea4d..859d652d54 100644 --- a/web/docs/project/customizing-app.md +++ b/web/docs/project/customizing-app.md @@ -2,7 +2,7 @@ title: Customizing the App --- -import { Required } from '@site/src/components/Required'; +import { Required } from '@site/src/components/Tag'; Each Wasp project can have only one `app` type declaration. It is used to configure your app and its components. @@ -76,9 +76,6 @@ app todoApp { db: { // ... }, - dependencies: [ - // ... - ], emailSender: { // ... }, @@ -113,28 +110,24 @@ The rest of the fields are covered in dedicated sections of the docs: - `auth: dict` - Authentication configuration. Read more in the [authentication section](/docs/auth/overview) of the docs. + Authentication configuration. Read more in the [authentication section](../auth/overview) of the docs. - `client: dict` - Configuration for the client side of your app. Read more in the [client configuration section](/docs/project/client-config) of the docs. + Configuration for the client side of your app. Read more in the [client configuration section](../project/client-config) of the docs. - `server: dict` - Configuration for the server side of your app. Read more in the [server configuration section](/docs/project/server-config) of the docs. + Configuration for the server side of your app. Read more in the [server configuration section](../project/server-config) of the docs. - `db: dict` - Database configuration. Read more in the [database configuration section](/docs/data-model/backends) of the docs. - -- `dependencies: [(string, string)]` - - List of npm dependencies for your app. Read more in the [dependencies section](/docs/project/dependencies) of the docs. + Database configuration. Read more in the [database configuration section](../data-model/backends) of the docs. - `emailSender: dict` - Email sender configuration. Read more in the [email sending section](/docs/advanced/email) of the docs. + Email sender configuration. Read more in the [email sending section](../advanced/email) of the docs. - `webSocket: dict` - WebSocket configuration. Read more in the [WebSocket section](/docs/advanced/web-sockets) of the docs. + WebSocket configuration. Read more in the [WebSocket section](../advanced/web-sockets) of the docs. diff --git a/web/docs/project/dependencies.md b/web/docs/project/dependencies.md index d6c179373b..a3a0d8593a 100644 --- a/web/docs/project/dependencies.md +++ b/web/docs/project/dependencies.md @@ -2,31 +2,12 @@ title: Dependencies --- -Specifying npm dependencies in Wasp project is done via the `dependencies` field in the `app` declaration, in the following way: - -```wasp -app MyApp { - title: "My app", - // ... - dependencies: [ - ("redux", "^4.0.5"), - ("react-redux", "^7.1.3") - ] -} -``` - -You will need to re-run `wasp start` after adding a dependency for Wasp to pick it up. - -The quickest way to find out the latest version of a package is to run: -```shell -npm view version -``` +Specifying npm dependencies in Wasp project is done in a typical way for JS projects: via [package.json](https://docs.npmjs.com/cli/configuring-npm/package-json) file at the top level of the project, via the `dependencies` (or `devDependencies`) field. :::note Using Packages that are Already Used by Wasp Internally In the current implementation of Wasp, if Wasp is already internally using a certain npm dependency with a certain version specified, you are not allowed to define that same npm dependency yourself while specifying _a different version_. If you do that, you will get an error message telling you which exact version you have to use for that dependency. This means Wasp _dictates exact versions of certain packages_, so for example you can't choose version of React you want to use. - -We are currently working on a restructuring that will solve this and some other quirks that the current dependency system has: check [issue #734](https://github.com/wasp-lang/wasp/issues/734) to follow our progress. +We are currently working on a restructuring that will solve this and some other quirks: check [issue #734](https://github.com/wasp-lang/wasp/issues/734) to follow our progress. ::: diff --git a/web/docs/project/env-vars.md b/web/docs/project/env-vars.md index d4157b239d..775c75cc39 100644 --- a/web/docs/project/env-vars.md +++ b/web/docs/project/env-vars.md @@ -131,4 +131,4 @@ The way you provide env vars to your Wasp project in production depends on where flyctl secrets set SOME_VAR_NAME=somevalue ``` -You can read a lot more details in the [deployment section](/docs/advanced/deployment/manually) of the docs. We go into detail on how to define env vars for each deployment option. +You can read a lot more details in the [deployment section](../advanced/deployment/manually) of the docs. We go into detail on how to define env vars for each deployment option. diff --git a/web/docs/project/server-config.md b/web/docs/project/server-config.md index 5fe5e60f2a..10800d1f59 100644 --- a/web/docs/project/server-config.md +++ b/web/docs/project/server-config.md @@ -90,7 +90,7 @@ function addCustomRoute(app: Application) { ### Storing Some Values for Later Use -In case you want to store some values for later use, or to be accessed by the [Operations](/docs/data-model/operations/overview) you do that in the `setupFn` function. +In case you want to store some values for later use, or to be accessed by the [Operations](../data-model/operations/overview) you do that in the `setupFn` function. Dummy example of such function and its usage: @@ -247,4 +247,4 @@ app MyApp { - #### `middlewareConfigFn: ServerImport` - The import statement to an Express middleware config function. This is a global modification affecting all operations and APIs. See more in the [configuring middleware section](/docs/advanced/middleware-config#1-customize-global-middleware). + The import statement to an Express middleware config function. This is a global modification affecting all operations and APIs. See more in the [configuring middleware section](../advanced/middleware-config#1-customize-global-middleware). diff --git a/web/docs/project/testing.md b/web/docs/project/testing.md index b64f2ed0bc..5816cd1c23 100644 --- a/web/docs/project/testing.md +++ b/web/docs/project/testing.md @@ -69,7 +69,7 @@ Wasp provides several functions to help you write React tests: const { mockQuery, mockApi } = mockServer(); ``` - - `mockQuery`: Takes a Wasp [query](/docs/data-model/operations/queries) to mock and the JSON data it should return. + - `mockQuery`: Takes a Wasp [query](../data-model/operations/queries) to mock and the JSON data it should return. ```js import getTasks from "@wasp/queries/getTasks"; @@ -81,7 +81,7 @@ Wasp provides several functions to help you write React tests: - Behind the scenes, Wasp uses [`msw`](https://npmjs.com/package/msw) to create a server request handle that responds with the specified data. - Mock are cleared between each test. - - `mockApi`: Similar to `mockQuery`, but for [APIs](/docs/advanced/apis). Instead of a Wasp query, it takes a route containing an HTTP method and a path. + - `mockApi`: Similar to `mockQuery`, but for [APIs](../advanced/apis). Instead of a Wasp query, it takes a route containing an HTTP method and a path. ```js import { HttpMethod } from "@wasp/types"; diff --git a/web/docs/tutorial/01-create.md b/web/docs/tutorial/01-create.md index a735e16678..96d31de7ea 100644 --- a/web/docs/tutorial/01-create.md +++ b/web/docs/tutorial/01-create.md @@ -5,7 +5,7 @@ title: 1. Creating a New Project import useBaseUrl from '@docusaurus/useBaseUrl'; :::info -You'll need to have the latest version of Wasp installed locally to follow this tutorial. If you haven't installed it yet, check out the [QuickStart](/docs/quick-start) guide! +You'll need to have the latest version of Wasp installed locally to follow this tutorial. If you haven't installed it yet, check out the [QuickStart](../quick-start) guide! ::: In this section, we'll guide you through the process of creating a simple Todo app with Wasp. In the process, we'll take you through the most important and useful features of Wasp. diff --git a/web/docs/tutorial/02-project-structure.md b/web/docs/tutorial/02-project-structure.md index 0160d5cebd..168e6ac85c 100644 --- a/web/docs/tutorial/02-project-structure.md +++ b/web/docs/tutorial/02-project-structure.md @@ -35,7 +35,7 @@ By _your code_, we mean the _"the code you write"_, as opposed to the code gener Many of the other files (`tsconfig.json`, `vite-env.d.ts`, etc.) are used by your IDE to improve your development experience with tools like autocompletion, intellisense, and error reporting. The file `vite.config.ts` is used to configure [Vite](https://vitejs.dev/guide/), Wasp's build tool of choice. -We won't be configuring Vite in this tutorial, so you can safely ignore the file. Still, if you ever end up wanting more control over Vite, you'll find everything you need to know in [custom Vite config docs](/docs/project/custom-vite-config.md). +We won't be configuring Vite in this tutorial, so you can safely ignore the file. Still, if you ever end up wanting more control over Vite, you'll find everything you need to know in [custom Vite config docs](../project/custom-vite-config.md). :::note TypeScript Support Wasp supports TypeScript out of the box, but you are free to choose between or mix JavaScript and TypeScript as you see fit. diff --git a/web/docs/tutorial/03-pages.md b/web/docs/tutorial/03-pages.md index ca0f9944ac..7c8052f10f 100644 --- a/web/docs/tutorial/03-pages.md +++ b/web/docs/tutorial/03-pages.md @@ -142,7 +142,7 @@ Now you can visit `/hello/johnny` and see "Here's johnny!" :::tip Type-safe links -Since you are using Typescript, you can benefit from using Wasp's type-safe `Link` component and the `routes` object. Check out the [type-safe links docs](/docs/advanced/links) for more details. +Since you are using Typescript, you can benefit from using Wasp's type-safe `Link` component and the `routes` object. Check out the [type-safe links docs](../advanced/links) for more details. ::: diff --git a/web/docs/tutorial/04-entities.md b/web/docs/tutorial/04-entities.md index 42ee9d68c2..2565c9d3d4 100644 --- a/web/docs/tutorial/04-entities.md +++ b/web/docs/tutorial/04-entities.md @@ -21,7 +21,7 @@ psl=} :::note Wasp uses [Prisma](https://www.prisma.io) as a way to talk to the database. You define entities by defining [Prisma models](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/data-model/) using the Prisma Schema Language (PSL) between the `{=psl psl=}` tags. -Read more in the [Entities](/docs/data-model/entities) section of the docs. +Read more in the [Entities](../data-model/entities) section of the docs. ::: To update the database schema to include this entity, stop the `wasp start` process, if its running, and run: diff --git a/web/docs/tutorial/05-queries.md b/web/docs/tutorial/05-queries.md index 748de193f9..2056a71e8b 100644 --- a/web/docs/tutorial/05-queries.md +++ b/web/docs/tutorial/05-queries.md @@ -5,7 +5,7 @@ title: 5. Querying the Database import useBaseUrl from '@docusaurus/useBaseUrl'; import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers'; -We want to know which tasks we need to do, so let's list them! The primary way of interacting with entities in Wasp is by using [queries and actions](/docs/data-model/operations/overview), collectively known as _operations_. +We want to know which tasks we need to do, so let's list them! The primary way of interacting with entities in Wasp is by using [queries and actions](../data-model/operations/overview), collectively known as _operations_. Queries are used to read an entity, while actions are used to create, modify, and delete entities. Since we want to list the tasks, we'll want to use a query. @@ -225,14 +225,14 @@ Most of this code is regular React, the only exception being the two< - `import getTasks from '@wasp/queries/getTasks'` - Imports the client-side query function. -- `import { useQuery } from '@wasp/queries'` - Imports Wasp's [useQuery](/docs/data-model/operations/queries#the-usequery-hook-1) React hook, which is based on [react-query](https://github.com/tannerlinsley/react-query)'s hook with the same name. +- `import { useQuery } from '@wasp/queries'` - Imports Wasp's [useQuery](../data-model/operations/queries#the-usequery-hook-1) React hook, which is based on [react-query](https://github.com/tannerlinsley/react-query)'s hook with the same name. - `import getTasks from '@wasp/queries/getTasks'` - Imports the client-side query function. -- `import { useQuery } from '@wasp/queries'` - Imports Wasp's [useQuery](/docs/data-model/operations/queries#the-usequery-hook-1) React hook, which is based on [react-query](https://github.com/tannerlinsley/react-query)'s hook with the same name. +- `import { useQuery } from '@wasp/queries'` - Imports Wasp's [useQuery](../data-model/operations/queries#the-usequery-hook-1) React hook, which is based on [react-query](https://github.com/tannerlinsley/react-query)'s hook with the same name. - `import { Task } from '@wasp/entities'` - The type for the task entity we defined in `main.wasp`. Notice how you don't need to annotate the type of the query's return value: Wasp uses the types you defined while implementing the query for the generated client-side function. This is **full-stack type safety**: the types on the client always match the types on the server. diff --git a/web/docs/tutorial/07-auth.md b/web/docs/tutorial/07-auth.md index 8e7ebde46c..8be57baaea 100644 --- a/web/docs/tutorial/07-auth.md +++ b/web/docs/tutorial/07-auth.md @@ -19,27 +19,19 @@ First, we'll create a Todo list for what needs to be done (luckily we have an ap ## Creating a User Entity -Since Wasp manages authentication, it expects certain fields to exist on the `User` entity. Specifically, it expects a unique `username` field and a `password` field, both of which should be strings. +Since Wasp manages authentication, it will create [the auth related entities](../auth/entities) for us in the background, that we don't have to worry about. However, we still need to add the `User` entity that will help us keep track of which user owns which tasks. ```wasp title="main.wasp" // ... entity User {=psl id Int @id @default(autoincrement()) - username String @unique - password String psl=} ``` -As we talked about earlier, we have to remember to update the database schema: - -```sh -wasp db migrate-dev -``` - ## Adding Auth to the Project -Next, we want to tell Wasp that we want to use full-stack [authentication](/docs/auth/overview) in our app: +Next, we want to tell Wasp that we want to use full-stack [authentication](../auth/overview) in our app: ```wasp {7-16} title="main.wasp" app TodoApp { @@ -63,15 +55,21 @@ app TodoApp { // ... ``` +As we talked about earlier, we have to remember to update the database schema: + +```sh +wasp db migrate-dev +``` + By doing this, Wasp will create: -- [Auth UI](/docs/auth/ui) with login and signup forms. +- [Auth UI](../auth/ui) with login and signup forms. - A `logout()` action. - A React hook `useAuth()`. - `context.user` for use in Queries and Actions. :::info -Wasp also supports authentication using [Google](/docs/auth/social-auth/google), [GitHub](/docs/auth/social-auth/github), and [email](/docs/auth/email), with more on the way! +Wasp also supports authentication using [Google](../auth/social-auth/google), [GitHub](../auth/social-auth/github), and [email](../auth/email), with more on the way! ::: ## Adding Login and Signup Pages @@ -216,7 +214,7 @@ export default SignupPage :::tip Type-safe links -Since you are using Typescript, you can benefit from using Wasp's type-safe `Link` component and the `routes` object. Check out the [type-safe links docs](/docs/advanced/links) for more details. +Since you are using Typescript, you can benefit from using Wasp's type-safe `Link` component and the `routes` object. Check out the [type-safe links docs](../advanced/links) for more details. ::: @@ -250,9 +248,9 @@ const MainPage = ({ user }) => { ```tsx {3} title="src/client/MainPage.tsx" -import { User } from '@wasp/entities' +import { User as AuthenticatedUser } from '@wasp/auth/types' -const MainPage = ({ user }: { user: User }) => { +const MainPage = ({ user }: { user: AuthenticatedUser }) => { // Do something with the user } ``` @@ -271,25 +269,31 @@ wasp db studio ``` Database demonstration - password hashing -We see there is a user and that its password is already hashed 🤯 + +You'll notice that we now have a `User` entity in the database alongside the `Task` entity. However, you will notice that if you try logging in as different users and creating some tasks, all users share the same tasks. That's because we haven't yet updated the queries and actions to have per-user tasks. Let's do that next. + + +You might notice some extra Prisma models like `Auth`, `AuthIdentity` and `Session` that Wasp created for us. You don't need to care about these right now, but if you are curious, you can read more about them [here](../auth/entities). + + + ## Defining a User-Task Relation First, let's define a one-to-many relation between users and tasks (check the [Prisma docs on relations](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/relations)): -```wasp {7,14-15} title="main.wasp" +```wasp title="main.wasp" // ... entity User {=psl id Int @id @default(autoincrement()) - username String @unique - password String + // highlight-next-line tasks Task[] psl=} @@ -297,7 +301,9 @@ entity Task {=psl id Int @id @default(autoincrement()) description String isDone Boolean @default(false) + // highlight-next-line user User? @relation(fields: [userId], references: [id]) + // highlight-next-line userId Int? psl=} @@ -515,8 +521,8 @@ You should be ready to learn about more complicated features and go more in-dept Looking for inspiration? -- Get a jump start on your next project with [Starter Templates](/docs/project/starter-templates) -- Make a real-time app with [Web Sockets](/docs/advanced/web-sockets) +- Get a jump start on your next project with [Starter Templates](../project/starter-templates) +- Make a real-time app with [Web Sockets](../advanced/web-sockets) :::note If you notice that some of the features you'd like to have are missing, or have any other kind of feedback, please write to us on [Discord](https://discord.gg/rzdnErX) or create an issue on [Github](https://github.com/wasp-lang/wasp), so we can learn which features to add/improve next 🙏 diff --git a/web/docs/typescript.md b/web/docs/typescript.md deleted file mode 100644 index 6f04d7277b..0000000000 --- a/web/docs/typescript.md +++ /dev/null @@ -1,521 +0,0 @@ ---- -title: TypeScript Support ---- - -import OldDocsNote from '@site/docs/OldDocsNote' - -# Using Wasp with TypeScript - - - -TypeScript is a programming language that brings static type analysis to JavaScript. It is a superset of JavaScript (i.e., all valid JavaScript programs are valid TypeScript programs) and compiles to JavaScript before running. TypeScript's type system detects common errors at build time (reducing the chance of runtime errors in production) and enables type-based auto-completion in IDEs. - -This document assumes you are familiar with TypeScript and primarily focuses on how to use it with Wasp. To learn more about TypeScript itself, we recommend reading [the official docs](https://www.typescriptlang.org/docs/). - -The document also assumes a basic understanding of core Wasp features (e.g., Queries, Actions, Entities). You can read more about these features in [our feature docs](/docs/language/features). - -Besides allowing you to write your code in TypeScript, Wasp also supports: - -- Importing and using Wasp Entity types (on both the server and the client). -- Automatic full-stack type support for Queries and Actions - frontend types are automatically inferred from backend definitions. -- Type-safe generic hooks (`useQuery` and `useAction`) with the accompanying type inference. -- Type-safe optimistic update definitions. - -We'll dig into the details of each feature in the following sections. But first, let's see how you can introduce TypeScript to an existing Wasp project. - -:::info -To get the best IDE experience, make sure to leave `wasp start` running in the background. Wasp will track the working directory and ensure the generated code/types are up to date with your changes. - -Your editor may sometimes report type and import errors even while `wasp start` is running. This happens when the TypeScript Language Server gets out of sync with the current code. If you're using VS Code, you can manually restart the language server by opening the command palette and selecting _"TypeScript: Restart TS Server."_ -::: - -## Migrating your project to TypeScript - -Wasp supports TypeScript out of the box! - -Our scaffolding already includes TypeScript, so migrating your project to TypeScript is as simple as changing file extensions and using the language. This approach allows you to gradually migrate your project on a file-by-file basis. - -### Example - -Let's first assume your Wasp file contains the following definitions: - -```wasp title=main.wasp -entity Task {=psl - id Int @id @default(autoincrement()) - description String - isDone Boolean @default(false) -psl=} - -query getTaskInfo { - fn: import { getTaskInfo } from "@server/queries.js", - entities: [Task] -} -``` - -Let's now assume that your `queries.js` file looks something like this: - -```javascript title="src/server/queries.js" -import HttpError from "@wasp/core/HttpError.js" - -function getInfoMessage(task) { - const isDoneText = task.isDone ? "is done" : "is not done" - return `Task '${task.description}' is ${isDoneText}.` -} - -export const getTaskInfo = async ({ id }, context) => { - const Task = context.entities.Task - const task = await Task.findUnique({ where: { id } }) - if (!task) { - throw new HttpError(404) - } - return getInfoMessage(task) -} -``` -To migrate this file to TypeScript, all you have to do is: - -1. Change the filename from `queries.js` to `queries.ts`. -2. Write some types. - -Let's start by only providing a basic `getInfoMessage` function. We'll see how to properly type the rest of the file in the following sections. - -```typescript title=src/server/queries.ts -import HttpError from "@wasp/core/HttpError.js" - -// highlight-next-line -function getInfoMessage(task: { - isDone: boolean - description: string -}): string { - const isDoneText = task.isDone ? "is done" : "is not done" - return `Task '${task.description}' is ${isDoneText}.` -} - -export const getTaskInfo = async ({ id }, context) => { - const Task = context.entities.Task - const task = await Task.findUnique({ where: { id } }) - if (!task) { - throw new HttpError(404) - } - return getInfoMessage(task) -} -``` - -You don't need to change anything inside the `.wasp` file. -:::caution - - - -Even when you use TypeScript, and your file is called `queries.ts`, you still need to import it using the `.js` extension: - -```wasp -query getTaskInfo { - fn: import { getTaskInfo } from "@server/queries.js", - entities: [Task] -} -``` - -Wasp internally uses `esnext` module resolution, which always requires specifying the extension as `.js` (i.e., the extension used in the emitted JS file). This applies to all `@server` imports (and files on the server in general). This quirk does not apply to client files (the transpiler takes care of it). - -Read more about ES modules in TypeScript [here](https://www.typescriptlang.org/docs/handbook/esm-node.html). If you're interested in the discussion and the reasoning behind this, read about it [in this GitHub issue](https://github.com/microsoft/TypeScript/issues/33588). -::: - -## Entity Types - -Instead of manually specifying the types for `isDone` and `description`, we can get them from the `Task` entity type. Wasp will generate types for all entities and let you import them from `"@wasp/entities"`: - -```typescript title=src/server/queries.ts -import HttpError from "@wasp/core/HttpError.js" -// highlight-next-line -import { Task } from "@wasp/entities" - -// highlight-next-line -function getInfoMessage(task: Pick): string { - const isDoneText = task.isDone ? "is done" : "is not done" - return `Task '${task.description}' is ${isDoneText}.` -} - -export const getTaskInfo = async ({ id }, context) => { - const Task = context.entities.Task - const task = await Task.findUnique({ where: { id } }) - if (!task) { - throw new HttpError(404) - } - return getInfoMessage(task) -} -``` - -By doing this, we've connected the argument type of the `getInfoMessage` function with the `Task` entity. This coupling removes duplication and ensures the function keeps the correct signature even if we change the entity. Of course, the function might throw type errors depending on how we change the entity, but that's precisely what we want! - -Don't worry about typing the query function for now. We'll see how to handle this in the next section. - -Entity types are also available on the client under the same import: - -```tsx title=src/client/Main.jsx -import { Task } from "@wasp/entities" - -export function ExamplePage() {} - const task: Task = { - id: 123, - description: "Some random task", - isDone: false, - } - return
    {task.description}
    -} - -``` - -The mentioned type safety mechanisms also apply here: changing the task entity in our `.wasp` file changes the imported type, which might throw a type error and warn us that our task definition is outdated. - -## Backend type support for Queries and Actions - -Wasp automatically generates the appropriate types for all Operations (i.e., Actions and Queries) you define inside your `.wasp` file. Assuming your `.wasp` file contains the following definition: - -```wasp title=main.wasp -// ... - -query GetTaskInfo { - fn: import { getTaskInfo } from "@server/queries.js", - entities: [Task] -} -``` - -Wasp will generate a type called `GetTaskInfo`, which you can use to type the Query's implementation. By assigning the `GetTaskInfo` type to your function, you get the type information for its context. In this case, TypeScript will know the `context.entities` object must include the `Task` entity. If the Query had auth enabled, it would also know that `context` includes user information. - -`GetTaskInfo` can is a generic type that takes two (optional) type arguments: - -1. `Input` - The argument (i.e., payload) received by the query function. -2. `Output` - The query function's return type. - -Suppose you don't care about typing the Query's inputs and outputs. In that case, you can omit both type arguments, and TypeScript will infer the most general types (i.e., `never` for the input, `unknown` for the output.). - -```typescript title=src/server/queries.ts -import HttpError from "@wasp/core/HttpError.js" -import { Task } from "@wasp/entities" -// highlight-next-line -import { GetTaskInfo } from "@wasp/queries/types" - -function getInfoMessage(task: Pick): string { - const isDoneText = task.isDone ? "is done" : "is not done" - return `Task '${task.description}' is ${isDoneText}.` -} - -// Use the type parameters to specify the Query's argument and return types. -// highlight-next-line -export const getTaskInfo: GetTaskInfo, string> = async ({ id }, context) => { - // Thanks to the definition in your .wasp file, the compiler knows the type of - // `context` (and that it contains the `Task` entity). - const Task = context.entities.Task - - // Thanks to the first type argument in `GetTaskInfo`, the compiler knows `args` - // is of type `Pick`. - const task = await Task.findUnique({ where: { id } }) - if (!task) { - throw new HttpError(404) - } - - // Thanks to the second type argument in `GetTaskInfo`, the compiler knows the - // function must return a value of type `string`. - return getInfoMessage(task) -} -``` -Everything described above applies to Actions as well. -:::tip - -If don't want to define a new type for the Query's return value, the new `satisfies` keyword will allow TypeScript to infer it automatically: -```typescript -const getFoo = (async (_args, context) => { - const foos = await context.entities.Foo.findMany() - return { - foos, - message: "Here are some foos!", - queriedAt: new Date(), - } -}) satisfies GetFoo -``` -From the snippet above, TypeScript knows: -1. The correct type for `context`. -2. The Query's return type is `{ foos: Foo[], message: string, queriedAt: Date }`. - -If you don't need the context, you can skip specifying the Query's type (and arguments): -```typescript -const getFoo = () => {{ name: 'Foo', date: new Date() }} -``` - -::: - -## Frontend type support for Queries and Actions - -Wasp supports automatic full-stack type safety à la tRPC. You only need to define the Operation's type on the backend, and the frontend will automatically know how to call it. - -### Frontend type support for Queries -The examples assume you've defined the Query `getTaskInfo` from the previous sections: - -```typescript title="src/server/queries.ts" -export const getTaskInfo: GetTaskInfo, string> = - async ({ id }, context) => { - // ... - } -``` - -Wasp will use the type of `getTaskInfo` to infer the Query's types on the frontend: - -```tsx title="src/client/TaskInfo.tsx" -import { useQuery } from "@wasp/queries" -// Wasp knows the type of `getTaskInfo` thanks to your backend definition. -// highlight-next-line -import getTaskInfo from "@wasp/queries/getTaskInfo" - -export const TaskInfo = () => { - const { - // TypeScript knows `taskInfo` is a `string | undefined` thanks to the - // backend definition. - data: taskInfo, - // TypeScript also knows `isError` is a `boolean`. - isError, - // TypeScript knows `error` is of type `Error`. - error, - // TypeScript knows `id` must be a `Task["id"]` (i.e., a number) thanks to - // your backend definition. - // highlight-next-line - } = useQuery(getTaskInfo, { id: 1 }) - - if (isError) { - return
    Error during fetching tasks: {error.message || "unknown"}
    - } - - // TypeScript forces you to perform this check. - return taskInfo === undefined ? ( -
    Waiting for info...
    - ) : ( -
    {taskInfo}
    - ) -} -``` - -### Frontend type support for Actions - -Assuming the following action definition in your `.wasp` file - -```wasp title=main.wasp -action addTask { - fn: import { addTask } from "@server/actions.js" - entities: [Task] -} -``` - -And its corresponding implementation in `src/server/actions.ts`: - -```typescript title=src/server/actions.ts -import { AddTask } from "@wasp/actions/types" - -type TaskPayload = Pick - -const addTask: AddTask = async (args, context) => { - // ... -} -``` - -Here's how to use it on the frontend: -```tsx title=src/client/AddTask.tsx -import { useAction } from "@wasp/actions" -// TypeScript knows `addTask` is a function that expects a value of type -// `TaskPayload` and returns a value of type `Promise`. -import addTask from "@wasp/queries/addTask" - -const AddTask = ({ description }: Pick) => { - return ( -
    - - -
    - ) -} - -``` -#### Type support for the `useAction` hook -Type inference also works if you decide to use the action via the `useAction` hook: -```typescript -// addTaskFn is of type (args: TaskPayload) => Task -const addTaskFn = useAction(addTask) -``` - -The `useAction` hook also includes support for optimistic updates. Read [the feature docs](/docs/language/features#the-useaction-hook) to understand more about optimistic updates and how to define them in Wasp. - -Here's an example that shows how you can use static type checking in their definitions (the example assumes an appropriate action defined in the `.wasp` file and implemented on the server): - -```tsx title=Task.tsx -import { useQuery } from "@wasp/queries" -import { OptimisticUpdateDefinition, useAction } from "@wasp/actions" -import updateTaskIsDone from "@wasp/actions/updateTaskIsDone" - -type TaskPayload = Pick - -const Task = ({ taskId }: Pick) => { - const updateTaskIsDoneOptimistically = useAction( - updateTaskIsDone, - { - optimisticUpdates: [ - { - getQuerySpecifier: () => [getTask, { id: taskId }], - // This query's cache should should never be empty - updateQuery: ({ isDone }, oldTask) => ({ ...oldTask!, isDone }), - // highlight-next-line - } as OptimisticUpdateDefinition, - { - getQuerySpecifier: () => [getTasks], - updateQuery: (updatedTask, oldTasks) => - oldTasks && - oldTasks.map((task) => - task.id === updatedTask.id ? { ...task, ...updatedTask } : task - ), - // highlight-next-line - } as OptimisticUpdateDefinition, - ], - } - ) - // ... -} -``` - -## Database seeding - -When implementing a seed function in TypeScript, you can import a `DbSeedFn` type via - -```ts -import type { DbSeedFn } from "@wasp/dbSeed/types.js" -``` - -and use it to type your seed function like this: - -```ts -export const devSeedSimple: DbSeedFn = async (prismaClient) => { ... } -``` - -## CRUD operations on entities - -For a specific [Entity](/docs/language/features#entity), you can tell Wasp to automatically instantiate server-side logic ([Queries](/docs/language/features#query) and [Actions](/docs/language/features#action)) for creating, reading, updating and deleting such entities. - -Read more about CRUD operations in Wasp [here](/docs/language/features#crud-operations). - -### Using types for CRUD operations overrides - -If you writing the override implementation in Typescript, you'll have access to generated types. The overrides are functions that take the following arguments: -- `args` - The arguments of the operation i.e. the data that's sent from the client. -- `context` - Context containing the `user` making the request and the `entities` object containing the entity that's being operated on. - -You can types for each of the functions you want to override from `@wasp/crud/{crud name}`. The types that are available are: -- `GetAllQuery` -- `GetQuery` -- `CreateAction` -- `UpdateAction` -- `DeleteAction` - -If you have a CRUD named `Tasks`, you would import the types like this: -```ts -import type { GetAllQuery, GetQuery, CreateAction, UpdateAction, DeleteAction } from '@wasp/crud/Tasks' - -// Each of the types is a generic type, so you can use it like this: -export const getAllOverride: GetAllQuery = async (args, context) => { - // ... -} -``` - -## WebSocket full-stack type support - - -Defining event names with the matching payload types on the server makes those types exposed automatically on the client. This helps you avoid mistakes when emitting events or handling them. - -### Defining the events handler -On the server, you will get Socket.IO `io: Server` argument and `context` for your WebSocket function, which contains all entities you defined in your Wasp app. You can type the `webSocketFn` function like this: - -```ts title=src/server/webSocket.ts -import type { WebSocketDefinition, WaspSocketData } from '@wasp/webSocket' - -// Using the generic WebSocketDefinition type to define the WebSocket function. -type WebSocketFn = WebSocketDefinition< - ClientToServerEvents, - ServerToClientEvents, - InterServerEvents, - SocketData -> - -interface ServerToClientEvents { - // The type for the payload of the "chatMessage" event. - chatMessage: (msg: { id: string, username: string, text: string }) => void; -} - -interface ClientToServerEvents { - // The type for the payload of the "chatMessage" event. - chatMessage: (msg: string) => void; -} - -interface InterServerEvents {} - -interface SocketData extends WaspSocketData {} - -// Use the WebSocketFn to type the webSocketFn function. -export const webSocketFn: WebSocketFn = (io, context) => { - io.on('connection', (socket) => { - socket.on('chatMessage', async (msg) => { - io.emit('chatMessage', { ... }) - }) - }) -} -``` - -### Using the WebSocket on the client - -After you have defined the WebSocket function on the server, you can use it on the client. The `useSocket` hook will give you the `socket` instance and the `isConnected` boolean. The `socket` instance is typed with the types you defined on the server. - -The `useSocketListener` hook will give you a type-safe event handler. The event name and its payload type are defined on the server. - -You can additonally use the `ClientToServerPayload` and `ServerToClientPayload` helper types to get the payload type for a specific event. - -```tsx title=src/client/ChatPage.tsx -import React, { useState } from 'react' -import { - useSocket, - useSocketListener, - ServerToClientPayload, - ClientToServerPayload, -} from '@wasp/webSocket' - -export const ChatPage = () => { - const [messageText, setMessageText] = useState< - // We are using a helper type to get the payload type for the "chatMessage" event. - ClientToServerPayload<'chatMessage'> - >('') - - const [messages, setMessages] = useState< - // We are using a helper type to get the payload type for the "chatMessage" event. - ServerToClientPayload<'chatMessage'>[] - >([]) - - // The "socket" instance is typed with the types you defined on the server. - const { socket, isConnected } = useSocket() - - // This is a type-safe event handler: "chatMessage" event and its payload type - // are defined on the server. - useSocketListener('chatMessage', logMessage) - - function logMessage(msg: ServerToClientPayload<'chatMessage'>) { - // ... - } - - function handleSubmit(e: React.FormEvent) { - e.preventDefault() - // This is a type-safe event emitter: "chatMessage" event and its payload type - // are defined on the server. - socket.emit('chatMessage', messageText) - setMessageText('') - } - - return ( - ... - ) -} -``` \ No newline at end of file diff --git a/web/docusaurus.config.js b/web/docusaurus.config.js index b547a1f75e..2df5ea3ef5 100644 --- a/web/docusaurus.config.js +++ b/web/docusaurus.config.js @@ -56,12 +56,16 @@ module.exports = { }, items: [ { - to: 'docs/', - activeBasePath: 'docs', - label: 'Docs', + type: 'docsVersion', position: 'left', + label: 'Docs', className: 'navbar-item-docs navbar-item-outside', }, + { + type: 'docsVersionDropdown', + position: 'left', + className: 'navbar-item-docs-version-dropdown', + }, { to: 'blog', label: 'Blog', @@ -101,11 +105,7 @@ module.exports = { { label: 'Todo app tutorial', to: 'docs/tutorial/create', - }, - { - label: 'Reference', - to: 'docs/language/features', - }, + } ], }, { @@ -137,10 +137,10 @@ module.exports = { appId: 'RG0JSZOWH4', apiKey: 'feaa2a25dc596d40418c82cd040e2cbe', indexName: 'wasp-lang', - // TODO: contextualSearch is useful when you are doing versioning, - // it searches only in v1 docs if you are searching from v1 docs. - // We should enable it if we start doing versioning. - // contextualSearch: true + // ContextualSearch is useful when you are doing versioning, + // it searches only in v1 docs if you are searching from v1 docs. + // Therefore we have it enabled, since we have multiple doc versions. + contextualSearch: true }, image: 'img/wasp_twitter_cover.png', metadata: [{ name: 'twitter:card', content: 'summary_large_image' }], @@ -156,13 +156,44 @@ module.exports = { docs: { sidebarPath: require.resolve('./sidebars.js'), sidebarCollapsible: true, - // Please change this to your repo. editUrl: 'https://github.com/wasp-lang/wasp/edit/release/web', remarkPlugins: [autoImportTabs, fileExtSwitcher], + + // ------ Configuration for multiple docs versions ------ // + + // "current" docs (under /docs) are in-progress docs, so we show them only in development. + includeCurrentVersion: process.env.NODE_ENV === 'development', + + // Uncomment line below to build and show only current docs, for faster build times + // during development, if/when needed. + // onlyIncludeVersions: process.env.NODE_ENV === 'development' ? ["current"] : undefined, + + // "versions" option here enables us to customize each version of docs individually, + // and there are also other options if we ever need to customize versioned docs further. + versions: { + ...( + (process.env.NODE_ENV === 'development') ? { + "current": { + path: "next", // Token used in the URL to address this version of docs: {baseUrl}/docs/{path}. + label: "Next", // Label shown in the documentation to address this version of docs. + noIndex: true, // these are un-released docs, we don't want search engines indexing them. + } + } : {} + ), + // Configuration example: + // "0.11.1": { + // path: "0.11.1", // default, but can be anything. + // label: "0.11.1", // default, but can be anything. + // banner: "unmaintained" + // // and more! + // }, + } + + // ------------------------------------------------------ // + }, blog: { showReadingTime: true, - // Please change this to your repo. blogSidebarCount: 'ALL', blogSidebarTitle: 'All our posts', postsPerPage: 'ALL', diff --git a/web/sidebars.js b/web/sidebars.js index f2ae06c26a..f9e1decfd3 100644 --- a/web/sidebars.js +++ b/web/sidebars.js @@ -6,8 +6,8 @@ module.exports = { collapsed: false, collapsible: false, items: [ - 'introduction/what-is-wasp', - 'introduction/getting-started', + 'introduction/introduction', + 'introduction/quick-start', 'introduction/editor-setup', ], }, @@ -67,6 +67,7 @@ module.exports = { 'auth/social-auth/google', ], }, + 'auth/entities/entities', ], }, { @@ -103,7 +104,7 @@ module.exports = { 'advanced/deployment/manually', ], }, - 'advanced/email', + 'advanced/email/email', 'advanced/jobs', 'advanced/web-sockets', 'advanced/apis', diff --git a/web/src/components/Required.tsx b/web/src/components/Required.tsx deleted file mode 100644 index 17a4f2c8eb..0000000000 --- a/web/src/components/Required.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react' - -const color = '#f59e0b' - -export function Required() { - return ( - - required - - ) -} diff --git a/web/src/components/Tag.tsx b/web/src/components/Tag.tsx new file mode 100644 index 0000000000..1724c49180 --- /dev/null +++ b/web/src/components/Tag.tsx @@ -0,0 +1,37 @@ +import * as React from 'react' + +export const Tag = ({ + color, + children, +}: React.PropsWithChildren<{ + color: string +}>) => { + return ( + + {children} + + ) +} + +// Used to mark something as internal to +// Wasp and not to be used by the user. +export function Internal() { + return internal +} + +// Used to mark something as required e.g. required +// fields in Wasp file. +export function Required() { + return required +} diff --git a/web/src/css/custom.css b/web/src/css/custom.css index 36935dbbea..1b504d6459 100644 --- a/web/src/css/custom.css +++ b/web/src/css/custom.css @@ -132,3 +132,30 @@ font-weight: bold; font-size: var(--ifm-h6-font-size); } + +/******** DOCS NAVBAR *********/ + +/* This hides Docs version dropdown when not on docs (i.e. when we are on blog). */ +.navbar__item:has(.navbar-item-docs-version-dropdown:not(.active)) { + display: none; +} + +/******************************/ + +/******* OTHER ********/ + +.video-container { + position: relative; + width: 100%; + padding-bottom: 56.25%; + margin-bottom: 1.5rem; +} + +.video-container iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; +} diff --git a/web/static/img/auth-entities/model-example.png b/web/static/img/auth-entities/model-example.png new file mode 100644 index 0000000000000000000000000000000000000000..cd41e4107e0df6a3283b01da85df9b7faa4116d6 GIT binary patch literal 175631 zcmeEuXIPZkvaX^Sz)VuIgP>%UoG}q4NKOq(l+;MhAmheBR&qwkS+X=y$&zzunkGY& zLqn7Mt`En3&OXDQ@yGpf?tS);ab_C%zO|}q)v9{mxB8u;yySTzN}^-Oj-8j5dZ={l z*y*HW#|ZPzoC2TR$!3!S|2l4~BzgZ>P8&4_{Lk4JQku5Mj*&A${~VWAy0LQX82vHn zhxb&RbQktdhp5->iyc%g@c2xVaQEdBnJqA$;@0@A?@z@0`B@#o09oEMf7oa9h2InO zznPW|Um|FP9!Q2r+v{&R5tnW6p@4F3s+{{+K7E%5()FoayXDzs_Jn=O^_;7vgG zEIhR}nVyl{dln;;=MQ zB=ssx#*JQo;Bz+O-3QJD84xU(QfMO6)AOIrC0xEY2sH=`l)TzXMx2Nyw99TZdM2pM|cZ2 zzLq}n`N9O4mRV96)*of1`1;wT^(5un+1%tHbF&;-)7^eWbdHk$_Nnuf8tW*zo%5=}Eq=D;Hi53iEwNC8_+s1pJQvGm(!SXmo(^ykSCcjo<0QbC9TYuzbM)R76Hs?$ z)v4{{McoNI$wF12W9M~E^=X&v>0sTi#)5*z8&sJ|gk4;7%kOr7JF18lXYevpliVF2 ze)ZtcT9m{p>wJYFNaS+4Z=u&qvM$=QM{c6!B|)!S4D^RF$M~Vk7p$shi zkso%ELlJuPD8s;{fFlj6NbMz~_X(L%#&2tMRT?CkM(N+nGBVETt znuaAraJEEW(pHCl?RE`O5?6|=9Xfe#cO9on58J5u^xM&4pD$i!6viRsrtMQaImqJx z?I|LRAoL(bUVz-IP~1gJG&<+#c*HO%E;X-%lPR(DP3QU#8?q3q4A~ogr=@<^InaHr zD>VKN&ygGL7zZUXJN)~(2*`g#EVrMJR$t|Q{!}%QM2a{m(?wC`Xs5`Mr`e4pb2e+Q zEl>(U8+vRG45Xe)9axn|fHB>7L(m?%&6HB{WX?8sP0`lWTEfEvK}f8!Q^pd+Ay!(v z0RMWi>Lba2OU?f@GQDg<7sU*LY5V!Z$EL|83_rO_VCrROsSrH$(L!3U=6C9H5#ljm$M&)(FlU?31 zHhFP zEdFC7-lS=yQWt6O-Vn5#)o9G4I{dXuw?4fT7sO|Qjr^~$|7>T4s~^p-U7gTucd8W@ zgDlan=u^2zkBTxzRvhKQ4zrWrvnOp{+hsbR8@>fug8Mz#kyz4ScqI#X<+HNEkK2>r z&?|C(;T1LDm4Wm-w-3GIb?6nQzwinx@XA8Z)vJeI5r@1Y{a0QgyCg;7V0P~Cs@6(? zoRI|Xm4D}qZ_r^7;yHDg<=7RW!_f5?hv6A?7-A_;FdQC+ThL)J{foo!8Eo#Gyua6v z!$1lh2JXK&3>si_&r<8Fe;fv|vUP>1|Ck-*k^f=#pSip4Jr2{&w}V~O&eJu|pbvt} z6)fr=m1myw07s~C^}<7-lbqrO1$6#@Ec<86{(V4C5nxKt&Ly8Gcn{Anz2ZaYL~36C zi#+-N&x!P-5#n_LHO_eBE*3-OzO$LFk;|icNvb4e&>{Dz8>^5KSSK^mj86u-@w&x9 zexcL+e=RkFJAI-dFmOJnf>$Dd`r3LcpPoe-syJ_V$+ecPphc;1m6FuUlpVwlC&w@3 zaj*MCF)#L3?-(|Yx)=y;hbVt?#nF0G=ia*<(Fa`yqN|!HZ{paX6bu|}7R2)MGjicF(qb)Zh?E5tJ zmr7^eInTuMO-DM$MY!ga&L0TPP}|mL6|_a6K1i9oQF<#1LRzK3Gd1Ga8H^ud6kT~u<|bFYjSPqRw1L|l$7TCOOD->THWKn;7RK82BsZzOk}B6EE3tp_}Q32R5(4pz{%hWr|Sb zCMw_>7n1|BNOIa+GJuk7ai}PM^Oemb0>Z=c_|h+2!ck8WOxi^zRvT!|>scRK6!rF5 zf35avb@4YvZO1weUw%X9p)qX~Pq}VaXo+;ty4bXFhEyJ^A7p;TE)rg6Ne(Wn>slM> zcx)TB+Jrv1Ai}*l;u;@eo>Ll?jK}n1lEZbb%*46MPfzK&?zi7*ptgOS`>~Y6>l+l` z3f{k9oo?~b20Bl_K+&CB2M4&Sx_@{+etQ-la|wSihrY9le0RrUw&SXphg;&4y|wTv zzw^AaI?FS*?Ocv*rIR1>CO_ag)%4sqo_D9Ta$j|~43F4qlZT^ zueCC06PS5x6lO}%%u#01a<`d{OlYBhIpWLIbTn0Ov`L)v_>^_7ibXDgSKn`nOIGtY zUIA=GBEHgHn^;;E#~hU7Bh3gEf`4ib3fVyFY>whgjCCMDA?*Fbb=~qh3Crv-S14d^ zO@&mADms{GtIBtsiF5%%%jI0O9ONwcR(iy5n_k35s_1Tu_u)- z#S&n$@*8Mf>sN2a1eqJgyKid7*nDrYUKYWHSfFu14?a0AiqjF(kpC>9)IAlGo+KYUtrGyxPzZ=en7N8zkl|zcdr?2u~76s0d(^j_1w8%X)?7 z&z~WMB}9q4tOQ-}{P>{Z6Z$(hO}w~Elug-T{r)F6r})1VcNyX@lWD%<1YYy0**Mzh z!p>WUKp))-mtMCA3c?3Y)OH>i`mTy>4pd~A z8`^8ZH*mnWbW~P_&!#otU#wqbRcv}T2w0ET@^4Qbk2I0vvnXG3nXlvN*U{eCPnSwa zhE7szh`3;UwAxW2cz2AsYnnXj&p~C-Y6=Q%@2$6G zB8vqTN>{%5>aVjGrww4smMeYF3t3$Pmu9X;iHf(ff>1WRc_mA!Z4#5$UW?yHlVj)7h9?*xy8o1qklU4o~z3i9dm2&{*PEV(`X`85+mhu{U&K&KsFIm4bbGp06?_ zc%Oay$3W&xoW04T8TP8ZDb9(p&cIsX^%mt~U+_}a3z+B%ltazy^!DU%UBQF(czi9$ z5jwb4a&7pvH?%GqEqrLV$o^}*cF(cPo)(GiN_qzI&b&)io+{K%)yH{I)B6{NHTW)g zSNlO14PPPw#tp6oH8_82k^#gDU^#zURDt?~lzUbCZDrmxWnPz`%X_irmjBkaH$lh6 zY26UZpr0J7mNB<>n$GU2AGJV!24ae3W7$&&J8YhtzaQf;ly2SHs~(;eyfa(4gvcAv z_pQ^hZB<>5XcJs-X;B@Ch9fi=3fJZH9KBcNoWFw5z2Snx(6Huc#2U1|>U~*l2cZzr z$Gq%B`Ld5lGyvNC*?6TCh?l_&XoO5S9r_7*t^}w?X>mr&0WdW`e@I0nkKPDlV_@Sm&yh_$}A^< ze;Tub35Hm6%I^Z?gnxTIP6X%IKo;)5S1VbsnG=?Vvd!%<`OcsMC^keh>!8h=*;xXtzZ037FC_*FFd2hH z8#R>v_^HGtC53B!lKNF+*SM(feyWClAIi_HVCav481j`iiJyn(R8C@mK# znQ-wfJY6j651@im>ue5zi&_U;>g4#&H=XNk=FxSm*w7!XzIO-h zw%(iRFqj-(FEA4W+u-NC%743f$ZS4iK2z$c1XP|yKlwE~rHc#3mI`l1n#cw$4VuLJ z!yHDK*VliRn89MMGm9mOt~eMdpexgM;H;KN)aS$!vzXI$n%iZk$@52o$T&%q_nXC~~0T_hr4!X}JP- zcejZM)f}4+gWo<)WA|dA2sd&EzqdX^Ss-yzD+4;EEx&e3T^6f6g||jeL?%Yu)qrb! z)S-7@V@`cI)U`VmuItm8C>dT}Ln>HeS*F-DZxZ_e#@Y`|M}=}6ES(G+v8q~!S8xdJ z!Ly^(Y!n9hy|;G3s74 z?eblCnSAf8RfWA%`OaLXb(i!_w}8cO~V0lOTu=K5b-^GuAbgN_3xIWqC;ej`KP*0`*p$0oRrAI_ZTZ4}w@*bnfp zzzyCM-I+}sc)GtKvl(3cO%(gNhRJgzTf5&0Q%5o1i`H*uPXy^EukknE+IBI?$6F0z zbH6X$F^FR+8!D9z=XkI7Oo0Wz-R`MhN9GYM5C+PwThmtEOzLOQ>B_16^H~Lni}#7A zHoJbMboj6I7o;Ycs@kPFwdA_wKNYT5IeF$H$PrfsYz$i+I}#-WufAAg4n{7PwT~E; zY|3XQQE0oZLY;!$&b}azUarckZUr$0hhfvyCc1F#>+Y+J;q{=&C5Q3^byta1lWn>z zEECYMcbpo7n5fDE&X7KWEX(w3mZfuBxo0sT;F@eZq~u!EtIjF#O+jq?2JfKZ#MaJy zZf2#gLEs{t2yTegILb7o$uiEvdV+`glhl!W)7ic(st2Zi^O<6 zP-%R!ooUfBA}9}S4H)WxGw-HW};bsX{{^M(N^^VLUx%Y zYy5`HPc^+W)FE7VrQX-rsZ6I>cVVYxNd;n-Z+J7WX8i^D9e9dPlT7Zu-yt1 zajr`NRrvkoRV4a=SI}OIZyJ1JDu~dAmoo0q*xDtU|Gb!jJr7nJ=o-LY)omWx*Ieil z7=V>I_|iBG-w8ufYoFioJ^1Uk;otK9b z-Bp?UHgi%d2Fkc0zmRbyp5|ABevouOK!EA>#VY%?_5h)StpVHixJVfc+f2W%E4!}e zei1(Bdz|}bgnRLz!TU!P{*NeN+~k^u?-5+lslvGkt))V;ZE%}iS_BPuW){M%)!hxh zk9)rpaeIs|Z!ROV4m6^gi^;tV9iO0LC`I<~i=o>D{wEK1`l}>{zPY9+hxrgv=mcFl zPC$5AG*kaVJpEuRAjn1zP)wge^$GV*G>?1*x`o3$F!0Y%&`kbi(5UQu+go%TVP6_s+!P+?&pS^ zNb$vqc-cLPe?D<^#~=>~{RrYD+izxvoL06L_&s$myUmH~LGa!xUU=LQ3-r471hCH2 z1ej~qeJB#h0^B9y$yK=!?`$jfYUkz=-Q7#^>nB;3pg$N-i2mmT^#5P9{QsTtJj@}0{{P#?`+s2$ z`M-Xo&m=Ka0<4E0H*Bk48|&D&kN4cFDekP04Ak%d@OmpUHQs~rCbg%V-?eoW&{6X8 zg8Fp8eA!+9DoAnCbOJJ_b<$ z|C{3M^3Q^IWBU1+H$}i{TDMyOkjrrqTXt;egn)*g-N6e&OIJ-|RvW&x<$3KZjOKdo zn_Cn;o-ES|7?Im%K@6-i*)7mAiGF z4b-m!phb)BPX=q4#4gaBigPi0TnKyqu}P%zV8=YKwNIN5@y#qVm$qa?u)Lm?q6z`> zJM~}YcMuF}Fr%rKpzWS+d0$X^BjYPht*nDPo{DjcVLcw@`Glw~MS5co9E1Xg=i*VTM`juN@ z)leB1?KsG0tH>bf@8hsIq;RkR>Sy2J)90S6sMDkJG=c?VB^iR^+DJy%j%Z$iXvPd2 zgNHz`)n+I%{+40BjUrAZ&S`HI)GZ=+=POsI_4NBRO|tn#5+(e7l;vqVo;MFEozJZr zs|S{{XW7iWZ%<%yuo`GR6{^miQQo5@+iTgG19Dz;bu(ubHLL{9QPBB0+A3Y>ri+pQ zzI=TgH0x$6C4aOILma~dX4=u^Y-+jE@9*Q#4OU@#9JU@n4*;lHZ0z13%n?QQ zhIXwAFJhEfGUAcCz>B1F-+D&vsDpPJe|OW&g`8Z^?cG(tvEA8SDCqBzpR({eenQ@d zDyOQl^9`nDZ6sy&+FkcgeU|v&s}|CgB`H%Z=y@4=OT>KoPutBfrg2Ce_xct z;`>r|C!UM-?hKNTb~o|J&EhEDIYVOVKiUVro!geIF~t;WXD+45J;ly;<}Hi5n(!h3 zVK+}B^kVe*>Dql9-+{>dA84gn*$d)ynAzz7=^6#rnrp-qgmop>K>l6XsMJn=N6@Dz^Z*T$N?NWJnBj-dCN`?yDU&7wb-Y?$eKW>}Dh4Qx|iPoMje; z+9@!=3##SKnGLeGnIkl~R7U^+2$3hxdv@oYVA9#{dAS) z7#~#S_c=4maRPu%?4e8B49LL4v2+fz*YEr`fv(a!SPNgp4wx|Emq7o9e@CXiB{yxq ziLO$ocJ6sn+Mqc3ngBCoB1sP`izGkt$lodyH@}$ViWB9Bm@Em~kZ&P*?ZQIaGp39y zHqFLX(G_Tq-G(QUbgOQYYGKc^U z(QQLFx6Q$sya52ryXVT?NjXcxRC_Hop0?%Rq6KhRj%oD2|O=* zQIAqy?G@Sju4XnGKwv(~s@$0))2o%5kdvfMTIFSf8d9!yLG!i1?xJpDK}|->{@CKh zxXZrch)qp0seX70;gJWHh`l4~1jgo>3=zG8=u}9PZMfPA$3csNjGB;wHes%uFXL+; zY~$5b>5%vV7n3)-Hmh|I^3i8r0HNqBNVpa0xy8bX3#)tU?Fu>31icWcM6Q+QaRz)K z*6>zn*$)_vUUYBeRm*CKw%7iEL16uRN$SgbZmW;v+O3JObZ)a_TXmcsPn64~L#sfo z;{=*~#OMNGKLRd_Zke~BQ{=L$4t6@Tkr!4$0z^!EGz8Mo#d=i9w=OdANO_@Pj%X6w zHUX0+QBeSD@=y46PqYawrR@Nwhf6^R$UM4@v^TN&U(=o>>JNPWxC*oueN?tCDe^-5 zeh9Mf%F12M*8sq5pJ-yM6pvwiXAmHoG0qjH){zog*qxZ9x&$yx?O|Rwr-~JxR+!62 zfJhXyhvgb)VI1vx8sA%^?EC8r+7Ff)BbbpX=v1R{7;jf!AM+QO#cEw92B8~^9hY-C zo;vhFR|8Z0pafVz!~oc^)nXIet988ERs$Vn{oWIt>qxiCy;vWgMZ0Tc*6Lf9;d%0$2#$`GDTT{*?G( zJ?!c(8*^lm)PqiC!(~ed)(av@R!cLEKuz)vq9;=}O+%OJC}5-@@ya!2w*gG3^^583 z=O}gm6xC*ob9|J|e0B{(tDc%OkpCFqP!8Ewvy6-6ye86imjO#e9uYOBisH>Cr}LQ4 zww_B*4M8c1YF;ec4$IiJ)K~?$PvF;Ar~CK$33mW{FUBn0o7s-MoG3y3UVuUL0)m6v=TC zQ#)*3&KrA8rDhRMusjpz&U--w8zjYP)T7mP;rJ_#@>_j?;Fpl2^Vk|2utK}vEu&t? z4s7-2dhl!){JiL&pp+zOdj#7)^-%!taQ&Tk_5(fl82WY=qaxSWhu7QtiBEU)mhA7) zbf>|IUN2Rq9)zL)Nz{2LuQ>I?98KVFV%n~axXfqI5wwyL#5#3J+asm;*gW^=^$Zfv z7nGE^J7F21oKRc)jwI0vExNk$;G(MA0p+Q|^n2Nv%(Ac>*|!c_t@7LbTM@aU6|N2D zxfNR0EkM;0=&%^}Zi8=@8fjJKeki{{4m;JHhtP7hEcKlG>7%Q?>Uw~Kb&Skd|5k#o zG>@_098~71G^6y~95m@n#eHQ|pWt0cR8E!88(wSf&vt+Th%&SEAuQHT%~7zx_4@}k z)Cj)%#2wI$)HQQfkiyE~diI{_`i{=?9HNFMn6t_PK+aNi$wlbnL#x;Qsgr+`&HQm>jtG@2c%c*`5#0Z`^eclJKL>c4D z;zQ$tglf(*fiOpZ5K=h<+!cyJ_v_P{bIW(M5(^OBF_!tO5~XVK@&aLn1Y1B1^SE_| zrC2T#hZ}ImeL$9|&(UYUSn5iaO*XTS4|h2}8XzPIY0m&Nz?-{$O}5>^r9Qe$$7w9# zs?%4(QjTD?y$e-~#Xcso%N6~)Wa_%=LN70aLYFn`v%c|$U5~h+O#-vbj)-iPRj^EY z;<RB{?pan(weNnK3)F5ue9gG*ngSvjvSnE%G)nOyvL69vFBu}zF8LUuGTei&W z9H4Y-tNO%6UahY&_eUX`txGdMSVrr-b8~jhe0Ey};C9D@DhH7|bgmY7(6UxUPdZB( zGznTc;83`?JbgNelUFHqg|k0D_QEazmECrt($~%;B2#-Cbfvq2H<@}C@&gV~c<)Ga zj6w_7?qt7YQ|)s21}a0Vj925NHaUe|GDep8&#ki$c-0tRLSY(DdcTD(Lja9R-Hlk*^FDgU8iodbY+jZ!> zyOMPKL5OxhYWwS9?!wL-LQI<1QrUuz6&t|HJ*n-nopRHPwE-Zw+kM0l4qWVz5B`J# zDL#kF@zkcs+t2QU@^m1=K1_4I%{(siA^jHA`zR}jTq8NSBf-w4_EQURom|td6TziQ z1t)VK`1q#w?PZ3$Gv#Bvqsr?~V_cU6tTBw?+;hKqr|EfYKdK8&WqvHS_m+Vw)^!(L zs}dz}nCykcmnSY}A&VWq9}`jqq@O$2iF(qY_!s&Z%lv44sztl~^L#_MlsYtXRES;! zGXE--Rlc>|!WRdWa|3jZ*DA* zMQIe>!$_F0yHMe%%4*YHQH2|EWqk7j_^`q2G>wY&TY#D)INYo>x(3G~;liLOAXcYWk|ea$k*!pOXzyMg0>znPK3r>2m*Yz=R#=VaEi+59GnI8e z%3p0IwRcx@J+j`R>*aBK5w{&o>gqT#r^L|jWlGYVjKXWP1{*oS0hgsX=h^Zbf6E&( z%M2a9@A>T?p0(WuaRGE5d)LFX;oHyAm6-SqnThzmc=f0mKnOqFDUsZcei)&_5+*CjpIctCPo0YfgK##T_6FsPV^wtyx&-skE!n{2aGq+V0VL+>R9D= zc!|uZXQouxUUh9ATWVm4hYMZq;Vvex)lWpe|*4@?Z&Czb*)j7+S#zsyr09PY?_i$&n%6XGzV)XTa#x| zSx0WK3(9K%HSh!o?4(K5$bE$otwbe0-UCVreP8zPq6W;hB7n2~WZs{1$8NTRf3Z+m z_QKQ-72FRgP3G*oUwGnEg&lkufi$?;xmzyxD@oLr$zrRNAsZ(pP=Fo$SVvX>xE(#SQ{}GQF z)cwFJsaLXIOQ5I8-4X)4?0CYf3-J6gpadw(!!S|x_n`mic*_l=3kCE04|YvF;?x54 zTu~d5URLS0hRTvNdbe*EgNt3lJ;gS>1nGdxH$kR|^AXybGMe$=wBg7lo5f|%(XSc+ z$uZA=97^Yoeg8o_G*!`h_VY7;wagR;2se3kL70BsMbkv3U-^Ia?>ZB!PD`O&2L zy0>2_CiM=d`~9J19c)&koPkKB=3*=LjgD?=G(d%d9dAmpW_A=b&L`c5y)PsTl6ZW_k>0IC|3tf#{1w z9k!90Z=#*ryMgnkzQ)zc-95RP+?K&Xv2^pz7ZQt_pJyR z#z0`CwLMcqlQpM6+VvH)D6Ka1mVWC!m(yR;iiDcc%Sc!4KYTIRo-@LPw z!$_;eUf}nwK@AwHBex6Gk&vLuqQ@k8g)_TWo;CkwC~M9aVMzLw?{PEENok>YcPY{y zVt`R*(^(%JSvLUj&RqJTa7>^KgTMnmecY#+GZ0do*;1QQZ6M)QiA{0ud!9rk!d&gz zg{_3$k)dLe#fMC#AF%_*B!42+hBtdw`W!l z^w5Eq9Y?M~JzJCZkm*e9L}9nQ71p_wD@1t;Gbe3BT;2&?CwHM!HS;lTR?k@Shx|rkEW|x7ia1G=KqX2U# z2SJ0KF9-WnPm)x>Hx;KhgCxiVI-1@VxGQ${iDS-2Nb*oY`%gu*;byc0L!)4U0O3CK z3a2_p8McdmCZ>QZlY6*$_ea-3m1``|8Ij~k^*z*v6Q<+d%9969EZ>Go%ZvT9pb%2} z%R&e+hOu!JN1qLZXSIT4S`sN(^!at-S4jf<3KE-7ek2b#o@0ZD46UE~&kybSg? z`oLVs5D{}Ztv&t(gyu-daa2rxfRO*YWkaB6oB+je8g8&x9j5A-W61-+QQg?`#r2Sl z=Iod5(}Ob*;-8hW+hot;J;!5De)$hbAhXYyY(88NDa6wvczWxDFJ}Ni=SuXeLcg`x zwu!QGlr7}@>?aW^i108CaTF?6AOLo9M_xGZz&3~_iGr+)m{?8|YC{kdwE5rHnmOr8 zE>y8k1IoCGE*0Ih(YL6wIrY1AXm=knxcI)+falN$VN1D@&ujp(Sgn3_F|?u%YFX?# zgA79%0tH#Re3qoAfd17N(pNp{EtK1tm84~MjSbiH5J153RpAd(vKFW@b8+8fJPo=d zXH9qQ{swTA(F+rLfG9r8Np>aG)`iY}A+KqsU2Se7nL^KxtBt>6$!`cMIDoKmPa@L^ zk{{)>YIZy4<3=3Ola{ZWVe65329V^7<$PvFyYlmhHV@tBAHBJN(S-P}iR@zsT)b?u zzk=kQ-;6^46l-K6N~A-hy5?7E0^hoCqWFN4r8ufyG2KSc)+sb}-l{+}U9Qie6`FyO zuhL6ZDhU=-;G~8i#mEiU&0#JeC(HA3r*V6+%s<)jfU39bU}t`O^7skLeK#NoeoM@0 zL7k&kt{6!~q065(S=!PM^L(%e^!T~`h$!~fn>+*Hh2!PxoQ>r!=?VP%wB^D}q4_0P z5Mh$L4uhNEKGLIhZ@CkU3?DiWR&||T1T0kJ-$`8;i zLuxxztU>U-t7nq5>vCU5A2rmQzyny=d>wiivb*CC`KmFm%)LcW29SYW$u`M%lec5` zM((|uvyzP=^HU{C;;fwP8n7r>2#r5~TCthDIr&t)T5BBWWb^7N2WOtAI-ouA?qC>a z>TVhTA9rcoo@Txjk}?oL=e@z7F)7h?jN!UNX$L>bw4e$lpsIM&0B9544IZVsl$E(b z+gyZYZnxRm^et23xk55~HED?^GbL&5YC0P!^cP&5YX+?h`oV>FBNe-beQ3i8e-?ON z-0%I^yVU?X=uZm-xV>nXz{Gi3Ez1}+8a^ZDB6|mb@l?KNm?td$Ic&lM6ZLM!=u&|S zu2P^YY(Nv7K0V*k>8Q*}dkEf@;&d%U5|l2IpB1I@ldgMG^xDhZCH*59bx|o&#f8KsXAg9T zEu5DvP2i6esB-VmiTsHD$65Sa5eIbQWMn_2Z5CDDqrw7}Uqd~WWkl19eiW|O%40qH z!$Zte6!wFhTDo0;6C2Ur*Edxh|Cob%h~8W20a<=(di?0WZ24N_xrwC2$(tT)ex?>0KQtyraSPxtc&#!C^?oJ(b3>F$=#Fk51!~xxl;-YY-st z9qt>WduppYRJ||zq(XZwAYiB?6 zf7RsXP?NDQdx>9R6kjHG-qB*AqFr=DGz!f4r&rjTTBQYzm>uD;20>q|xgQ%sSu( zf8!nE-<|r95*2;zV?4M&2)q1EH0ebcgtwQxj--H;&-}<~yAyYvRz~?(|1n{1rIAus z&@O_$b(`H@*M82px+(QxAgxFS?Ss7tqU#usX)ZoW+zP{j7Z_=<<}u@Mr(_Ueg>CBp@AYP-5losaUyeOTo$1VS>ohV^z43bFk? z;>ujLSEg33{3cuUsGe-J4jAe=dFC^*rwot@P zIaJ9cD)k^-%PieTJ216cprWHAahGEwZkM?seB;AIV)Z!}x_3fuIgNtukQX?K|o@~G*tb0s_; zn^INRqmkvTurmfp3I`KjsouBVx9@*Mbfw^Xi1tHu!Ck~~1mB2L@RzL*vUB$*{CS5z z8$Lap_mO+>rmlsnUAVLilo_|$#FoqRZ!mZ4dg86puZkTkgOMq{$2rkOgf8jq-O4Dy zMKT_IUM%*{2`x799Dmoq5c^)|yBTnk?Z+Fs0O1^fBgJ~Ul5|l(Mo|Y= zr(E(~sC5vam$8HDQNhg|c=X(;+3$THYv2J8R>9Z+(9-z2AsQh=k2u``DNbo_)jHMK!h*&%v1YaYPe&y3nHLvv(oMOdZPOuc;|b>O-KQ7VXzbuh?u ztE`KcaDtgWUPZ=|5!Q4g+o!^Gwj%;$tN9FF_g+BZvq1i9ZYP4KA6W3&79dFw3(xJ%2^&af#sC>d zRtr~SHxbTm$yqrRpmDyc>e|`qi0F^I=~zBBdXy9BtGKYa|A>an3cbf)s4;1EQ~Mim*p#2E39lVb*nHH^x``F*)Fe0okr0e@-)Xv;*CuElzF-n4 zvgtRW!2fIwAT5EZM9FHaybA#5yAUV@uM%00!X*2q)(fL<$eR*w^=Vs@Syk<;$;nGn zTN`4{J~QqNn#7WUf?l)9gVx2#4jK#%7UL{|IkCUO;0K3@fEwlFB|D{mBsJ zsEid#+xhr$-k$>=a^a!VI31B53lCi zi$EKv$q^K{Q!0hLhz1hrJBBMk`6l@(WJLg4l(t(M761waU!P~%=^dQ0tt;$`4 z{g`tLDJ(*Z<+iY(;m=8AlK}LG*S+BPPK2N@=;Cd;jZgC&L51V6zG;gmEq1t^v+_Daw_wJd}bne-bhrvTDlFpEds*K z(o^Hep)vSG_?vUrsBcooyYsr#u*dA;#CU-^D@vfyB+i|KT<67CnVNtf(=o>JUa^#7 zn_DE1(qCmEHHrx1JdkP9?t&}~Hw*u^(k(A0(j~&jMpCuS1nJyAu#PR4;wm-ZrBh6N zyEVMXo;@~s=hi}={murgFHfIF;({eDMEdL3zBRk`y-)fp4v^GoUl8sYoZKhu6@PfZ z5O^R#?^wx?FZ=>&Trv73J%X3B=`1?Y=d~wB;=mL}W{I5IUW*u+38TZz4#Qo;MK}l6VjEm5rO7odFVlhu03~uBv zb~~fW7Rr^U&E`q_&I8U+0{6b{wLI%r2;RzA&eLi11nRB2rW2=BQ0i)Rq=@Ab`~jKX z{YKaKcMR%l&oAGR24moR8y9ZA4D(cWKji`UnvMRMITHv4e04#Tf2gEgbV5)))rc}VJFL5};e7KW z3cV{Zgk6(Gr0zMX%sx)U%D4O(!Bn!LH#81Tb@UvTjvhOvsS-p3ne5DyNT4Y+0F