mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-11-27 06:47:31 +03:00
Merge branch 'main' into release
This commit is contained in:
commit
7a91bd8bb4
48
.github/workflows/ci.yaml
vendored
48
.github/workflows/ci.yaml
vendored
@ -133,6 +133,54 @@ jobs:
|
||||
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'macos-latest'
|
||||
run: ./tools/ensure_todoapp_builds.sh
|
||||
|
||||
- name: Headless - Cache Node Modules
|
||||
id: headless-cache-node-modules
|
||||
uses: actions/cache@v3
|
||||
if: matrix.os == 'ubuntu-20.04'
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: node-modules-${{ hashFiles('./waspc/headless-test/package-lock.json') }}
|
||||
|
||||
- name: Headless - Install Dependencies
|
||||
id: headless-install-dependencies
|
||||
if: matrix.os == 'ubuntu-20.04'
|
||||
run: |
|
||||
cd headless-test
|
||||
npm ci
|
||||
|
||||
- name: Headless - Store Playwright's Version
|
||||
id: headless-store-playwright-version
|
||||
if: matrix.os == 'ubuntu-20.04'
|
||||
run: |
|
||||
cd headless-test
|
||||
PLAYWRIGHT_VERSION=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//')
|
||||
echo "Playwright's Version: $PLAYWRIGHT_VERSION"
|
||||
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Headless - Cache Playwright Browsers for Playwright's Version
|
||||
id: headless-cache-playwright-browsers
|
||||
if: matrix.os == 'ubuntu-20.04'
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-browsers-${{ env.PLAYWRIGHT_VERSION }}
|
||||
|
||||
- name: Headless - Setup Playwright
|
||||
id: headless-setup-playwright
|
||||
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true' && matrix.os == 'ubuntu-20.04'
|
||||
run: |
|
||||
cd headless-test
|
||||
npx playwright install --with-deps
|
||||
|
||||
- name: Headless - Run Playwright Tests
|
||||
id: headless-run-playwright-tests
|
||||
if: matrix.os == 'ubuntu-20.04'
|
||||
run: |
|
||||
cd headless-test
|
||||
npm ci
|
||||
# Runs the tests with the debug flag so that we can see Wasp output
|
||||
DEBUG=pw:webserver npx playwright test
|
||||
|
||||
- name: Create binary package (Unix)
|
||||
if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'macos-latest')
|
||||
run: |
|
||||
|
@ -116,3 +116,7 @@ You can also:
|
||||
# Careers
|
||||
|
||||
Check our [careers](https://wasp-lang.notion.site/Wasp-Careers-59fd1682c80d446f92be5fa65cc17672) page for a list of currently opened positions!
|
||||
|
||||
# Sponsors
|
||||
|
||||
<a href="https://github.com/michelwaechter"><img src="https://github.com/michelwaechter.png" width="50px" alt="michelwaechter" /></a> - Our first sponsor ever! Thanks so much Michel ❤️ , from the whole Wasp Team, for bravely going where nobody has been before :)!
|
||||
|
@ -1,5 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## v0.10.6
|
||||
|
||||
### Bug fixes
|
||||
- `wasp deploy fly launch` now supports the latest `flyctl launch` toml file for the web client (which changed their default structure and port).
|
||||
|
||||
## v0.10.5
|
||||
|
||||
### Bug fixes
|
||||
|
4
waspc/headless-test/.gitignore
vendored
Normal file
4
waspc/headless-test/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
26
waspc/headless-test/README.md
Normal file
26
waspc/headless-test/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
## Headless Test
|
||||
|
||||
We are testing if a simple scenario works for the user each time:
|
||||
- signup
|
||||
- login
|
||||
- create a new task
|
||||
|
||||
Check out the test file: `tests/simple.spec.ts`
|
||||
|
||||
### How to run
|
||||
|
||||
Running headless tests:
|
||||
```
|
||||
$ cd headless-test
|
||||
$ npm install
|
||||
$ npx playwright test
|
||||
```
|
||||
|
||||
If something breaks, maybe the example app won't run. Try running it and see if there are any errors:
|
||||
```
|
||||
npm run example-app:start
|
||||
```
|
||||
|
||||
### How to run in CI
|
||||
|
||||
We set up a GitHub Action to run the test in CI. See `.github/workflows/ci.yml` for details.
|
3
waspc/headless-test/examples/todoApp/.gitignore
vendored
Normal file
3
waspc/headless-test/examples/todoApp/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/.wasp/
|
||||
/.env.server
|
||||
/.env.client
|
1
waspc/headless-test/examples/todoApp/.wasproot
Normal file
1
waspc/headless-test/examples/todoApp/.wasproot
Normal file
@ -0,0 +1 @@
|
||||
File marking the root of Wasp project.
|
1
waspc/headless-test/examples/todoApp/Dockerfile
Normal file
1
waspc/headless-test/examples/todoApp/Dockerfile
Normal file
@ -0,0 +1 @@
|
||||
## HELLO!
|
@ -0,0 +1,44 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"email" TEXT,
|
||||
"password" TEXT,
|
||||
"isEmailVerified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"emailVerificationSentAt" TIMESTAMP(3),
|
||||
"passwordResetSentAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SocialLogin" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"providerId" TEXT NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "SocialLogin_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Task" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"isDone" BOOLEAN NOT NULL DEFAULT false,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "Task_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "SocialLogin_provider_providerId_userId_key" ON "SocialLogin"("provider", "providerId", "userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SocialLogin" ADD CONSTRAINT "SocialLogin_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Task" ADD CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
6
waspc/headless-test/examples/todoApp/postcss.config.cjs
Normal file
6
waspc/headless-test/examples/todoApp/postcss.config.cjs
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
6
waspc/headless-test/examples/todoApp/prettier.config.js
Normal file
6
waspc/headless-test/examples/todoApp/prettier.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
trailingComma: 'es5',
|
||||
tabWidth: 2,
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
}
|
2
waspc/headless-test/examples/todoApp/sample.env.client
Normal file
2
waspc/headless-test/examples/todoApp/sample.env.client
Normal file
@ -0,0 +1,2 @@
|
||||
#REACT_APP_API_URL=http://localhost:3001
|
||||
#PORT=3000
|
3
waspc/headless-test/examples/todoApp/sample.env.server
Normal file
3
waspc/headless-test/examples/todoApp/sample.env.server
Normal file
@ -0,0 +1,3 @@
|
||||
TEST_ENV_VAR="I am test"
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost/todoapp
|
||||
#PORT=3001
|
3
waspc/headless-test/examples/todoApp/src/.waspignore
Normal file
3
waspc/headless-test/examples/todoApp/src/.waspignore
Normal file
@ -0,0 +1,3 @@
|
||||
# Ignore editor tmp files
|
||||
**/*~
|
||||
**/#*#
|
42
waspc/headless-test/examples/todoApp/src/client/App.tsx
Normal file
42
waspc/headless-test/examples/todoApp/src/client/App.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import logout from '@wasp/auth/logout.js'
|
||||
import useAuth from '@wasp/auth/useAuth'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import getDate from '@wasp/queries/getDate'
|
||||
|
||||
import './Main.css'
|
||||
|
||||
export function App({ children }: any) {
|
||||
const { data: user } = useAuth()
|
||||
const { data: date } = useQuery(getDate)
|
||||
|
||||
return (
|
||||
<div className="app border-spacing-2 p-4">
|
||||
<header className="flex justify-between">
|
||||
<h1 className="font-bold text-3xl mb-5">
|
||||
<Link to="/">ToDo App</Link>
|
||||
</h1>
|
||||
<h2>
|
||||
Your site was loaded at: {date?.toLocaleString()}
|
||||
</h2>
|
||||
{user && (
|
||||
<div className="flex gap-3 items-center">
|
||||
<div>
|
||||
Hello, <Link to="/profile">{user.email}</Link>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-primary" onClick={logout}>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<main>{children}</main>
|
||||
<footer className="mt-8 text-center">
|
||||
Created with Wasp
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
46
waspc/headless-test/examples/todoApp/src/client/Main.css
Normal file
46
waspc/headless-test/examples/todoApp/src/client/Main.css
Normal file
@ -0,0 +1,46 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply m-2;
|
||||
}
|
||||
a {
|
||||
@apply underline text-blue-600 hover:text-blue-800 visited:text-purple-600;
|
||||
}
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
@apply rounded;
|
||||
}
|
||||
input[type="submit"] {
|
||||
@apply btn btn-primary;
|
||||
}
|
||||
form.auth-form input {
|
||||
@apply my-2;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
@apply appearance-none h-4 w-4 border border-gray-300 rounded-sm bg-white checked:bg-blue-600 checked:border-blue-600 focus:outline-none transition duration-200 mt-1 align-top bg-no-repeat bg-center bg-contain float-left mr-2 cursor-pointer;
|
||||
}
|
||||
h1 {
|
||||
@apply text-2xl;
|
||||
}
|
||||
h2 {
|
||||
@apply text-xl;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply font-bold py-2 px-4 rounded;
|
||||
}
|
||||
.btn-primary {
|
||||
@apply bg-blue-500 text-white;
|
||||
}
|
||||
.btn-red {
|
||||
@apply bg-red-500 text-white;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
@apply bg-blue-600;
|
||||
}
|
||||
}
|
219
waspc/headless-test/examples/todoApp/src/client/Todo.tsx
Normal file
219
waspc/headless-test/examples/todoApp/src/client/Todo.tsx
Normal file
@ -0,0 +1,219 @@
|
||||
import React, { useState, FormEventHandler, ChangeEventHandler } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import { OptimisticUpdateDefinition, useAction } from '@wasp/actions'
|
||||
import getTasks from '@wasp/queries/getTasks.js'
|
||||
import createTask from '@wasp/actions/createTask.js'
|
||||
import updateTaskIsDone from '@wasp/actions/updateTaskIsDone.js'
|
||||
import deleteCompletedTasks from '@wasp/actions/deleteCompletedTasks.js'
|
||||
import toggleAllTasks from '@wasp/actions/toggleAllTasks.js'
|
||||
import { Task } from '@wasp/entities'
|
||||
|
||||
type NonEmptyArray<T> = [T, ...T[]]
|
||||
|
||||
export function areThereAnyTasks(
|
||||
tasks: Task[] | undefined
|
||||
): tasks is NonEmptyArray<Task> {
|
||||
return !!(tasks && tasks.length > 0)
|
||||
}
|
||||
|
||||
const Todo = () => {
|
||||
const { data: tasks, isError, error: tasksError } = useQuery(getTasks)
|
||||
|
||||
const TasksError = () => {
|
||||
return (
|
||||
<div>{'Error during fetching tasks: ' + (tasksError?.message || '')}</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<div className="w-3/6 shadow-md rounded p-6">
|
||||
<h1 className="mb-2">Todos</h1>
|
||||
|
||||
<div className="flex justify-start gap-2">
|
||||
<ToggleAllTasksButton disabled={!areThereAnyTasks(tasks)} />
|
||||
<NewTaskForm />
|
||||
</div>
|
||||
|
||||
{isError && <TasksError />}
|
||||
|
||||
{areThereAnyTasks(tasks) && (
|
||||
<>
|
||||
<Tasks tasks={tasks} />
|
||||
|
||||
<Footer tasks={tasks} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Footer = ({ tasks }: { tasks: NonEmptyArray<Task> }) => {
|
||||
const numCompletedTasks = tasks.filter((t) => t.isDone).length
|
||||
const numUncompletedTasks = tasks.filter((t) => !t.isDone).length
|
||||
|
||||
const handleDeleteCompletedTasks = async () => {
|
||||
try {
|
||||
await deleteCompletedTasks()
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-between">
|
||||
<div>{numUncompletedTasks} items left</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
className={'btn btn-red ' + (numCompletedTasks > 0 ? '' : 'hidden')}
|
||||
onClick={handleDeleteCompletedTasks}
|
||||
>
|
||||
Delete completed
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Tasks = ({ tasks }: { tasks: NonEmptyArray<Task> }) => {
|
||||
return (
|
||||
<div>
|
||||
<table className="border-separate border-spacing-2">
|
||||
<tbody>
|
||||
{tasks.map((task, idx) => (
|
||||
<TaskView task={task} key={idx} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type UpdateTaskIsDonePayload = Pick<Task, 'id' | 'isDone'>
|
||||
|
||||
const TaskView = ({ task }: { task: Task }) => {
|
||||
const updateTaskIsDoneOptimistically = useAction(updateTaskIsDone, {
|
||||
optimisticUpdates: [
|
||||
{
|
||||
getQuerySpecifier: () => [getTasks],
|
||||
updateQuery: (updatedTask, oldTasks) => {
|
||||
if (oldTasks === undefined) {
|
||||
// cache is empty
|
||||
return [{ ...task, ...updatedTask }]
|
||||
} else {
|
||||
return oldTasks.map((task) =>
|
||||
task.id === updatedTask.id ? { ...task, ...updatedTask } : task
|
||||
)
|
||||
}
|
||||
},
|
||||
} as OptimisticUpdateDefinition<UpdateTaskIsDonePayload, Task[]>,
|
||||
],
|
||||
})
|
||||
const handleTaskIsDoneChange: ChangeEventHandler<HTMLInputElement> = async (
|
||||
event
|
||||
) => {
|
||||
const id = parseInt(event.target.id)
|
||||
const isDone = event.target.checked
|
||||
|
||||
try {
|
||||
await updateTaskIsDoneOptimistically({ id, isDone })
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={String(task.id)}
|
||||
checked={task.isDone}
|
||||
onChange={handleTaskIsDoneChange}
|
||||
color="default"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`/task/${task.id}`}> {task.description} </Link>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
const NewTaskForm = () => {
|
||||
const defaultDescription = ''
|
||||
const [description, setDescription] = useState(defaultDescription)
|
||||
const createTaskFn = useAction(createTask, {
|
||||
optimisticUpdates: [
|
||||
{
|
||||
getQuerySpecifier: () => [getTasks],
|
||||
updateQuery: (newTask, oldTasks) => {
|
||||
if (oldTasks === undefined) {
|
||||
// cache is empty
|
||||
return [newTask as Task]
|
||||
} else {
|
||||
return [...oldTasks, newTask as Task]
|
||||
}
|
||||
},
|
||||
} as OptimisticUpdateDefinition<
|
||||
Pick<Task, 'isDone' | 'description'>,
|
||||
Task[]
|
||||
>,
|
||||
],
|
||||
})
|
||||
|
||||
const createNewTask = async (description: Task['description']) => {
|
||||
const task = { isDone: false, description }
|
||||
await createTaskFn(task)
|
||||
}
|
||||
|
||||
const handleNewTaskSubmit: FormEventHandler<HTMLFormElement> = async (
|
||||
event
|
||||
) => {
|
||||
event.preventDefault()
|
||||
try {
|
||||
setDescription(defaultDescription)
|
||||
await createNewTask(description)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleNewTaskSubmit} className="content-start flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter task"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
<button className="btn btn-primary">Create new task</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
const ToggleAllTasksButton = ({ disabled }: { disabled: boolean }) => {
|
||||
const handleToggleAllTasks = async () => {
|
||||
try {
|
||||
await toggleAllTasks()
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={disabled}
|
||||
onClick={handleToggleAllTasks}
|
||||
>
|
||||
✓
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default Todo
|
@ -0,0 +1,6 @@
|
||||
import { sayHi } from '../shared/util'
|
||||
|
||||
export default function setup() {
|
||||
console.log("This was called from the client setup function")
|
||||
sayHi()
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
const About = () => {
|
||||
return (
|
||||
<>
|
||||
<div>I am About page!</div>
|
||||
<Link to='/'>Go to dashboard</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default About;
|
@ -0,0 +1,7 @@
|
||||
import Todo from '../Todo'
|
||||
|
||||
const Main = () => {
|
||||
return <Todo />
|
||||
}
|
||||
|
||||
export default Main
|
@ -0,0 +1,31 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { User } from '@wasp/entities'
|
||||
import api from '@wasp/api'
|
||||
|
||||
async function fetchCustomRoute() {
|
||||
const res = await api.get('/foo/bar')
|
||||
console.log(res.data)
|
||||
}
|
||||
|
||||
export const ProfilePage = ({
|
||||
user: { email, isEmailVerified },
|
||||
}: {
|
||||
user: User
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
fetchCustomRoute()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Profile page</h2>
|
||||
<div>
|
||||
Hello <strong>{email}</strong>! Your status is{' '}
|
||||
<strong>{isEmailVerified ? 'verfied' : 'unverified'}</strong>.
|
||||
</div>
|
||||
<br />
|
||||
<Link to="/">Go to dashboard</Link>
|
||||
</>
|
||||
)
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import { OptimisticUpdateDefinition, useAction } from '@wasp/actions'
|
||||
import updateTaskIsDone from '@wasp/actions/updateTaskIsDone'
|
||||
import getTask from '@wasp/queries/getTask.js'
|
||||
import getTasks from '@wasp/queries/getTasks.js'
|
||||
import { Task } from '@wasp/entities'
|
||||
|
||||
type TaskPayload = Pick<Task, "id" | "isDone">
|
||||
|
||||
const Todo = (props: any) => {
|
||||
const taskId = parseInt(props.match.params.id)
|
||||
|
||||
const { data: task, isFetching, error } = useQuery(getTask, { id: taskId })
|
||||
|
||||
const updateTaskIsDoneOptimistically = useAction(updateTaskIsDone, {
|
||||
optimisticUpdates: [
|
||||
{
|
||||
getQuerySpecifier: () => [getTask, { id: taskId }],
|
||||
// This query's cache should should never be emtpy
|
||||
updateQuery: ({ isDone }, oldTask) => ({ ...oldTask!, isDone }),
|
||||
} as OptimisticUpdateDefinition<TaskPayload, Task>,
|
||||
{
|
||||
getQuerySpecifier: () => [getTasks],
|
||||
updateQuery: (updatedTask, oldTasks) =>
|
||||
oldTasks && oldTasks.map(task =>
|
||||
task.id === updatedTask.id ? { ...task, ...updatedTask } : task
|
||||
),
|
||||
} as OptimisticUpdateDefinition<TaskPayload, Task[]>
|
||||
]
|
||||
})
|
||||
|
||||
if (!task) return <div>Task with id {taskId} does not exist.</div>
|
||||
if (error) return <div>Error occurred! {error}</div>
|
||||
|
||||
async function toggleIsDone({ id, isDone }: Task) {
|
||||
try {
|
||||
updateTaskIsDoneOptimistically({ id, isDone: !isDone })
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isFetching ? (
|
||||
<div> Fetching task ... </div>
|
||||
) : (
|
||||
<>
|
||||
<h2>Task</h2>
|
||||
<div> id: {task.id} </div>
|
||||
<div> description: {task.description} </div>
|
||||
<div> is done: {task.isDone ? 'Yes' : 'No'} </div>
|
||||
<button onClick={() => toggleIsDone(task)}>Mark as {task.isDone ? 'undone' : 'done'}</button>
|
||||
</>
|
||||
)}
|
||||
<br />
|
||||
<Link to='/'>Go to dashboard</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Todo
|
@ -0,0 +1,27 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { VerifyEmailForm } from '@wasp/auth/forms/VerifyEmail'
|
||||
import appearance from './appearance'
|
||||
import todoLogo from '../../todoLogo.png'
|
||||
|
||||
export function EmailVerification() {
|
||||
return (
|
||||
<div className="w-full h-full bg-white">
|
||||
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
|
||||
<div className="w-full h-full max-w-sm p-5 bg-white">
|
||||
<div>
|
||||
<VerifyEmailForm
|
||||
appearance={appearance}
|
||||
logo={todoLogo}
|
||||
socialLayout="horizontal"
|
||||
/>
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
If everything is okay, <Link to="/login">go to login</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
import appearance from './appearance'
|
||||
import todoLogo from '../../todoLogo.png'
|
||||
|
||||
const Login = () => {
|
||||
return (
|
||||
<div className="w-full h-full bg-white">
|
||||
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
|
||||
<div className="w-full h-full max-w-sm p-5 bg-white">
|
||||
<div>
|
||||
<LoginForm
|
||||
appearance={appearance}
|
||||
logo={todoLogo}
|
||||
socialLayout="horizontal"
|
||||
/>
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
Don't have an account yet? <Link to="/signup">go to signup</Link>.
|
||||
</span>
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
Forgot your password?{' '}
|
||||
<Link to="/request-password-reset">reset it</Link>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Login
|
@ -0,0 +1,27 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { ResetPasswordForm } from '@wasp/auth/forms/ResetPassword'
|
||||
import appearance from './appearance'
|
||||
import todoLogo from '../../todoLogo.png'
|
||||
|
||||
export function PasswordReset() {
|
||||
return (
|
||||
<div className="w-full h-full bg-white">
|
||||
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
|
||||
<div className="w-full h-full max-w-sm p-5 bg-white">
|
||||
<div>
|
||||
<ResetPasswordForm
|
||||
appearance={appearance}
|
||||
logo={todoLogo}
|
||||
socialLayout="horizontal"
|
||||
/>
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
If everything is okay, <Link to="/login">go to login</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { ForgotPasswordForm } from '@wasp/auth/forms/ForgotPassword'
|
||||
import appearance from './appearance'
|
||||
import todoLogo from '../../todoLogo.png'
|
||||
|
||||
export function RequestPasswordReset() {
|
||||
return (
|
||||
<div className="w-full h-full bg-white">
|
||||
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
|
||||
<div className="w-full h-full max-w-sm p-5 bg-white">
|
||||
<div>
|
||||
<ForgotPasswordForm
|
||||
appearance={appearance}
|
||||
logo={todoLogo}
|
||||
socialLayout="horizontal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { SignupForm } from '@wasp/auth/forms/Signup'
|
||||
import getNumTasks from '@wasp/queries/getNumTasks'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import { getTotalTaskCountMessage } from './helpers'
|
||||
|
||||
import appearance from './appearance'
|
||||
import todoLogo from '../../todoLogo.png'
|
||||
|
||||
const Signup = () => {
|
||||
const { data: numTasks } = useQuery(getNumTasks)
|
||||
|
||||
return (
|
||||
<div className="w-full h-full bg-white">
|
||||
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
|
||||
<div className="w-full h-full max-w-sm p-5 bg-white">
|
||||
<div>
|
||||
<SignupForm
|
||||
appearance={appearance}
|
||||
logo={todoLogo}
|
||||
socialLayout="horizontal"
|
||||
/>
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
I already have an account (<Link to="/login">go to login</Link>).
|
||||
</span>
|
||||
<br />
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<span>{getTotalTaskCountMessage(numTasks)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Signup
|
@ -0,0 +1,9 @@
|
||||
const appearance = {
|
||||
colors: {
|
||||
brand: '#5969b8', // blue
|
||||
brandAccent: '#de5998', // pink
|
||||
submitButtonText: 'white',
|
||||
},
|
||||
}
|
||||
|
||||
export default appearance
|
@ -0,0 +1,12 @@
|
||||
export function getTotalTaskCountMessage(numTasks?: number): string {
|
||||
if (numTasks === undefined) {
|
||||
return ''
|
||||
}
|
||||
if (numTasks === 0) {
|
||||
return 'No tasks created, yet.'
|
||||
}
|
||||
if (numTasks === 1) {
|
||||
return 'There is just one task.'
|
||||
}
|
||||
return `There are ${numTasks} tasks created so far.`
|
||||
}
|
BIN
waspc/headless-test/examples/todoApp/src/client/todoLogo.png
Normal file
BIN
waspc/headless-test/examples/todoApp/src/client/todoLogo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 75 KiB |
@ -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"
|
||||
],
|
||||
}
|
1
waspc/headless-test/examples/todoApp/src/client/vite-env.d.ts
vendored
Normal file
1
waspc/headless-test/examples/todoApp/src/client/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="../../.wasp/out/web-app/node_modules/vite/client" />
|
86
waspc/headless-test/examples/todoApp/src/server/actions.ts
Normal file
86
waspc/headless-test/examples/todoApp/src/server/actions.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
import { getSomeResource } from './serverSetup.js'
|
||||
import { Task } from '@wasp/entities'
|
||||
import {
|
||||
CreateTask,
|
||||
DeleteCompletedTasks,
|
||||
ToggleAllTasks,
|
||||
UpdateTaskIsDone,
|
||||
} from '@wasp/actions/types'
|
||||
|
||||
export const createTask: CreateTask<Pick<Task, 'description'>> = async (
|
||||
task,
|
||||
context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
|
||||
const Task = context.entities.Task
|
||||
|
||||
console.log(
|
||||
'New task created! Btw, current value of someResource is: ' +
|
||||
getSomeResource()
|
||||
)
|
||||
|
||||
return Task.create({
|
||||
data: {
|
||||
description: task.description,
|
||||
user: {
|
||||
connect: { id: context.user.id },
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const updateTaskIsDone: UpdateTaskIsDone<
|
||||
Pick<Task, 'id' | 'isDone'>
|
||||
> = async ({ id, isDone }, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
|
||||
// Uncomment to test optimistic updates
|
||||
// const sleep = (ms) => new Promise(res => setTimeout(res, ms))
|
||||
// await sleep(3000);
|
||||
|
||||
const Task = context.entities.Task
|
||||
const updateResult = await Task.updateMany({
|
||||
where: { id, user: { id: context.user.id } },
|
||||
data: { isDone },
|
||||
})
|
||||
return updateResult
|
||||
}
|
||||
|
||||
export const deleteCompletedTasks: DeleteCompletedTasks = async (
|
||||
_args,
|
||||
context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
|
||||
const Task = context.entities.Task
|
||||
await Task.deleteMany({
|
||||
where: { isDone: true, user: { id: context.user.id } },
|
||||
})
|
||||
}
|
||||
|
||||
export const toggleAllTasks: ToggleAllTasks = async (_args, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
|
||||
const whereIsDone = (isDone: boolean) => ({
|
||||
isDone,
|
||||
user: { id: context.user.id },
|
||||
})
|
||||
const Task = context.entities.Task
|
||||
const notDoneTasksCount = await Task.count({ where: whereIsDone(false) })
|
||||
|
||||
if (notDoneTasksCount > 0) {
|
||||
await Task.updateMany({ where: whereIsDone(false), data: { isDone: true } })
|
||||
} else {
|
||||
await Task.updateMany({ where: whereIsDone(true), data: { isDone: false } })
|
||||
}
|
||||
}
|
50
waspc/headless-test/examples/todoApp/src/server/apis.ts
Normal file
50
waspc/headless-test/examples/todoApp/src/server/apis.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { BarBaz, FooBar, WebhookCallback } from '@wasp/apis/types'
|
||||
import express from 'express'
|
||||
import { MiddlewareConfigFn } from '@wasp/middleware'
|
||||
|
||||
export const fooBar: FooBar = (_req, res, context) => {
|
||||
res.json({ msg: `Hello, ${context?.user?.email}!` })
|
||||
}
|
||||
|
||||
export const fooBarMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
// console.log('fooBarMiddlewareFn: Adding custom middleware for route.')
|
||||
|
||||
const customMiddleware : express.RequestHandler = (_req, _res, next) => {
|
||||
console.log('fooBarMiddlewareFn: custom route middleware')
|
||||
next()
|
||||
}
|
||||
|
||||
middlewareConfig.set('custom.route', customMiddleware)
|
||||
|
||||
return middlewareConfig
|
||||
}
|
||||
|
||||
export const barBaz: BarBaz = (_req, res, _context) => {
|
||||
res.json({ msg: `Hello, stranger!` })
|
||||
}
|
||||
|
||||
export const barNamespaceMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
console.log('barNamespaceMiddlewareFn: Ignoring all default middleware.')
|
||||
|
||||
middlewareConfig.set('custom.apiNamespace',
|
||||
(req, _res, next) => {
|
||||
console.log(`barNamespaceMiddlewareFn: custom middleware (path: ${req.path})`)
|
||||
next()
|
||||
}
|
||||
)
|
||||
|
||||
return middlewareConfig
|
||||
}
|
||||
|
||||
export const webhookCallback: WebhookCallback = (req, res, _context) => {
|
||||
res.json({ msg: req.body.length })
|
||||
}
|
||||
|
||||
export const webhookCallbackMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
// console.log('webhookCallbackMiddlewareFn: Swap express.json for express.raw')
|
||||
|
||||
middlewareConfig.delete('express.json')
|
||||
middlewareConfig.set('express.raw', express.raw({ type: '*/*' }))
|
||||
|
||||
return middlewareConfig
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import {
|
||||
GetPasswordResetEmailContentFn,
|
||||
GetVerificationEmailContentFn,
|
||||
} from '@wasp/types'
|
||||
|
||||
export const getPasswordResetEmailContent: GetPasswordResetEmailContentFn = ({
|
||||
passwordResetLink,
|
||||
}) => ({
|
||||
subject: 'Password reset',
|
||||
text: `Click the link below to reset your password: ${passwordResetLink}`,
|
||||
html: `
|
||||
<p>Click the link below to reset your password</p>
|
||||
<a href="${passwordResetLink}">Reset password</a>
|
||||
`,
|
||||
})
|
||||
|
||||
export const getVerificationEmailContent: GetVerificationEmailContentFn = ({
|
||||
verificationLink,
|
||||
}) => ({
|
||||
subject: 'Verify your email',
|
||||
text: `Click the link below to verify your email: ${verificationLink}`,
|
||||
html: `
|
||||
<p>Click the link below to verify your email</p>
|
||||
<a href="${verificationLink}">Verify email</a>
|
||||
`,
|
||||
})
|
35
waspc/headless-test/examples/todoApp/src/server/dbSeeds.ts
Normal file
35
waspc/headless-test/examples/todoApp/src/server/dbSeeds.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { createTask } from './actions.js'
|
||||
import type { DbSeedFn } from '@wasp/dbSeed/types.js'
|
||||
|
||||
async function createUser (prismaClient, data) {
|
||||
const { password, ...newUser } = await prismaClient.user.create({ data })
|
||||
return newUser
|
||||
}
|
||||
|
||||
export const devSeedSimple: DbSeedFn = async (prismaClient) => {
|
||||
const user = await createUser(prismaClient, {
|
||||
username: "martinsos",
|
||||
password: "test1234"
|
||||
})
|
||||
|
||||
await createTask(
|
||||
{ description: "My initial task" },
|
||||
{ user, entities: { Task: prismaClient.task } }
|
||||
)
|
||||
|
||||
console.log("Did simple dev seed!")
|
||||
}
|
||||
|
||||
export const prodSeed: DbSeedFn = async (prismaClient) => {
|
||||
const user = await createUser(prismaClient, {
|
||||
username: "martinsosProd",
|
||||
password: "test1234prod"
|
||||
})
|
||||
|
||||
await createTask(
|
||||
{ description: "My initial task in production" },
|
||||
{ user, entities: { Task: prismaClient.task } }
|
||||
)
|
||||
|
||||
console.log("Did seeding intended for production!")
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import { sleep } from '@wasp/utils.js'
|
||||
|
||||
export async function foo(args, context) {
|
||||
console.log("Inside Job bar's callback foo: ", args, context)
|
||||
await sleep(4000)
|
||||
return { hello: "world" }
|
||||
}
|
54
waspc/headless-test/examples/todoApp/src/server/queries.ts
Normal file
54
waspc/headless-test/examples/todoApp/src/server/queries.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
import { Task } from '@wasp/entities'
|
||||
import {
|
||||
GetNumTasks,
|
||||
GetTask,
|
||||
GetTasks,
|
||||
GetDate,
|
||||
} from '@wasp/queries/types'
|
||||
|
||||
export const getTasks: GetTasks<void, Task[]> = async (_args, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
console.log('user who made the query: ', context.user)
|
||||
console.log('TEST_ENV_VAR', process.env.TEST_ENV_VAR)
|
||||
|
||||
const Task = context.entities.Task
|
||||
const tasks = await Task.findMany(
|
||||
{
|
||||
where: { user: { id: context.user.id } },
|
||||
orderBy: { id: 'asc' },
|
||||
}
|
||||
)
|
||||
return tasks
|
||||
}
|
||||
|
||||
export const getNumTasks: GetNumTasks<void, number> = async (_args, context) => {
|
||||
return context.entities.Task.count()
|
||||
}
|
||||
|
||||
export const getTask: GetTask<Pick<Task, 'id'>, Task> = async (where, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
|
||||
const Task = context.entities.Task
|
||||
// NOTE(matija): we can't call findUnique() with the specific user, so we have to fetch user first
|
||||
// and then manually check.
|
||||
const task = await Task.findUnique({ where, include: { user: true } })
|
||||
if (!task) {
|
||||
throw new HttpError(404)
|
||||
}
|
||||
// 404 is used to 'hide' the current existence of a forbidden target resource as a security measure
|
||||
// for vulnerabilities like IDOR
|
||||
if (task.user.id !== context.user.id) {
|
||||
throw new HttpError(404)
|
||||
}
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
export const getDate: GetDate<void, Date> = async () => {
|
||||
return new Date()
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import { mySpecialJob } from '@wasp/jobs/mySpecialJob.js'
|
||||
import { sayHi } from '../shared/util.js'
|
||||
import { ServerSetupFn, Application } from '@wasp/types'
|
||||
import { MiddlewareConfigFn } from '@wasp/middleware'
|
||||
import cors from 'cors'
|
||||
import config from '@wasp/config.js'
|
||||
|
||||
let someResource = undefined
|
||||
|
||||
export const getSomeResource = () => someResource
|
||||
|
||||
const setup: ServerSetupFn = async ({ app }) => {
|
||||
addCustomRoute(app)
|
||||
|
||||
sayHi()
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
someResource = 'This resource is now set up.'
|
||||
console.log('Custom server setup done!')
|
||||
|
||||
console.log('Kicking off Job...')
|
||||
// Or: const submittedJob = await mySpecialJob.delay(10).submit({ something: "here" })
|
||||
const submittedJob = await mySpecialJob.submit({ something: "here" })
|
||||
console.log(submittedJob.jobId, submittedJob.jobName, submittedJob.executorName)
|
||||
console.log("submittedJob.pgBoss.details()", await submittedJob.pgBoss.details())
|
||||
}
|
||||
|
||||
function addCustomRoute(app: Application) {
|
||||
app.get('/customRoute', (_req, res) => {
|
||||
res.set('Access-Control-Allow-Origin', 'example-cors-override.com')
|
||||
res.removeHeader('X-Frame-Options')
|
||||
res.send('I am a custom route')
|
||||
})
|
||||
}
|
||||
|
||||
export const serverMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
// Example of adding an extra domain to CORS.
|
||||
middlewareConfig.set('cors', cors({ origin: [config.frontendUrl, 'http://127.0.0.1:3000'] }))
|
||||
return middlewareConfig
|
||||
}
|
||||
|
||||
export default setup
|
@ -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"
|
||||
],
|
||||
}
|
@ -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"]
|
||||
}
|
||||
}
|
3
waspc/headless-test/examples/todoApp/src/shared/util.js
Normal file
3
waspc/headless-test/examples/todoApp/src/shared/util.js
Normal file
@ -0,0 +1,3 @@
|
||||
export function sayHi() {
|
||||
console.log("This is coming from shared function.")
|
||||
}
|
13
waspc/headless-test/examples/todoApp/tailwind.config.cjs
Normal file
13
waspc/headless-test/examples/todoApp/tailwind.config.cjs
Normal file
@ -0,0 +1,13 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
require('@tailwindcss/typography'),
|
||||
],
|
||||
}
|
231
waspc/headless-test/examples/todoApp/todoApp.wasp
Normal file
231
waspc/headless-test/examples/todoApp/todoApp.wasp
Normal file
@ -0,0 +1,231 @@
|
||||
app todoApp {
|
||||
wasp: {
|
||||
version: "^0.10.0"
|
||||
},
|
||||
title: "ToDo App",
|
||||
dependencies: [
|
||||
("@tailwindcss/forms", "^0.5.3"),
|
||||
("@tailwindcss/typography", "^0.5.7")
|
||||
],
|
||||
auth: {
|
||||
userEntity: User,
|
||||
methods: {
|
||||
email: {
|
||||
fromField: {
|
||||
name: "ToDO App",
|
||||
email: "mihovil@ilakovac.com"
|
||||
},
|
||||
emailVerification: {
|
||||
getEmailContentFn: import { getVerificationEmailContent } from "@server/auth/email.js",
|
||||
clientRoute: EmailVerificationRoute,
|
||||
},
|
||||
passwordReset: {
|
||||
getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js",
|
||||
clientRoute: PasswordResetRoute
|
||||
},
|
||||
allowUnverifiedLogin: true,
|
||||
},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login",
|
||||
onAuthSucceededRedirectTo: "/profile"
|
||||
},
|
||||
server: {
|
||||
setupFn: import setup from "@server/serverSetup.js",
|
||||
middlewareConfigFn: import { serverMiddlewareFn } from "@server/serverSetup.js"
|
||||
},
|
||||
client: {
|
||||
rootComponent: import { App } from "@client/App.tsx",
|
||||
setupFn: import setup from "@client/clientSetup.js"
|
||||
},
|
||||
db: {
|
||||
system: PostgreSQL,
|
||||
seeds: [
|
||||
import { devSeedSimple } from "@server/dbSeeds.js",
|
||||
import { prodSeed } from "@server/dbSeeds.js"
|
||||
]
|
||||
},
|
||||
emailSender: {
|
||||
provider: SMTP,
|
||||
defaultFrom: {
|
||||
email: "mihovil@ilakovac.com"
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
// Email auth
|
||||
email String? @unique
|
||||
password String?
|
||||
isEmailVerified Boolean @default(false)
|
||||
emailVerificationSentAt DateTime?
|
||||
passwordResetSentAt DateTime?
|
||||
// Social login
|
||||
externalAuthAssociations SocialLogin[]
|
||||
// Business logic
|
||||
tasks Task[]
|
||||
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=}
|
||||
|
||||
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 SignupRoute { path: "/signup", to: SignupPage }
|
||||
page SignupPage {
|
||||
component: import Signup from "@client/pages/auth/Signup.tsx"
|
||||
}
|
||||
|
||||
route LoginRoute { path: "/login", to: LoginPage }
|
||||
page LoginPage {
|
||||
component: import Login from "@client/pages/auth/Login.tsx"
|
||||
}
|
||||
|
||||
route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
|
||||
page PasswordResetPage {
|
||||
component: import { PasswordReset } from "@client/pages/auth/PasswordReset.tsx",
|
||||
}
|
||||
|
||||
route EmailVerificationRoute { path: "/email-verification-", to: EmailVerificationPage }
|
||||
page EmailVerificationPage {
|
||||
component: import { EmailVerification } from "@client/pages/auth/EmailVerification.tsx",
|
||||
}
|
||||
|
||||
route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
|
||||
page RequestPasswordResetPage {
|
||||
component: import { RequestPasswordReset } from "@client/pages/auth/RequestPasswordReset.tsx",
|
||||
}
|
||||
|
||||
route HomeRoute { path: "/", to: MainPage }
|
||||
page MainPage {
|
||||
authRequired: true,
|
||||
component: import Main from "@client/pages/Main.jsx"
|
||||
}
|
||||
|
||||
route AboutRoute { path: "/about", to: AboutPage }
|
||||
page AboutPage {
|
||||
component: import About from "@client/pages/About.jsx"
|
||||
}
|
||||
|
||||
route ProfileRoute { path: "/profile", to: ProfilePage }
|
||||
page ProfilePage {
|
||||
authRequired: true,
|
||||
component: import { ProfilePage } from "@client/pages/ProfilePage.tsx"
|
||||
}
|
||||
|
||||
// Page for viewing a specific task
|
||||
//
|
||||
route TaskRoute { path: "/task/:id", to: TaskPage }
|
||||
page TaskPage {
|
||||
authRequired: true,
|
||||
component: import Task from "@client/pages/Task.tsx"
|
||||
}
|
||||
|
||||
// --------- Queries --------- //
|
||||
|
||||
query getTasks {
|
||||
fn: import { getTasks } from "@server/queries.js",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
api fooBar {
|
||||
fn: import { fooBar } from "@server/apis.js",
|
||||
middlewareConfigFn: import { fooBarMiddlewareFn } from "@server/apis.js",
|
||||
entities: [Task],
|
||||
// ALL here let's our CORS work. If we did GET, we would need an apiNamespace over it with CORS.
|
||||
httpRoute: (ALL, "/foo/bar")
|
||||
}
|
||||
|
||||
apiNamespace bar {
|
||||
middlewareConfigFn: import { barNamespaceMiddlewareFn } from "@server/apis.js",
|
||||
path: "/bar"
|
||||
}
|
||||
|
||||
api barBaz {
|
||||
fn: import { barBaz } from "@server/apis.js",
|
||||
auth: false,
|
||||
entities: [Task],
|
||||
httpRoute: (GET, "/bar/baz")
|
||||
}
|
||||
|
||||
api webhookCallback {
|
||||
fn: import { webhookCallback } from "@server/apis.js",
|
||||
middlewareConfigFn: import { webhookCallbackMiddlewareFn } from "@server/apis.js",
|
||||
httpRoute: (POST, "/webhook/callback"),
|
||||
auth: false
|
||||
}
|
||||
|
||||
query getNumTasks {
|
||||
fn: import { getNumTasks } from "@server/queries.js",
|
||||
entities: [Task],
|
||||
auth: false
|
||||
}
|
||||
|
||||
query getTask {
|
||||
fn: import { getTask } from "@server/queries.js",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
query getDate {
|
||||
fn: import { getDate } from "@server/queries.js"
|
||||
}
|
||||
|
||||
// --------- Actions --------- //
|
||||
|
||||
action createTask {
|
||||
fn: import { createTask } from "@server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
action updateTaskIsDone {
|
||||
fn: import { updateTaskIsDone } from "@server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
action deleteCompletedTasks {
|
||||
fn: import { deleteCompletedTasks } from "@server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
action toggleAllTasks {
|
||||
fn: import { toggleAllTasks } from "@server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
job mySpecialJob {
|
||||
executor: PgBoss,
|
||||
perform: {
|
||||
fn: import { foo } from "@server/jobs/bar.js",
|
||||
executorOptions: {
|
||||
pgBoss: {=json { "retryLimit": 1 } json=}
|
||||
}
|
||||
},
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
job mySpecialScheduledJob {
|
||||
executor: PgBoss,
|
||||
perform: {
|
||||
fn: import { foo } from "@server/jobs/bar.js"
|
||||
},
|
||||
schedule: {
|
||||
cron: "0 * * * *",
|
||||
args: {=json { "foo": "bar" } json=},
|
||||
executorOptions: {
|
||||
pgBoss: {=json { "retryLimit": 2 } json=}
|
||||
}
|
||||
}
|
||||
}
|
506
waspc/headless-test/package-lock.json
generated
Normal file
506
waspc/headless-test/package-lock.json
generated
Normal file
@ -0,0 +1,506 @@
|
||||
{
|
||||
"name": "headless-test",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "headless-test",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^4.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.33.0",
|
||||
"concurrently": "^8.0.1",
|
||||
"prisma": "^4.12.0",
|
||||
"wait-port": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz",
|
||||
"integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.13.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.33.0.tgz",
|
||||
"integrity": "sha512-YunBa2mE7Hq4CfPkGzQRK916a4tuZoVx/EpLjeWlTVOnD4S2+fdaQZE0LJkbfhN5FTSKNLdcl7MoT5XB37bTkg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.33.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"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.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.3.tgz",
|
||||
"integrity": "sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.0.1.tgz",
|
||||
"integrity": "sha512-Sh8bGQMEL0TAmAm2meAXMjcASHZa7V0xXQVDBLknCPa9TPtkY9yYs+0cnGGgfdkW0SV1Mlg+hVGfXcoI8d3MJA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
"date-fns": "^2.29.3",
|
||||
"lodash": "^4.17.21",
|
||||
"rxjs": "^7.8.0",
|
||||
"shell-quote": "^1.8.0",
|
||||
"spawn-command": "0.0.2-1",
|
||||
"supports-color": "^8.1.1",
|
||||
"tree-kill": "^1.2.2",
|
||||
"yargs": "^17.7.1"
|
||||
},
|
||||
"bin": {
|
||||
"conc": "dist/bin/concurrently.js",
|
||||
"concurrently": "dist/bin/concurrently.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.13.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "2.30.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/date-fns"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.33.0.tgz",
|
||||
"integrity": "sha512-aizyPE1Cj62vAECdph1iaMILpT0WUDCq3E6rW6I+dleSbBoGbktvJtzS6VHkZ4DKNEOG9qJpiom/ZxO+S15LAw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=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/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz",
|
||||
"integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/spawn-command": {
|
||||
"version": "0.0.2-1",
|
||||
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz",
|
||||
"integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
|
||||
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/wait-port": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.0.4.tgz",
|
||||
"integrity": "sha512-w8Ftna3h6XSFWWc2JC5gZEgp64nz8bnaTp5cvzbJSZ53j+omktWTDdwXxEF0jM8YveviLgFWvNGrSvRHnkyHyw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
"commander": "^9.3.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"bin": {
|
||||
"wait-port": "bin/wait-port.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
waspc/headless-test/package.json
Normal file
25
waspc/headless-test/package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "headless-test",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"example-app:start": "concurrently \"npm:example-app:start-db\" \"npm:example-app:start-app\" --kill-others-on-fail",
|
||||
"example-app:start-db": "npm run example-app:cleanup-db && cd ./examples/todoApp && cabal run wasp-cli start db",
|
||||
"example-app:start-app": "npm run example-app:wait-for-db && cd ./examples/todoApp && cabal run wasp-cli db migrate-dev && cabal run wasp-cli start",
|
||||
"example-app:wait-for-db": "sleep 5 && ./node_modules/.bin/wait-port 5432",
|
||||
"example-app:cleanup-db": "(docker container rm $(docker container ls -f name=^wasp-dev-db-todoApp- -q) -f || true) && docker volume rm $(docker volume ls -f name=^wasp-dev-db-todoApp- -q) -f || true"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.33.0",
|
||||
"concurrently": "^8.0.1",
|
||||
"prisma": "^4.12.0",
|
||||
"wait-port": "^1.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^4.12.0"
|
||||
}
|
||||
}
|
53
waspc/headless-test/playwright.config.ts
Normal file
53
waspc/headless-test/playwright.config.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: process.env.CI ? "dot" : "list",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://localhost:3000",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
/* Test against mobile viewports. */
|
||||
{
|
||||
name: "Mobile Chrome",
|
||||
use: { ...devices["Pixel 5"] },
|
||||
},
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: "npm run example-app:start",
|
||||
// Wait for the backend to start
|
||||
url: "http://localhost:3001",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120 * 1000,
|
||||
},
|
||||
});
|
51
waspc/headless-test/tests/simple.spec.ts
Normal file
51
waspc/headless-test/tests/simple.spec.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("has title", async ({ page }) => {
|
||||
await page.goto("http:/localhost:3000");
|
||||
|
||||
await expect(page).toHaveTitle(/ToDo App/);
|
||||
});
|
||||
test.describe("signup and login", () => {
|
||||
const randomEmail =
|
||||
"test" + Math.random().toString(36).substring(7) + "@test.com";
|
||||
const password = "12345678";
|
||||
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
test("can sign up", async ({ page }) => {
|
||||
await page.goto("/signup");
|
||||
|
||||
await page.waitForSelector("text=Create a new account");
|
||||
|
||||
await page.locator("input[type='email']").fill(randomEmail);
|
||||
await page.locator("input[type='password']").fill(password);
|
||||
await page.getByText("Sign up").click();
|
||||
|
||||
await expect(page).toHaveURL("/profile");
|
||||
});
|
||||
|
||||
test("can log in and create a task", async ({ page }) => {
|
||||
await page.goto("/login");
|
||||
|
||||
await page.waitForSelector("text=Log in to your account");
|
||||
|
||||
await page.locator("input[type='email']").fill(randomEmail);
|
||||
await page.locator("input[type='password']").fill("12345678xxx");
|
||||
await page.getByRole("button", { name: "Log in" }).click();
|
||||
|
||||
await expect(page.locator("body")).toContainText(`Invalid credentials`);
|
||||
|
||||
await page.locator("input[type='password']").fill(password);
|
||||
await page.getByRole("button", { name: "Log in" }).click();
|
||||
|
||||
await expect(page).toHaveURL("/profile");
|
||||
|
||||
await page.goto("/");
|
||||
|
||||
const randomTask = "New Task " + Math.random().toString(36).substring(7);
|
||||
await page.locator("input[type='text']").fill(randomTask);
|
||||
await page.getByText("Create new task").click();
|
||||
|
||||
await expect(page.locator("body")).toContainText(randomTask);
|
||||
});
|
||||
});
|
@ -70,8 +70,9 @@ async function setupClient(deploymentInfo: DeploymentInfo) {
|
||||
// This creates the fly.toml file, but does not attempt to deploy.
|
||||
await $`flyctl launch --no-deploy --name ${deploymentInfo.clientName} --region ${deploymentInfo.region}`;
|
||||
|
||||
// goStatic listens on port 8043 by default, but the default fly.toml assumes port 8080.
|
||||
replaceLineInLocalToml(/internal_port = 8080/g, 'internal_port = 8043');
|
||||
// goStatic listens on port 8043 by default, but the default fly.toml
|
||||
// assumes port 8080 (or 3000, depending on flyctl version).
|
||||
replaceLineInLocalToml(/internal_port = \d+/g, 'internal_port = 8043');
|
||||
|
||||
copyLocalClientTomlToProject(deploymentInfo.tomlFilePaths);
|
||||
|
||||
|
@ -36,7 +36,7 @@ Here is what it looks like live: https://waspleau.netlify.app/ There is also a s
|
||||
|
||||
So, what do we need to get started? First, we need a way to schedule and run jobs; for this, we decided to use [Bull](https://github.com/OptimalBits/bull). Ok, let’s wire it up. This should be easy, right? We can add external NPM dependencies in our Wasp files like so:
|
||||
|
||||
```css title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
app waspleau {
|
||||
title: "Waspleau",
|
||||
|
||||
@ -55,7 +55,7 @@ But where do we declare our queue and processing callback functions in Wasp? Uh
|
||||
|
||||
Thankfully, Waspleau can leverage a powerful and flexible [hook supplied by Wasp](https://wasp-lang.dev/docs/language/basic-elements#setupfn) called `server.setupFn`. This declares a JavaScript function that will be executed on server start. Yahoo! This means we can do things like the following:
|
||||
|
||||
```css title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
app waspleau {
|
||||
...
|
||||
|
||||
@ -130,7 +130,7 @@ _Note: Please see the [actual serverSetup.js file](https://github.com/wasp-lang/
|
||||
|
||||
We now have jobs running and data updating at regular intervals, nice, but we still need a way to send that data down the wire. Here, we expose the in-memory data from our `server.setupFn` module so our queries can also use it:
|
||||
|
||||
```css title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
...
|
||||
|
||||
query dashboard {
|
||||
|
@ -90,7 +90,7 @@ However, we will also continue to expand the number of job execution runtimes we
|
||||
|
||||
If you are a regular reader of this blog (thank you, you deserve a raise! 😊), you may recall we created an example app of a metrics dashboard called [Waspleau](https://wasp-lang.dev/blog/2022/01/27/waspleau) that used workers in the background to make periodic HTTP calls for data. In that example, we didn’t yet have access to recurring jobs in Wasp, so we used Bull for scheduled jobs instead. To set up our queue-related logic we had to have this huge `setupFn` wiring it all up; but now, we can remove all that code and simply use jobs instead! Here is what the new DSL looks like:
|
||||
|
||||
```js title=main.wasp
|
||||
```wasp title=main.wasp
|
||||
// A cron job for fetching GitHub stats
|
||||
job getGithubStats {
|
||||
executor: PgBoss,
|
||||
|
@ -172,7 +172,7 @@ Another thing to keep in mind is that we should also track how things change ove
|
||||
|
||||
If we try to apply the principles from above (less code, less detailed instructions, stating **what** we want instead of **how** it needs to be done), the code for auth might look something like this:
|
||||
|
||||
```css
|
||||
```wasp
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
|
@ -62,7 +62,7 @@ psl=}
|
||||
|
||||
### Step 2 - Update `app.auth` to use these items
|
||||
|
||||
```css title="./main.wasp"
|
||||
```wasp title="./main.wasp"
|
||||
app authExample {
|
||||
// ...
|
||||
auth: {
|
||||
|
@ -64,7 +64,7 @@ We already decided that we would pick an ORM(ish) solution for JS/TS which we wo
|
||||
|
||||
But then we looked at Prisma, and the winner was clear! Not only was Prisma taking care of everything that we cared about, but it had one additional feature that made it a perfect fit:
|
||||
|
||||
```
|
||||
```prisma
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
@ -82,8 +82,8 @@ So instead of implementing our own syntax for describing Entities, we decided to
|
||||
|
||||
Today, Entities are described like this in Wasp language:
|
||||
|
||||
```
|
||||
... some Wasp code ...
|
||||
```wasp
|
||||
// ... some Wasp code ...
|
||||
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
@ -91,7 +91,7 @@ entity User {=psl
|
||||
password String
|
||||
psl=}
|
||||
|
||||
... some Wasp code ...
|
||||
// ... some Wasp code ...
|
||||
```
|
||||
|
||||
So in the middle of Wasp, you just switch to writing PSL (Prisma Schema Language) to describe an entity!
|
||||
|
@ -48,7 +48,7 @@ To honor the tradition of demonstrating UIs using Todo apps, We'll show you how
|
||||
Before looking at our todo app in action, let's see how we've implemented it in Wasp.
|
||||
|
||||
These are the relevant declarations in our `.wasp` file:
|
||||
```javascript title=main.wasp
|
||||
```wasp title=main.wasp
|
||||
entity Task {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
description String
|
||||
|
@ -33,9 +33,7 @@ What makes Wasp unique is that it’s a framework that uses a super simple **lan
|
||||
|
||||
Check it out for yourself:
|
||||
|
||||
```c
|
||||
/* main.wasp */
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app todoApp {
|
||||
title: "ToDo App",/* visible in tab */
|
||||
|
||||
|
@ -173,7 +173,7 @@ Let's begin by creating the data models. Wasp uses Prisma under the hood to comm
|
||||
|
||||
I had to first declare all of the entities I needed with Prisma PSL in the Wasp config file.
|
||||
|
||||
```jsx
|
||||
```wasp
|
||||
entity Phrase {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
group String
|
||||
@ -228,7 +228,7 @@ Let’s now look at what I needed to do to get the data flowing from the backend
|
||||
|
||||
First, I declared a query in my Wasp config file:
|
||||
|
||||
```c
|
||||
```wasp
|
||||
app phraseTutor {
|
||||
...
|
||||
}
|
||||
@ -265,7 +265,7 @@ Let’s also add support for Google auth for our app. It involves declaring you
|
||||
|
||||
We declare it to the Wasp file by adding the `google` key under `auth`:
|
||||
|
||||
```jsx
|
||||
```wasp
|
||||
app phraseTutor {
|
||||
...
|
||||
auth: {
|
||||
|
@ -14,7 +14,7 @@ Based on your `main.wasp` file on the authentication providers you enabled, the
|
||||
|
||||
For example, if you only enabled e-mail authentication:
|
||||
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
//...
|
||||
@ -33,7 +33,7 @@ We'll get this:
|
||||
|
||||
And then we enable Google and Github:
|
||||
|
||||
```c title="main.wasp" {7-8}
|
||||
```wasp title="main.wasp" {7-8}
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
//...
|
||||
@ -60,7 +60,7 @@ Let's take a look at the components that are available for you to use.
|
||||
|
||||
### Login form
|
||||
|
||||
Useful for <span style={{ background: '#fee', padding: '0.2rem 0.5rem', borderRadius: '0.375rem' }}>username & password</span> and <span style={{ background: '#eef', padding: '0.2rem 0.5rem', borderRadius: '0.375rem' }}>email</span> authentication.
|
||||
Useful for <span className="pill pill-username-password">username & password</span> and <span className="pill pill-email">email</span> authentication.
|
||||
|
||||
![Login form](/img/authui/login.png)
|
||||
|
||||
@ -75,7 +75,7 @@ import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
### Signup form
|
||||
|
||||
Useful for <span style={{ background: '#fee', padding: '0.2rem 0.5rem', borderRadius: '0.375rem' }}>username & password</span> and <span style={{ background: '#eef', padding: '0.2rem 0.5rem', borderRadius: '0.375rem' }}>email</span> authentication.
|
||||
Useful for <span className="pill pill-username-password">username & password</span> and <span className="pill pill-email">email</span> authentication.
|
||||
|
||||
![Signup form](/img/authui/signup.png)
|
||||
|
||||
@ -90,7 +90,7 @@ import { SignupForm } from '@wasp/auth/forms/Signup'
|
||||
|
||||
### Forgot password form
|
||||
|
||||
Useful for <span style={{ background: '#eef', padding: '0.2rem 0.5rem', borderRadius: '0.375rem' }}>email</span> authentication.
|
||||
Useful for <span className="pill pill-email">email</span> authentication.
|
||||
|
||||
![Forgot password form](/img/authui/forgot_password.png)
|
||||
|
||||
@ -105,7 +105,7 @@ import { ForgotPasswordForm } from '@wasp/auth/forms/ForgotPassword'
|
||||
|
||||
### Reset password form
|
||||
|
||||
Useful for <span style={{ background: '#eef', padding: '0.2rem 0.5rem', borderRadius: '0.375rem' }}>email</span> authentication.
|
||||
Useful for <span className="pill pill-email">email</span> authentication.
|
||||
|
||||
![Reset password form](/img/authui/reset_password.png)
|
||||
|
||||
@ -120,7 +120,7 @@ import { ResetPasswordForm } from '@wasp/auth/forms/ResetPassword'
|
||||
|
||||
### Verify email form
|
||||
|
||||
Useful for <span style={{ background: '#eef', padding: '0.2rem 0.5rem', borderRadius: '0.375rem' }}>email</span> authentication.
|
||||
Useful for <span className="pill pill-email">email</span> authentication.
|
||||
|
||||
![Verify email form](/img/authui/email_verification.png)
|
||||
|
||||
|
@ -23,7 +23,7 @@ We'll need to take the following steps to set up email authentication:
|
||||
|
||||
Outline of the Wasp file we'll be working with:
|
||||
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
// Configuring e-mail authentication
|
||||
app myApp { ... }
|
||||
|
||||
@ -40,7 +40,7 @@ page SignupPage { ... }
|
||||
|
||||
Let's first set up the email authentication by adding the following to our `main.wasp` file:
|
||||
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.10.0"
|
||||
@ -79,7 +79,7 @@ app myApp {
|
||||
|
||||
Then we'll define the `User` entity in our `main.wasp` file:
|
||||
|
||||
```c title="main.wasp" {4-8}
|
||||
```wasp title="main.wasp" {4-8}
|
||||
// 5. Define the user entity
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
@ -99,7 +99,7 @@ Next, we need to define the routes and pages for the authentication pages. We'll
|
||||
|
||||
We'll add the following to our `main.wasp` file:
|
||||
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
// 6. Define the routes
|
||||
route SignupRoute { path: "/signup", to: SignupPage }
|
||||
page SignupPage {
|
||||
@ -133,7 +133,7 @@ We'll use SendGrid in this guide to send our e-mails. You can use any of the sup
|
||||
|
||||
To set up SendGrid to send emails, we will add the following to our `main.wasp` file:
|
||||
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
app myApp {
|
||||
...
|
||||
emailSender: {
|
||||
@ -224,7 +224,7 @@ By default, Wasp requires the e-mail to be verified before allowing the user to
|
||||
|
||||
Our setup looks like this:
|
||||
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
emailVerification: {
|
||||
clientRoute: EmailVerificationRoute,
|
||||
getEmailContentFn: import { getVerificationEmailContent } from "@server/auth/email.js",
|
||||
@ -233,7 +233,7 @@ emailVerification: {
|
||||
|
||||
When the user receives an e-mail, they receive a link that goes to the client route specified in the `clientRoute` field. In our case, this is the `EmailVerificationRoute` route we defined in the `main.wasp` file.
|
||||
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
|
||||
page EmailVerificationPage {
|
||||
component: import { EmailVerification } from "@client/pages/auth/EmailVerification.tsx",
|
||||
@ -294,7 +294,7 @@ Users can request a password and then they'll receive an e-mail with a link to r
|
||||
|
||||
Our setup in `main.wasp` looks like this:
|
||||
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
passwordReset: {
|
||||
clientRoute: PasswordResetRoute,
|
||||
getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js",
|
||||
@ -350,7 +350,7 @@ export const getPasswordResetEmailContent: GetPasswordResetEmailContentFn = ({
|
||||
|
||||
When the user receives an e-mail, they receive a link that goes to the client route specified in the `clientRoute` field. In our case, this is the `PasswordResetRoute` route we defined in the `main.wasp` file.
|
||||
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
|
||||
page PasswordResetPage {
|
||||
component: import { PasswordReset } from "@client/pages/auth/PasswordReset.tsx",
|
||||
|
@ -48,7 +48,7 @@ const defaultGlobalMiddleware: MiddlewareConfig = new Map([
|
||||
|
||||
If you would like to modify the middleware for _all_ operations and APIs, you can do something like:
|
||||
|
||||
```c title=todoApp.wasp
|
||||
```wasp title=todoApp.wasp
|
||||
app todoApp {
|
||||
// ...
|
||||
|
||||
@ -75,7 +75,7 @@ export const serverMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
|
||||
If you would like to modify the middleware for a single API, you can do something like:
|
||||
|
||||
```c title=todoApp.wasp
|
||||
```wasp title=todoApp.wasp
|
||||
api webhookCallback {
|
||||
fn: import { webhookCallback } from "@server/apis.js",
|
||||
middlewareConfigFn: import { webhookCallbackMiddlewareFn } from "@server/apis.js",
|
||||
@ -116,7 +116,7 @@ router.post('/webhook/callback', webhookCallbackMiddleware, ...)
|
||||
|
||||
If you would like to modify the middleware for all API routes under some common path, you can do something like:
|
||||
|
||||
```c title=todoApp.wasp
|
||||
```wasp title=todoApp.wasp
|
||||
apiNamespace fooBar {
|
||||
middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@server/apis.js",
|
||||
path: "/foo/bar"
|
||||
|
@ -8,7 +8,7 @@ import SendingEmailsInDevelopment from '../_sendingEmailsInDevelopment.md'
|
||||
|
||||
With Wasp's email-sending feature, you can easily integrate email functionality into your web application.
|
||||
|
||||
```js title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
@ -73,7 +73,7 @@ For each provider, you'll need to set up env variables in the `.env.server` file
|
||||
|
||||
First, set the provider to `SMTP` in your `main.wasp` file.
|
||||
|
||||
```js title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
@ -97,7 +97,7 @@ Many transactional email providers (e.g. Mailgun, SendGrid but also others) can
|
||||
|
||||
Set the provider to `Mailgun` in the `main.wasp` file.
|
||||
|
||||
```js title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
@ -125,7 +125,7 @@ MAILGUN_DOMAIN=
|
||||
|
||||
Set the provider field to `SendGrid` in your `main.wasp` file.
|
||||
|
||||
```js title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
|
@ -7,7 +7,7 @@ import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
# GitHub
|
||||
|
||||
To implement GitHub Auth, you'll need to add the Auth object with the following configuration to your `main.wasp` file:
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
wasp: {
|
||||
version: "^0.8.0"
|
||||
|
@ -7,7 +7,7 @@ import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
# Google
|
||||
|
||||
To implement Google Auth, you'll need to add the Auth object with the following configuration to your `main.wasp` file:
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
wasp: {
|
||||
version: "^0.8.0"
|
||||
|
@ -53,7 +53,7 @@ Let's say you want to build a web app that allows users to **create and share th
|
||||
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 immediatelly turn on the full-stack authentication via username and password:
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
app RecipeApp {
|
||||
title: "My Recipes",
|
||||
wasp: { version: "^0.10.0" },
|
||||
@ -67,7 +67,7 @@ app RecipeApp {
|
||||
|
||||
Let's then add the data models for your recipes. We will want to have Users and Users can own Recipes:
|
||||
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
...
|
||||
|
||||
entity User {=psl // Data models are defined using Prisma Schema Language.
|
||||
@ -92,7 +92,7 @@ We do that by defining Operations, in this case a Query `getRecipes` and Action
|
||||
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":
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
// Queries have automatic cache invalidation and are type-safe.
|
||||
query getRecipes {
|
||||
fn: import { getRecipes } from "@server/recipe.js",
|
||||
@ -127,11 +127,11 @@ 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:
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
...
|
||||
|
||||
route HomeRoute { path: "/", to: HomePage }
|
||||
component HomePage {
|
||||
page HomePage {
|
||||
component: import { HomePage } from "@client/pages/HomePage",
|
||||
authRequired: true // Will send user to /login if not authenticated.
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import SendingEmailsInDevelopment from '../_sendingEmailsInDevelopment.md'
|
||||
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.
|
||||
|
||||
```c
|
||||
```wasp
|
||||
app todoApp {
|
||||
wasp: {
|
||||
version: "^0.6.0"
|
||||
@ -67,7 +67,7 @@ Check [`app.emailSender`](/docs/language/features#email-sender) for more details
|
||||
|
||||
`page` declaration is the top-level layout abstraction. Your app can have multiple pages.
|
||||
|
||||
```c
|
||||
```wasp
|
||||
page MainPage {
|
||||
component: import Main from "@client/pages/Main",
|
||||
authRequired: false // optional
|
||||
@ -94,7 +94,7 @@ Check out this [section of our Todo app tutorial](/docs/tutorials/todo-app/06-au
|
||||
|
||||
`route` declaration provides top-level routing functionality in Wasp.
|
||||
|
||||
```css
|
||||
```wasp
|
||||
route AboutRoute { path: "/about", to: AboutPage }
|
||||
```
|
||||
|
||||
@ -109,7 +109,7 @@ Name of the `page` to which the path will lead.
|
||||
Referenced page must be defined somewhere in `.wasp` file.
|
||||
|
||||
### Example - parametrised URL path
|
||||
```css
|
||||
```wasp
|
||||
route TaskRoute { path: "/task/:id", to: TaskPage }
|
||||
```
|
||||
For details on URL path format check [React Router](https://reactrouter.com/web/)
|
||||
@ -121,7 +121,7 @@ Since Wasp under the hood generates code with [React Router](https://reactrouter
|
||||
the same rules apply when accessing URL params in your React components. Here is an example just to get you
|
||||
started:
|
||||
|
||||
```c title="todoApp.wasp"
|
||||
```wasp title="todoApp.wasp"
|
||||
// ...
|
||||
route TaskRoute { path: "/task/:id", to: TaskPage }
|
||||
page TaskPage {
|
||||
@ -147,7 +147,7 @@ export default Task
|
||||
Navigation can be performed from the React code via `<Link/>` component, also using the functionality of
|
||||
[React Router](https://reactrouter.com/web/):
|
||||
|
||||
```c title="todoApp.wasp"
|
||||
```wasp title="todoApp.wasp"
|
||||
// ...
|
||||
route HomeRoute { path: "/home", to: HomePage }
|
||||
page HomePage {
|
||||
@ -173,7 +173,7 @@ Wasp uses [Prisma](https://www.prisma.io/) to implement database functionality a
|
||||
|
||||
Each `Entity` declaration corresponds 1-to-1 to Prisma data model and is defined in a following way:
|
||||
|
||||
```css
|
||||
```wasp
|
||||
entity Task {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
description String
|
||||
@ -270,7 +270,7 @@ We'll leave this option aside for now. You can read more about it [here](#using-
|
||||
|
||||
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:
|
||||
```c title="pages/main.wasp"
|
||||
```wasp title="pages/main.wasp"
|
||||
// ...
|
||||
|
||||
// Again, it most likely makes sense to name the Wasp Query after
|
||||
@ -390,7 +390,7 @@ To prevent information leakage, the server won't forward these fields for any ot
|
||||
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:
|
||||
|
||||
```c {4,9} title="main.wasp"
|
||||
```wasp {4,9} title="main.wasp"
|
||||
|
||||
query fetchAllTasks {
|
||||
fn: import { getAllTasks } from "@server/queries.js",
|
||||
@ -436,7 +436,7 @@ export const sayHi = async () => {
|
||||
```
|
||||
Its corresponding declaration in Wasp:
|
||||
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
action sayHi {
|
||||
@ -463,7 +463,7 @@ export const updateTaskIsDone = ({ id, isDone }, context) => {
|
||||
})
|
||||
}
|
||||
```
|
||||
```c title=main.wasp
|
||||
```wasp title=main.wasp
|
||||
action updateTaskIsDone {
|
||||
fn: import { updateTaskIsDone } from "@server/actions.js",
|
||||
entities: [Task]
|
||||
@ -471,7 +471,7 @@ action updateTaskIsDone {
|
||||
```
|
||||
|
||||
And here is how you might use it:
|
||||
```js {4,18} title=src/client/pages/Task.js
|
||||
```jsx {4,18} title=src/client/pages/Task.js
|
||||
import React from 'react'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import fetchTask from '@wasp/queries/fetchTask'
|
||||
@ -591,15 +591,15 @@ 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
|
||||
}
|
||||
try {
|
||||
await context.entities.Task.create({...})
|
||||
} catch (e) {
|
||||
if (isPrismaError(e)) {
|
||||
throw prismaErrorToHttpError(e)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## APIs
|
||||
@ -639,7 +639,7 @@ export const fooBar : FooBar = (req, res, context) => {
|
||||
##### 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?
|
||||
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
api fooBar {
|
||||
fn: import { fooBar } from "@server/apis.js",
|
||||
entities: [Task],
|
||||
@ -672,7 +672,7 @@ We'll leave this option aside for now. You can read more about it [here](#using-
|
||||
- `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:
|
||||
```c title="pages/main.wasp"
|
||||
```wasp title="pages/main.wasp"
|
||||
// ...
|
||||
|
||||
api fooBar {
|
||||
@ -711,7 +711,7 @@ export const Foo = () => {
|
||||
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:
|
||||
|
||||
```c {3} title="main.wasp"
|
||||
```wasp {3} title="main.wasp"
|
||||
api fooBar {
|
||||
fn: import { fooBar } from "@server/apis.js",
|
||||
entities: [Task],
|
||||
@ -735,7 +735,7 @@ The object `context.entities.Task` exposes `prisma.task` from [Prisma's CRUD API
|
||||
|
||||
An `apiNamespace` is a simple declaration used to apply some `middlewareConfigFn` to all APIs under some specific path. For example:
|
||||
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
apiNamespace fooBar {
|
||||
middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@server/apis.js",
|
||||
path: "/foo/bar"
|
||||
@ -793,7 +793,7 @@ Keep in mind that pg-boss jobs run alongside your other server-side code, so the
|
||||
|
||||
To declare a `job` in Wasp, simply add a declaration with a reference to an `async` function, like the following:
|
||||
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
job mySpecialJob {
|
||||
executor: PgBoss,
|
||||
perform: {
|
||||
@ -822,7 +822,7 @@ Note that in our example, `foo` takes an argument, but this does not always have
|
||||
|
||||
If you have work that needs to be done on some recurring basis, you can add a `schedule` to your job declaration:
|
||||
|
||||
```c {6-9} title="main.wasp"
|
||||
```wasp {6-9} title="main.wasp"
|
||||
job mySpecialJob {
|
||||
executor: PgBoss,
|
||||
perform: {
|
||||
@ -840,7 +840,7 @@ In this example, you do _not_ need to invoke anything in JavaScript. You can ima
|
||||
### 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`.
|
||||
|
||||
```c
|
||||
```wasp
|
||||
job mySpecialJob {
|
||||
executor: PgBoss,
|
||||
perform: {
|
||||
@ -951,7 +951,7 @@ There will also be namespaced, job executor-specific objects.
|
||||
|
||||
You can specify additional npm dependencies via `dependencies` field in `app` declaration, in following way:
|
||||
|
||||
```c
|
||||
```wasp
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
@ -974,7 +974,7 @@ In the future, we will add support for picking any version you like, but we have
|
||||
|
||||
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:
|
||||
|
||||
```c
|
||||
```wasp
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
//...
|
||||
@ -1026,7 +1026,7 @@ Automatic redirect on successful login only works when using the Wasp provided [
|
||||
`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:
|
||||
|
||||
```c
|
||||
```wasp
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
//...
|
||||
@ -1213,7 +1213,7 @@ In the future, we will lift this limitation and enable smarter merging of accoun
|
||||
|
||||
`email` authentication method makes it possible to signup/login into the app by using an e-mail and a password.
|
||||
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
@ -1251,7 +1251,7 @@ This method requires that `userEntity` specified in `auth` contains:
|
||||
|
||||
#### Fields in the `email` dict
|
||||
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
@ -1401,7 +1401,7 @@ When using Social Login Providers, Wasp gives you the following options:
|
||||
<Tabs>
|
||||
<TabItem value="google" label="Google" default>
|
||||
|
||||
```c
|
||||
```wasp
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
@ -1422,7 +1422,7 @@ When using Social Login Providers, Wasp gives you the following options:
|
||||
</TabItem>
|
||||
<TabItem value="gitHub" label="GitHub">
|
||||
|
||||
```c
|
||||
```wasp
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
@ -1452,7 +1452,7 @@ It is also posslbe to [override the default](features#overrides-for-social-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:
|
||||
|
||||
```c {4,14}
|
||||
```wasp {4,14}
|
||||
//...
|
||||
auth: {
|
||||
userEntity: User,
|
||||
@ -1519,7 +1519,7 @@ Alternatively, you could add a `displayName` property to your User entity and as
|
||||
We also show you how to customize the configuration of the Provider's settings using:
|
||||
- the `configFn` function
|
||||
|
||||
```c title=main.wasp {9,10,13,14,26}
|
||||
```wasp title=main.wasp {9,10,13,14,26}
|
||||
app Example {
|
||||
//...
|
||||
|
||||
@ -1634,15 +1634,15 @@ 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
|
||||
}
|
||||
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
|
||||
@ -1726,7 +1726,7 @@ should be denied access to it.
|
||||
You can configure the client using the `client` field inside the `app`
|
||||
declaration,
|
||||
|
||||
```c
|
||||
```wasp
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
@ -1870,7 +1870,7 @@ explained in
|
||||
|
||||
Via `server` field of `app` declaration, you can configure behaviour of the Node.js server (one that is executing wasp operations).
|
||||
|
||||
```c
|
||||
```wasp
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
@ -1978,7 +1978,7 @@ console.log(process.env.DATABASE_URL)
|
||||
|
||||
Via `db` field of `app` declaration, you can configure the database used by Wasp.
|
||||
|
||||
```c
|
||||
```wasp
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
@ -2046,7 +2046,7 @@ Seeding is most commonly used for two following scenarios:
|
||||
|
||||
Wasp enables you to define multiple **seed functions** via `app.db.seeds`:
|
||||
|
||||
```c
|
||||
```wasp
|
||||
app MyApp {
|
||||
// ...
|
||||
db: {
|
||||
@ -2127,7 +2127,7 @@ Check out [our guide](/docs/guides/sending-emails#using-the-mailgun-provider) fo
|
||||
|
||||
You can optionally provide a default sender info that will be used when you don't provide it explicitly when sending an e-mail.
|
||||
|
||||
```c
|
||||
```wasp
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
|
@ -42,7 +42,7 @@ TodoApp/
|
||||
- shared/
|
||||
```
|
||||
|
||||
```css title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
app todoApp {
|
||||
wasp: {
|
||||
version: "^0.6.0"
|
||||
|
@ -8,7 +8,7 @@ Wasp is a declarative, statically typed, domain specific language (DSL).
|
||||
|
||||
The central point of Wasp language are **declarations**, and Wasp source is at the end just a bunch of declarations, each of them describing a part of your web app.
|
||||
|
||||
```c
|
||||
```wasp
|
||||
app MyApp {
|
||||
title: "My app"
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ Let's start with the `main.wasp` file, which introduces 3 new concepts:
|
||||
[page](language/features.md#page) and
|
||||
[route](language/features.md#route).
|
||||
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
app TodoApp { // Main declaration, defines a new web app.
|
||||
wasp: {
|
||||
version: "^0.10.0"
|
||||
|
@ -8,7 +8,7 @@ import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
[Entities](language/features.md#entity) are one of the very central concepts in Wasp, and they mainly play the role of data models.
|
||||
|
||||
Since our TodoApp is all about tasks, we will define a Task entity in Wasp:
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
entity Task {=psl
|
||||
|
@ -25,7 +25,7 @@ It consists of a declaration in Wasp and implementation in JS (in `src/server/`
|
||||
|
||||
### Wasp declaration
|
||||
Add the following code to `main.wasp`:
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
query getTasks {
|
||||
|
@ -15,7 +15,7 @@ Creating an action is very similar to creating a query.
|
||||
### Wasp declaration
|
||||
|
||||
First, we declare the action in Wasp:
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
action createTask {
|
||||
|
@ -16,7 +16,7 @@ For that, we will need to do two things:
|
||||
### Wasp declaration
|
||||
|
||||
We declare a Wasp action:
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
action updateTask {
|
||||
|
@ -18,7 +18,7 @@ Let's define a Todo list (luckily we have an app for that now ;)) to get this do
|
||||
|
||||
## Adding entity User
|
||||
First, let's define the `User` entity:
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
entity User {=psl
|
||||
@ -37,7 +37,7 @@ to propagate the schema change (we added User).
|
||||
## Defining `app.auth`
|
||||
Next, we want to tell Wasp that we want full-stack [authentication](language/features.md#authentication--authorization) in our app, and that it should use the `User` entity for it:
|
||||
|
||||
```c {7-16} title="main.wasp"
|
||||
```wasp {7-16} title="main.wasp"
|
||||
app TodoApp {
|
||||
wasp: {
|
||||
version: "^0.7.0"
|
||||
@ -76,7 +76,7 @@ To recap, so far we have defined:
|
||||
|
||||
When we defined `app.auth` we got login and signup forms generated for us, but now we have to create Login and Signup pages that use them. In our `main.wasp` file we'll add the following:
|
||||
|
||||
```c title="main.wasp"
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
route SignupRoute { path: "/signup", to: SignupPage }
|
||||
@ -141,7 +141,7 @@ Now, let's see how we're going to handle the situation when the user is not logg
|
||||
`MainPage` page is a private page and we want users to be able to see it only if they are authenticated.
|
||||
Wasp allows you to simply enforce private pages using the `authRequired` field:
|
||||
|
||||
```c {3} title="main.wasp"
|
||||
```wasp {3} title="main.wasp"
|
||||
// ...
|
||||
page MainPage {
|
||||
authRequired: true,
|
||||
@ -186,7 +186,7 @@ That is because we did not yet update queries and actions to work only on the cu
|
||||
## Defining User-Task relation in entities
|
||||
|
||||
First, let's define a one-to-many relation between User and Task (check the [prisma docs on relations](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/relations)):
|
||||
```c {6,13-14} title="main.wasp"
|
||||
```wasp {6,13-14} title="main.wasp"
|
||||
// ...
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
|
@ -10,7 +10,7 @@ What is a Todo app without some clocks!? Well, still a Todo app, but certainly n
|
||||
So, let's add a couple of clocks to our app, to help us track time while we perform our tasks (and to demonstrate the `app.dependencies` feature).
|
||||
|
||||
For this, we will use the `react-clock` library from NPM. We can add it to our project as a [dependency](language/features.md#dependencies) like this:
|
||||
```c {4-6} title="main.wasp"
|
||||
```wasp {4-6} title="main.wasp"
|
||||
app TodoApp {
|
||||
// ...
|
||||
|
||||
|
@ -35,7 +35,7 @@ Our scaffolding already includes TypeScript, so migrating your project to TypeSc
|
||||
|
||||
Let's first assume your Wasp file contains the following definitions:
|
||||
|
||||
```c title=main.wasp
|
||||
```wasp title=main.wasp
|
||||
entity Task {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
description String
|
||||
@ -103,7 +103,7 @@ You don't need to change anything inside the `.wasp` file.
|
||||
|
||||
Even when you use TypeScript, and your file is called `queries.ts`, you still need to import it using the `.js` extension:
|
||||
|
||||
```c
|
||||
```wasp
|
||||
query getTaskInfo {
|
||||
fn: import { getTaskInfo } from "@server/queries.js",
|
||||
entities: [Task]
|
||||
@ -166,7 +166,7 @@ The mentioned type safety mechanisms also apply here: changing the task entity i
|
||||
|
||||
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:
|
||||
|
||||
```c title=main.wasp
|
||||
```wasp title=main.wasp
|
||||
// ...
|
||||
|
||||
query GetTaskInfo {
|
||||
|
@ -1,5 +1,4 @@
|
||||
const lightCodeTheme = require("prism-react-renderer/themes/github");
|
||||
const darkCodeTheme = require("prism-react-renderer/themes/dracula");
|
||||
|
||||
/** @type {import('@docusaurus/types').DocusaurusConfig} */
|
||||
module.exports = {
|
||||
@ -82,6 +81,7 @@ module.exports = {
|
||||
},
|
||||
prism: {
|
||||
additionalLanguages: ["shell-session", "haskell"],
|
||||
theme: lightCodeTheme,
|
||||
},
|
||||
footer: {
|
||||
style: "dark",
|
||||
@ -157,7 +157,7 @@ module.exports = {
|
||||
// Please change this to your repo.
|
||||
blogSidebarCount: "ALL",
|
||||
blogSidebarTitle: "All our posts",
|
||||
postsPerPage: 'ALL',
|
||||
postsPerPage: "ALL",
|
||||
editUrl: "https://github.com/wasp-lang/wasp/edit/main/web/blog",
|
||||
},
|
||||
theme: {
|
||||
|
121
web/package-lock.json
generated
121
web/package-lock.json
generated
@ -20,11 +20,11 @@
|
||||
"plugin-image-zoom": "github:flexanalytics/plugin-image-zoom",
|
||||
"postcss": "^8.4.19",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-feather": "^2.0.10",
|
||||
"react-modal": "^3.14.3",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-tooltip": "^4.5.1",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"tailwindcss": "^3.2.4"
|
||||
@ -5844,17 +5844,6 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fault": {
|
||||
"version": "1.0.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"format": "^0.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/faye-websocket": {
|
||||
"version": "0.11.4",
|
||||
"license": "Apache-2.0",
|
||||
@ -6124,12 +6113,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/format": {
|
||||
"version": "0.2.2",
|
||||
"engines": {
|
||||
"node": ">=0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"license": "MIT",
|
||||
@ -6554,13 +6537,6 @@
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "10.7.3",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/history": {
|
||||
"version": "4.10.1",
|
||||
"license": "MIT",
|
||||
@ -7453,18 +7429,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lowlight": {
|
||||
"version": "1.20.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fault": "^1.0.0",
|
||||
"highlight.js": "~10.7.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"license": "ISC",
|
||||
@ -8965,7 +8929,8 @@
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.29.0",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
|
||||
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@ -9431,20 +9396,6 @@
|
||||
"react": ">=15"
|
||||
}
|
||||
},
|
||||
"node_modules/react-syntax-highlighter": {
|
||||
"version": "15.5.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"highlight.js": "^10.4.1",
|
||||
"lowlight": "^1.17.0",
|
||||
"prismjs": "^1.27.0",
|
||||
"refractor": "^3.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 0.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-textarea-autosize": {
|
||||
"version": "8.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.4.1.tgz",
|
||||
@ -9543,26 +9494,6 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/refractor": {
|
||||
"version": "3.6.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hastscript": "^6.0.0",
|
||||
"parse-entities": "^2.0.0",
|
||||
"prismjs": "~1.27.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/refractor/node_modules/prismjs": {
|
||||
"version": "1.27.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerate": {
|
||||
"version": "1.4.2",
|
||||
"license": "MIT"
|
||||
@ -16030,12 +15961,6 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"fault": {
|
||||
"version": "1.0.4",
|
||||
"requires": {
|
||||
"format": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"faye-websocket": {
|
||||
"version": "0.11.4",
|
||||
"requires": {
|
||||
@ -16203,9 +16128,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"format": {
|
||||
"version": "0.2.2"
|
||||
},
|
||||
"forwarded": {
|
||||
"version": "0.2.0"
|
||||
},
|
||||
@ -16468,9 +16390,6 @@
|
||||
"he": {
|
||||
"version": "1.2.0"
|
||||
},
|
||||
"highlight.js": {
|
||||
"version": "10.7.3"
|
||||
},
|
||||
"history": {
|
||||
"version": "4.10.1",
|
||||
"requires": {
|
||||
@ -16979,13 +16898,6 @@
|
||||
"lowercase-keys": {
|
||||
"version": "1.0.1"
|
||||
},
|
||||
"lowlight": {
|
||||
"version": "1.20.0",
|
||||
"requires": {
|
||||
"fault": "^1.0.0",
|
||||
"highlight.js": "~10.7.0"
|
||||
}
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"requires": {
|
||||
@ -17774,7 +17686,9 @@
|
||||
"requires": {}
|
||||
},
|
||||
"prismjs": {
|
||||
"version": "1.29.0"
|
||||
"version": "1.29.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
|
||||
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q=="
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1"
|
||||
@ -18074,16 +17988,6 @@
|
||||
"tiny-warning": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"react-syntax-highlighter": {
|
||||
"version": "15.5.0",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"highlight.js": "^10.4.1",
|
||||
"lowlight": "^1.17.0",
|
||||
"prismjs": "^1.27.0",
|
||||
"refractor": "^3.6.0"
|
||||
}
|
||||
},
|
||||
"react-textarea-autosize": {
|
||||
"version": "8.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.4.1.tgz",
|
||||
@ -18147,19 +18051,6 @@
|
||||
"minimatch": "^3.0.5"
|
||||
}
|
||||
},
|
||||
"refractor": {
|
||||
"version": "3.6.0",
|
||||
"requires": {
|
||||
"hastscript": "^6.0.0",
|
||||
"parse-entities": "^2.0.0",
|
||||
"prismjs": "~1.27.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"prismjs": {
|
||||
"version": "1.27.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"regenerate": {
|
||||
"version": "1.4.2"
|
||||
},
|
||||
|
@ -26,11 +26,11 @@
|
||||
"plugin-image-zoom": "github:flexanalytics/plugin-image-zoom",
|
||||
"postcss": "^8.4.19",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-feather": "^2.0.10",
|
||||
"react-modal": "^3.14.3",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-tooltip": "^4.5.1",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"tailwindcss": "^3.2.4"
|
||||
|
39
web/src/components/CodeHighlight.js
Normal file
39
web/src/components/CodeHighlight.js
Normal file
@ -0,0 +1,39 @@
|
||||
import React, { useEffect } from "react";
|
||||
import Prism from "prismjs";
|
||||
import "../css/prismjs-github-theme.css";
|
||||
|
||||
export default function CodeHighlight(props = {}) {
|
||||
const codeRef = React.createRef();
|
||||
const {
|
||||
prefixCls = "code-highlight-wrapper",
|
||||
className,
|
||||
language,
|
||||
source,
|
||||
children,
|
||||
...others
|
||||
} = props;
|
||||
const langCls = language ? `language-${language}` : "";
|
||||
async function highlight() {
|
||||
if (codeRef.current) {
|
||||
Prism.highlightElement(codeRef.current);
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
highlight();
|
||||
}, [language, source]);
|
||||
return (
|
||||
<pre
|
||||
className={`${prefixCls} ${className || ""} ${langCls}`}
|
||||
{...others}
|
||||
style={{
|
||||
borderBottomLeftRadius: "10px",
|
||||
borderBottomRightRadius: "10px",
|
||||
paddingLeft: "15px",
|
||||
}}
|
||||
>
|
||||
<code className={langCls} ref={codeRef}>
|
||||
{source || children}
|
||||
</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
import React from 'react'
|
||||
import Link from '@docusaurus/Link'
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter'
|
||||
import { qtcreatorLight, atomOneLight, atomOneDark, a11ylight } from 'react-syntax-highlighter/dist/cjs/styles/hljs'
|
||||
|
||||
import './prismCustomization'
|
||||
|
||||
import CodeHighlight from './CodeHighlight'
|
||||
|
||||
import { Terminal, ArrowUpRight, Play, BookOpen, Grid, Layout, Trello } from 'react-feather'
|
||||
|
||||
// Terminal, BookOpen, Grid, Layout, Trello, FileText
|
||||
@ -174,20 +177,12 @@ entity Task {=psl ... psl=} // Your Prisma data model.
|
||||
<div className='bg-yellow-500 h-2 w-2 rounded-full' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editor body */}
|
||||
<div className='w-full text-sm shadow-2xl rounded-b-md'>
|
||||
<SyntaxHighlighter
|
||||
language="javascript"
|
||||
style={atomOneLight}
|
||||
customStyle={{
|
||||
borderBottomLeftRadius: '10px',
|
||||
borderBottomRightRadius: '10px',
|
||||
paddingLeft: '15px',
|
||||
}}
|
||||
>
|
||||
{codeString}
|
||||
</SyntaxHighlighter>
|
||||
<CodeHighlight
|
||||
language='wasp'
|
||||
source={codeString}
|
||||
/>
|
||||
</div> {/* EOF code block wrapper */}
|
||||
</div> {/* EOF wrapper of header + code */}
|
||||
</div> {/* EOF col-span-6 */}
|
||||
|
6
web/src/components/prismCustomization.js
Normal file
6
web/src/components/prismCustomization.js
Normal file
@ -0,0 +1,6 @@
|
||||
import Prism from "prismjs";
|
||||
import addWaspLangauge from "../prism/wasp";
|
||||
import addPrismaLanguage from "../prism/prisma";
|
||||
|
||||
addPrismaLanguage(Prism);
|
||||
addWaspLangauge(Prism);
|
@ -5,7 +5,7 @@
|
||||
* work well for content-centric websites.
|
||||
*/
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap");
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@ -15,24 +15,43 @@
|
||||
* Issue on Tailwind's repo: https://github.com/tailwindlabs/tailwindcss/discussions/2984
|
||||
*/
|
||||
@layer base {
|
||||
html {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-font-smoothing: auto;
|
||||
}
|
||||
html {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-font-smoothing: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Docusaurus stuff */
|
||||
|
||||
.blog-list-page {
|
||||
background-color: var(--custom-blog-list-background-color);
|
||||
}
|
||||
|
||||
.pill {
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.pill.pill-username-password {
|
||||
background-color: var(--auth-ui-username-password-bg);
|
||||
}
|
||||
|
||||
.pill.pill-email {
|
||||
background-color: var(--auth-ui-email-bg);
|
||||
}
|
||||
|
||||
/* Highlight Prisma field types properly */
|
||||
.token.type-class-name {
|
||||
color: rgb(54, 172, 170);
|
||||
}
|
||||
.token.annotation {
|
||||
color: rgb(116, 116, 116) !important;
|
||||
}
|
||||
|
||||
/* You can override the default Infima variables here. */
|
||||
:root {
|
||||
/* Our custom values */
|
||||
--custom-background-color: #FDFDFD;
|
||||
--custom-background-color: #fdfdfd;
|
||||
--custom-background-color-diff: #f4f4f4;
|
||||
--custom-shadow-lw: 0 3px 5px 0px rgba(0, 0, 0, 0.1);
|
||||
--custom-border-radius: 3px;
|
||||
@ -52,8 +71,8 @@
|
||||
|
||||
/* Infima overrides */
|
||||
--ifm-container-width-xl: 1280px;
|
||||
--ifm-font-family-base: 'Inter';
|
||||
--ifm-color-primary: #BF9900; /* wasp color (ffcc00) darkened by 25% */
|
||||
--ifm-font-family-base: "Inter";
|
||||
--ifm-color-primary: #bf9900; /* wasp color (ffcc00) darkened by 25% */
|
||||
--ifm-color-primary-dark: #8a6f04;
|
||||
--ifm-color-primary-darker: rgb(31, 165, 136);
|
||||
--ifm-color-primary-darkest: rgb(26, 136, 112);
|
||||
@ -76,14 +95,19 @@
|
||||
--ifm-h2-font-size: 2rem;
|
||||
|
||||
--ifm-col-spacing-vertical: 0.5rem;
|
||||
--docusaurus-highlighted-code-line-bg: #e8edf2;
|
||||
--auth-ui-username-password-bg: #fee;
|
||||
--auth-ui-email-bg: #eef;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
--custom-background-color-diff: #2A2A2A;
|
||||
:root[data-theme="dark"] {
|
||||
--custom-background-color-diff: #2a2a2a;
|
||||
|
||||
/* Blog */
|
||||
--custom-blog-list-background-color: var(--ifm-background-color);
|
||||
--custom-blog-card-timestamp-color: #a3a3a3;
|
||||
--custom-blog-card-background-color: black;
|
||||
--docusaurus-highlighted-code-line-bg: #dee6ed;
|
||||
--auth-ui-username-password-bg: rgb(93, 57, 57);
|
||||
--auth-ui-email-bg: rgb(71, 71, 112);
|
||||
}
|
||||
|
||||
|
127
web/src/css/prismjs-github-theme.css
Normal file
127
web/src/css/prismjs-github-theme.css
Normal file
@ -0,0 +1,127 @@
|
||||
/**
|
||||
* GHColors theme by Avi Aryan (http://aviaryan.in)
|
||||
* Inspired by Github syntax coloring
|
||||
*/
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: #393a34;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
"Liberation Mono", "Courier New", monospace;
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.2em;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
pre > code[class*="language-"] {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::-moz-selection,
|
||||
pre[class*="language-"] ::-moz-selection,
|
||||
code[class*="language-"]::-moz-selection,
|
||||
code[class*="language-"] ::-moz-selection {
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::selection,
|
||||
pre[class*="language-"] ::selection,
|
||||
code[class*="language-"]::selection,
|
||||
code[class*="language-"] ::selection {
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
/* padding: 1em; */
|
||||
/* margin: 0.5em 0; */
|
||||
overflow: auto;
|
||||
/* border: 1px solid #dddddd; */
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*="language-"] {
|
||||
padding: 0.2em;
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
background: #f8f8f8;
|
||||
border: 1px solid #dddddd;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #999988;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.namespace {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.token.string,
|
||||
.token.attr-value {
|
||||
color: #e3116c;
|
||||
}
|
||||
|
||||
.token.punctuation,
|
||||
.token.operator {
|
||||
color: #393a34; /* no highlight */
|
||||
}
|
||||
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.token.symbol,
|
||||
.token.number,
|
||||
.token.boolean,
|
||||
.token.variable,
|
||||
.token.constant,
|
||||
.token.property,
|
||||
.token.regex,
|
||||
.token.inserted {
|
||||
color: #36acaa;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.keyword,
|
||||
.token.attr-name,
|
||||
.language-autohotkey .token.selector {
|
||||
color: #00009f;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.deleted,
|
||||
.language-autohotkey .token.tag {
|
||||
color: #9a050f;
|
||||
}
|
||||
|
||||
.token.tag,
|
||||
.token.selector,
|
||||
.language-autohotkey .token.keyword {
|
||||
color: #00009f;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.function,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
26
web/src/prism/prisma.js
Normal file
26
web/src/prism/prisma.js
Normal file
@ -0,0 +1,26 @@
|
||||
// Taken from the Prisma docs
|
||||
module.exports = (Prism) => {
|
||||
Prism.languages.prisma = Prism.languages.extend("clike", {
|
||||
keyword: /\b(?:datasource|enum|generator|model|type|view)\b/,
|
||||
"type-class-name": /(\s+)[A-Z]\w+/, ///(\b)(\s+)[A-Z]\w+/
|
||||
});
|
||||
|
||||
Prism.languages.javascript["class-name"][0].pattern =
|
||||
/(\b(?:model|datasource|enum|generator|type)\s+)[\w.\\]+/;
|
||||
|
||||
Prism.languages.insertBefore("prisma", "function", {
|
||||
annotation: {
|
||||
pattern: /(^|[^.])@+\w+/,
|
||||
lookbehind: true,
|
||||
alias: "punctuation",
|
||||
},
|
||||
});
|
||||
|
||||
Prism.languages.insertBefore("prisma", "punctuation", {
|
||||
"type-args": /\b(?:references|fields|onDelete|onUpdate):/,
|
||||
});
|
||||
|
||||
Prism.languages.insertBefore("prisma", "type-class-name", {
|
||||
"not-class": /\n(\s+)[A-Z]\w+/,
|
||||
});
|
||||
};
|
83
web/src/prism/wasp.js
Normal file
83
web/src/prism/wasp.js
Normal file
@ -0,0 +1,83 @@
|
||||
// Converted from the TextMate definition at https://github.com/wasp-lang/vscode-wasp/blob/main/syntaxes/wasp.tmLanguage.yaml
|
||||
module.exports = (Prism) => {
|
||||
Prism.languages.wasp = {
|
||||
"prisma-closure": {
|
||||
pattern: /{=psl[\s\S]*?psl=}/,
|
||||
inside: {
|
||||
prisma: {
|
||||
pattern: /[\s\S]+/,
|
||||
inside: Prism.languages.prisma,
|
||||
},
|
||||
},
|
||||
},
|
||||
comment: {
|
||||
pattern: /\/\/.*|\/\*[\s\S]*?\*\//,
|
||||
greedy: true,
|
||||
},
|
||||
"json-closure": {
|
||||
pattern: /{=json[\s\S]*?json=}/,
|
||||
inside: {
|
||||
punctuation: /[{}[\],]/,
|
||||
property: {
|
||||
pattern: /(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,
|
||||
lookbehind: true,
|
||||
greedy: true,
|
||||
},
|
||||
string: {
|
||||
pattern: /(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,
|
||||
lookbehind: true,
|
||||
greedy: true,
|
||||
},
|
||||
number: /-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,
|
||||
operator: /:/,
|
||||
boolean: /\b(?:false|true)\b/,
|
||||
null: {
|
||||
pattern: /\bnull\b/,
|
||||
alias: "keyword",
|
||||
},
|
||||
},
|
||||
},
|
||||
"js-import": {
|
||||
pattern: /import.*",?/,
|
||||
inside: Prism.languages.javascript,
|
||||
},
|
||||
string: {
|
||||
pattern: /"(?:\\.|[^\\"\r\n])*"/,
|
||||
greedy: true,
|
||||
},
|
||||
number: /-?\d+(?:\.\d+)?/,
|
||||
boolean: /\b(?:true|false)\b/,
|
||||
enum: {
|
||||
pattern:
|
||||
/\b(EmailAndPassword|PostgreSQL|SQLite|Simple|PgBoss|SMTP|SendGrid|Mailgun)\b/,
|
||||
alias: "constant",
|
||||
},
|
||||
"dict-key": {
|
||||
pattern: /[a-zA-Z]+(?=:)/,
|
||||
alias: "plain",
|
||||
},
|
||||
"declaration-type": {
|
||||
pattern: /\b(action|apiNamespace|api|app|entity|job|page|query|route)\b/,
|
||||
alias: "keyword",
|
||||
},
|
||||
"class-name": {
|
||||
pattern: /[a-zA-Z][0-9a-zA-Z]*/,
|
||||
alias: "variable",
|
||||
},
|
||||
"http-method": {
|
||||
pattern: /\b(ALL|GET|POST|PUT|DELETE)\b/,
|
||||
alias: "constant",
|
||||
},
|
||||
array: {
|
||||
pattern: /\[[\s\S]*?\]/,
|
||||
inside: {
|
||||
punctuation: /[{}[\],]/,
|
||||
value: {
|
||||
pattern: /[^,\s\]]+/,
|
||||
alias: "variable",
|
||||
},
|
||||
},
|
||||
},
|
||||
punctuation: /[{}[\],]/,
|
||||
};
|
||||
};
|
24
web/src/theme/prism-include-languages.js
Normal file
24
web/src/theme/prism-include-languages.js
Normal file
@ -0,0 +1,24 @@
|
||||
// This file gets auto-generated when you "eject" to add custom languages to Docosaurus
|
||||
// We use it to add support for Prisma and Wasp syntax highlighting
|
||||
import siteConfig from "@generated/docusaurus.config";
|
||||
export default function prismIncludeLanguages(PrismObject) {
|
||||
const {
|
||||
themeConfig: { prism },
|
||||
} = siteConfig;
|
||||
const { additionalLanguages } = prism;
|
||||
// Prism components work on the Prism instance on the window, while prism-
|
||||
// react-renderer uses its own Prism instance. We temporarily mount the
|
||||
// instance onto window, import components to enhance it, then remove it to
|
||||
// avoid polluting global namespace.
|
||||
// You can mutate PrismObject: registering plugins, deleting languages... As
|
||||
// long as you don't re-assign it
|
||||
globalThis.Prism = PrismObject;
|
||||
additionalLanguages.forEach((lang) => {
|
||||
// eslint-disable-next-line global-require, import/no-dynamic-require
|
||||
require(`prismjs/components/prism-${lang}`);
|
||||
});
|
||||
|
||||
require("./prism-prisma");
|
||||
require("./prism-wasp");
|
||||
delete globalThis.Prism;
|
||||
}
|
1
web/src/theme/prism-prisma.js
Normal file
1
web/src/theme/prism-prisma.js
Normal file
@ -0,0 +1 @@
|
||||
require("../prism/prisma")(Prism);
|
1
web/src/theme/prism-wasp.js
Normal file
1
web/src/theme/prism-wasp.js
Normal file
@ -0,0 +1 @@
|
||||
require("../prism/wasp")(Prism);
|
Loading…
Reference in New Issue
Block a user