diff --git a/examples/todo-typescript/.gitignore b/examples/todo-typescript/.gitignore new file mode 100644 index 000000000..c51177f6d --- /dev/null +++ b/examples/todo-typescript/.gitignore @@ -0,0 +1,3 @@ +/.wasp/ +/.env.server +/.env.client diff --git a/examples/todo-typescript/.wasproot b/examples/todo-typescript/.wasproot new file mode 100644 index 000000000..ca2cfdb48 --- /dev/null +++ b/examples/todo-typescript/.wasproot @@ -0,0 +1 @@ +File marking the root of Wasp project. diff --git a/examples/todo-typescript/main.wasp b/examples/todo-typescript/main.wasp new file mode 100644 index 000000000..08d34e399 --- /dev/null +++ b/examples/todo-typescript/main.wasp @@ -0,0 +1,67 @@ +app VideoTodoNotifier { + wasp: { + version: "^0.7.3" + }, + 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 Main from "@client/MainPage" +} + +route LoginRoute { path: "/login", to: LoginPage } +page LoginPage { + component: import Login from "@client/LoginPage" +} + +route SignupRoute { path: "/signup", to: SignupPage } +page SignupPage { + component: import Signup from "@client/SignupPage" +} + +query getTasks { + // We specify the JS implementation of our query (which is an async JS function) + // which can be found in `server/queries.js` as a named export `getTasks`. + 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/examples/todo-typescript/migrations/20221219114539_init/migration.sql b/examples/todo-typescript/migrations/20221219114539_init/migration.sql new file mode 100644 index 000000000..7b6262767 --- /dev/null +++ b/examples/todo-typescript/migrations/20221219114539_init/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/examples/todo-typescript/migrations/migration_lock.toml b/examples/todo-typescript/migrations/migration_lock.toml new file mode 100644 index 000000000..e5e5c4705 --- /dev/null +++ b/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/examples/todo-typescript/src/.waspignore b/examples/todo-typescript/src/.waspignore new file mode 100644 index 000000000..1c432f30d --- /dev/null +++ b/examples/todo-typescript/src/.waspignore @@ -0,0 +1,3 @@ +# Ignore editor tmp files +**/*~ +**/#*# diff --git a/examples/todo-typescript/src/client/LoginPage.tsx b/examples/todo-typescript/src/client/LoginPage.tsx new file mode 100644 index 000000000..8adb11ae0 --- /dev/null +++ b/examples/todo-typescript/src/client/LoginPage.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import LoginForm from '@wasp/auth/forms/Login'; + +const 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). + +
+ ); +}; + +export default LoginPage; diff --git a/examples/todo-typescript/src/client/Main.css b/examples/todo-typescript/src/client/Main.css new file mode 100644 index 000000000..5c086b1dc --- /dev/null +++ b/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/examples/todo-typescript/src/client/MainPage.tsx b/examples/todo-typescript/src/client/MainPage.tsx new file mode 100644 index 000000000..70cdeaeed --- /dev/null +++ b/examples/todo-typescript/src/client/MainPage.tsx @@ -0,0 +1,94 @@ +import './Main.css'; +import React from 'react'; +import logout from '@wasp/auth/logout.js'; +import useAuth from '@wasp/auth/useAuth.js'; +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 waspLogo from './waspLogo.png'; +import { Task } from './types' + +const MainPage = () => { + const { data: user } = useAuth(); + const { data: tasks, isLoading, error } = useQuery(getTasks); + + React.useEffect(() => { + console.log(user); + }, [user]); + + if (isLoading) return 'Loading...'; + if (error) return 'Error: ' + error; + + return ( +
+ wasp logo +

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

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

No tasks yet.

; + return ( +
+ {tasks.map((tsk, idx) => ( + + ))} +
+ ); +}; + +const NewTaskForm = () => { + const handleSubmit = async (event: React.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 ( +
+ + +
+ ); +}; + +export default MainPage; diff --git a/examples/todo-typescript/src/client/SignupPage.tsx b/examples/todo-typescript/src/client/SignupPage.tsx new file mode 100644 index 000000000..8a8e8e7fe --- /dev/null +++ b/examples/todo-typescript/src/client/SignupPage.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import SignupForm from '@wasp/auth/forms/Signup'; + +const 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). + +
+ ); +}; + +export default SignupPage; diff --git a/examples/todo-typescript/src/client/react-app-env.d.ts b/examples/todo-typescript/src/client/react-app-env.d.ts new file mode 100644 index 000000000..e80934ce3 --- /dev/null +++ b/examples/todo-typescript/src/client/react-app-env.d.ts @@ -0,0 +1,60 @@ +declare module '*.avif' { + const src: string; + export default src; +} + +declare module '*.bmp' { + const src: string; + export default src; +} + +declare module '*.gif' { + const src: string; + export default src; +} + +declare module '*.jpg' { + const src: string; + export default src; +} + +declare module '*.jpeg' { + const src: string; + export default src; +} + +declare module '*.png' { + const src: string; + export default src; +} + +declare module '*.webp' { + const src: string; + export default src; +} + +declare module '*.svg' { + import * as React from 'react'; + + export const ReactComponent: React.FunctionComponent & { title?: string }>; + + const src: string; + export default src; +} + +declare module '*.module.css' { + const classes: { readonly [key: string]: string }; + export default classes; +} + +declare module '*.module.scss' { + const classes: { readonly [key: string]: string }; + export default classes; +} + +declare module '*.module.sass' { + const classes: { readonly [key: string]: string }; + export default classes; +} diff --git a/examples/todo-typescript/src/client/tsconfig.json b/examples/todo-typescript/src/client/tsconfig.json new file mode 100644 index 000000000..d501a4193 --- /dev/null +++ b/examples/todo-typescript/src/client/tsconfig.json @@ -0,0 +1,55 @@ +// =============================== 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/examples/todo-typescript/src/client/types.ts b/examples/todo-typescript/src/client/types.ts new file mode 100644 index 000000000..7739835bb --- /dev/null +++ b/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/examples/todo-typescript/src/client/waspLogo.png b/examples/todo-typescript/src/client/waspLogo.png new file mode 100644 index 000000000..d39a9443a Binary files /dev/null and b/examples/todo-typescript/src/client/waspLogo.png differ diff --git a/examples/todo-typescript/src/server/actions.ts b/examples/todo-typescript/src/server/actions.ts new file mode 100644 index 000000000..7d29e0078 --- /dev/null +++ b/examples/todo-typescript/src/server/actions.ts @@ -0,0 +1,33 @@ +import HttpError from '@wasp/core/HttpError.js'; +import { Context, Task } from './serverTypes' + +type createArgs = { description: Task['description'] }; + +export const createTask = async ({ 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 = { taskId: Task['id']; isDone: Task['isDone'] }; + +export const updateTask = async ({ taskId, isDone }: updateArgs, context: Context) => { + if (!context.user) { + throw new HttpError(401); + } + + return context.entities.Task.updateMany({ + where: { + id: taskId, + user: { id: context.user.id }, + }, + data: { isDone }, + }); +}; diff --git a/examples/todo-typescript/src/server/queries.ts b/examples/todo-typescript/src/server/queries.ts new file mode 100644 index 000000000..dab9b76a9 --- /dev/null +++ b/examples/todo-typescript/src/server/queries.ts @@ -0,0 +1,9 @@ +import HttpError from '@wasp/core/HttpError.js'; +import { Context, Task } from './serverTypes' + +export const getTasks = async (args: null, context: Context): Promise => { + if (!context.user) { + throw new HttpError(401); + } + return context.entities.Task.findMany({ where: { user: { id: context.user.id } } }); +}; diff --git a/examples/todo-typescript/src/server/serverTypes.ts b/examples/todo-typescript/src/server/serverTypes.ts new file mode 100644 index 000000000..870f56eca --- /dev/null +++ b/examples/todo-typescript/src/server/serverTypes.ts @@ -0,0 +1,11 @@ +import { Task as TaskType, User, Prisma } from '@prisma/client'; + +export type Task = TaskType; + +export type Context = { + user: User; + entities: { + Task: Prisma.TaskDelegate<{}>; + User: Prisma.UserDelegate<{}>; + }; +}; diff --git a/examples/todo-typescript/src/server/tsconfig.json b/examples/todo-typescript/src/server/tsconfig.json new file mode 100644 index 000000000..70a79b44e --- /dev/null +++ b/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/examples/todo-typescript/src/shared/tsconfig.json b/examples/todo-typescript/src/shared/tsconfig.json new file mode 100644 index 000000000..f78b58a77 --- /dev/null +++ b/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"], + } +}