Upgraded to Prisma 2.21 (stable migrations) + postgre can be used in local dev.

- `wasp db migrate-save` and `wasp db migrate-up` got replaced with `wasp db migrate-dev`.
- Wasp now has a declarative way to express which db is used, postgresql or sqlite: `db { system: PostgreSQL }`.
- Prisma is now at the latest version, 2.21. PSL parser was upgraded to work with this.
- PostgreSQL can now be used for local development.
- We migrated examples/realworld to work with this new version of Wasp.
This commit is contained in:
Martin Šošić 2021-04-21 14:06:25 +02:00 committed by GitHub
parent 57cd5a8a6f
commit f9e8f88b66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 411 additions and 1565 deletions

View File

@ -1 +1,2 @@
/.wasp/
.env

View File

@ -5,7 +5,19 @@ Realworld app
Here, we implement it in Wasp, by following their [specification](https://github.com/gothinkster/realworld/tree/master/spec).
Todo:
# Development
### Database
Wasp needs postgre database running - provide it with database connection URL via env var `DATABASE_URL` - best to do it via .env file.
Easy way to get going with postgresql database: run db with `docker run --rm --publish 5432:5432 -v postgresql-data:/var/lib/postgresql/data --env POSTGRES_PASSWORD=devpass postgres`.
`DATABASE_URL` in this case is `postgresql://postgres:devpass@localhost:5432/postgres`.
### Running
`wasp start`
## TODO
- [x] User + auth (JWT).
- [x] Login and signup pages.
- [x] Settings page with logout button (no user deletion needed).
@ -22,7 +34,7 @@ Todo:
- [x] Make tags work again (Prisma problems!).
- [x] Following other users.
- [x] Paginated lists of articles (on profile page, on home page).
- [ ] Implement design (use Bootstrap 4 styling?).
- [x] Implement design (use Bootstrap 4 styling?).
- [ ] Display proper error messages on login/signup.
- [ ] Improve error handling in React, we don't do a really good job there.

View File

@ -144,7 +144,20 @@ const ArticleEditor = (props) => {
onChange={e => setMarkdownContent(e.target.value)}
/>
{/* TODO(matija): For some reason Prisma dies if we two articles share tag, so we disabled adding tags for now. */}
<TextField
className={classes.textField}
label="Enter tags"
fullWidth
value={newTagName}
onChange={e => setNewTagName(e.target.value)}
onKeyPress={e => {
if (e.key === 'Enter') {
e.preventDefault()
setTags([...tags, { name: newTagName }])
setNewTagName('')
}
}}
/>
<div className={classes.tags}>
{ tags.map(tag => (

View File

@ -12,6 +12,10 @@ auth {
onAuthFailedRedirectTo: "/login"
}
db {
system: PostgreSQL
}
// ----------------- Pages ------------------ //
route "/" -> page Main

View File

@ -1,145 +0,0 @@
# Migration `20201127131647-init`
This migration has been generated by Martin Sosic at 11/27/2020, 2:16:47 PM.
You can check out the [state of the schema](./schema.prisma) after the migration.
## Database Steps
```sql
CREATE TABLE "User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"username" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
"bio" TEXT,
"profilePictureUrl" TEXT
)
CREATE TABLE "Article" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"slug" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"markdownContent" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE
)
CREATE TABLE "Comment" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"content" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"articleId" INTEGER NOT NULL,
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE
)
CREATE TABLE "ArticleTag" (
"name" TEXT NOT NULL,
PRIMARY KEY ("name")
)
CREATE TABLE "_FavoritedArticles" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL,
FOREIGN KEY ("A") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE
)
CREATE TABLE "_ArticleToArticleTag" (
"A" INTEGER NOT NULL,
"B" TEXT NOT NULL,
FOREIGN KEY ("A") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY ("B") REFERENCES "ArticleTag"("name") ON DELETE CASCADE ON UPDATE CASCADE
)
CREATE UNIQUE INDEX "User.username_unique" ON "User"("username")
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email")
CREATE UNIQUE INDEX "Article.slug_unique" ON "Article"("slug")
CREATE UNIQUE INDEX "_FavoritedArticles_AB_unique" ON "_FavoritedArticles"("A", "B")
CREATE INDEX "_FavoritedArticles_B_index" ON "_FavoritedArticles"("B")
CREATE UNIQUE INDEX "_ArticleToArticleTag_AB_unique" ON "_ArticleToArticleTag"("A", "B")
CREATE INDEX "_ArticleToArticleTag_B_index" ON "_ArticleToArticleTag"("B")
```
## Changes
```diff
diff --git schema.prisma schema.prisma
migration ..20201127131647-init
--- datamodel.dml
+++ datamodel.dml
@@ -1,0 +1,57 @@
+
+datasource db {
+ provider = "sqlite"
+ url = "***"
+}
+
+generator client {
+ provider = "prisma-client-js"
+ output = "../server/node_modules/.prisma/client"
+}
+
+model User {
+ id Int @id @default(autoincrement())
+ username String @unique
+ email String @unique
+ password String
+ bio String?
+ profilePictureUrl String?
+
+ articles Article[]
+ comments Comment[]
+ favoriteArticles Article[] @relation("FavoritedArticles")
+}
+
+model Article {
+ id Int @id @default(autoincrement())
+ slug String @unique
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ title String
+ description String
+ markdownContent String
+
+ user User @relation(fields: [userId], references: [id])
+ userId Int
+ comments Comment[]
+ tags ArticleTag[]
+ favoritedBy User[] @relation("FavoritedArticles")
+}
+
+model Comment {
+ id Int @id @default(autoincrement())
+ createdAt DateTime @default(now())
+ content String
+
+ user User @relation(fields: [userId], references: [id])
+ userId Int
+ article Article @relation(fields: [articleId], references: [id])
+ articleId Int
+}
+
+model ArticleTag {
+ name String @id
+
+ articles Article[]
+}
+
```

View File

@ -1,57 +0,0 @@
datasource db {
provider = "sqlite"
url = "***"
}
generator client {
provider = "prisma-client-js"
output = "../server/node_modules/.prisma/client"
}
model User {
id Int @id @default(autoincrement())
username String @unique
email String @unique
password String
bio String?
profilePictureUrl String?
articles Article[]
comments Comment[]
favoriteArticles Article[] @relation("FavoritedArticles")
}
model Article {
id Int @id @default(autoincrement())
slug String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String
description String
markdownContent String
user User @relation(fields: [userId], references: [id])
userId Int
comments Comment[]
tags ArticleTag[]
favoritedBy User[] @relation("FavoritedArticles")
}
model Comment {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
content String
user User @relation(fields: [userId], references: [id])
userId Int
article Article @relation(fields: [articleId], references: [id])
articleId Int
}
model ArticleTag {
name String @id
articles Article[]
}

View File

@ -1,633 +0,0 @@
{
"version": "0.3.14-fixed",
"steps": [
{
"tag": "CreateSource",
"source": "db"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Source",
"source": "db"
},
"argument": "provider",
"value": "\"sqlite\""
},
{
"tag": "CreateArgument",
"location": {
"tag": "Source",
"source": "db"
},
"argument": "url",
"value": "\"***\""
},
{
"tag": "CreateModel",
"model": "User"
},
{
"tag": "CreateField",
"model": "User",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "CreateField",
"model": "User",
"field": "username",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "username"
},
"directive": "unique"
}
},
{
"tag": "CreateField",
"model": "User",
"field": "email",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "email"
},
"directive": "unique"
}
},
{
"tag": "CreateField",
"model": "User",
"field": "password",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "User",
"field": "bio",
"type": "String",
"arity": "Optional"
},
{
"tag": "CreateField",
"model": "User",
"field": "profilePictureUrl",
"type": "String",
"arity": "Optional"
},
{
"tag": "CreateField",
"model": "User",
"field": "articles",
"type": "Article",
"arity": "List"
},
{
"tag": "CreateField",
"model": "User",
"field": "comments",
"type": "Comment",
"arity": "List"
},
{
"tag": "CreateField",
"model": "User",
"field": "favoriteArticles",
"type": "Article",
"arity": "List"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "favoriteArticles"
},
"directive": "relation"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "User",
"field": "favoriteArticles"
},
"directive": "relation"
},
"argument": "",
"value": "\"FavoritedArticles\""
},
{
"tag": "CreateModel",
"model": "Article"
},
{
"tag": "CreateField",
"model": "Article",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Article",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Article",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Article",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "CreateField",
"model": "Article",
"field": "slug",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Article",
"field": "slug"
},
"directive": "unique"
}
},
{
"tag": "CreateField",
"model": "Article",
"field": "createdAt",
"type": "DateTime",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Article",
"field": "createdAt"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Article",
"field": "createdAt"
},
"directive": "default"
},
"argument": "",
"value": "now()"
},
{
"tag": "CreateField",
"model": "Article",
"field": "updatedAt",
"type": "DateTime",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Article",
"field": "updatedAt"
},
"directive": "updatedAt"
}
},
{
"tag": "CreateField",
"model": "Article",
"field": "title",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "Article",
"field": "description",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "Article",
"field": "markdownContent",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "Article",
"field": "user",
"type": "User",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Article",
"field": "user"
},
"directive": "relation"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Article",
"field": "user"
},
"directive": "relation"
},
"argument": "fields",
"value": "[userId]"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Article",
"field": "user"
},
"directive": "relation"
},
"argument": "references",
"value": "[id]"
},
{
"tag": "CreateField",
"model": "Article",
"field": "userId",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "Article",
"field": "comments",
"type": "Comment",
"arity": "List"
},
{
"tag": "CreateField",
"model": "Article",
"field": "tags",
"type": "ArticleTag",
"arity": "List"
},
{
"tag": "CreateField",
"model": "Article",
"field": "favoritedBy",
"type": "User",
"arity": "List"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Article",
"field": "favoritedBy"
},
"directive": "relation"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Article",
"field": "favoritedBy"
},
"directive": "relation"
},
"argument": "",
"value": "\"FavoritedArticles\""
},
{
"tag": "CreateModel",
"model": "Comment"
},
{
"tag": "CreateField",
"model": "Comment",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Comment",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Comment",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Comment",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "CreateField",
"model": "Comment",
"field": "createdAt",
"type": "DateTime",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Comment",
"field": "createdAt"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Comment",
"field": "createdAt"
},
"directive": "default"
},
"argument": "",
"value": "now()"
},
{
"tag": "CreateField",
"model": "Comment",
"field": "content",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "Comment",
"field": "user",
"type": "User",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Comment",
"field": "user"
},
"directive": "relation"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Comment",
"field": "user"
},
"directive": "relation"
},
"argument": "fields",
"value": "[userId]"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Comment",
"field": "user"
},
"directive": "relation"
},
"argument": "references",
"value": "[id]"
},
{
"tag": "CreateField",
"model": "Comment",
"field": "userId",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "Comment",
"field": "article",
"type": "Article",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Comment",
"field": "article"
},
"directive": "relation"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Comment",
"field": "article"
},
"directive": "relation"
},
"argument": "fields",
"value": "[articleId]"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Comment",
"field": "article"
},
"directive": "relation"
},
"argument": "references",
"value": "[id]"
},
{
"tag": "CreateField",
"model": "Comment",
"field": "articleId",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateModel",
"model": "ArticleTag"
},
{
"tag": "CreateField",
"model": "ArticleTag",
"field": "name",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "ArticleTag",
"field": "name"
},
"directive": "id"
}
},
{
"tag": "CreateField",
"model": "ArticleTag",
"field": "articles",
"type": "Article",
"arity": "List"
}
]
}

View File

@ -1,48 +0,0 @@
# Migration `20201127145135-added-following-of-other-users`
This migration has been generated by Martin Sosic at 11/27/2020, 3:51:35 PM.
You can check out the [state of the schema](./schema.prisma) after the migration.
## Database Steps
```sql
CREATE TABLE "_FollowedUser" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL,
FOREIGN KEY ("A") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE
)
CREATE UNIQUE INDEX "_FollowedUser_AB_unique" ON "_FollowedUser"("A", "B")
CREATE INDEX "_FollowedUser_B_index" ON "_FollowedUser"("B")
```
## Changes
```diff
diff --git schema.prisma schema.prisma
migration 20201127131647-init..20201127145135-added-following-of-other-users
--- datamodel.dml
+++ datamodel.dml
@@ -1,8 +1,8 @@
datasource db {
provider = "sqlite"
- url = "***"
+ url = "***"
}
generator client {
provider = "prisma-client-js"
@@ -19,8 +19,10 @@
articles Article[]
comments Comment[]
favoriteArticles Article[] @relation("FavoritedArticles")
+ followedBy User[] @relation("FollowedUser", references: [id])
+ following User[] @relation("FollowedUser", references: [id])
}
model Article {
id Int @id @default(autoincrement())
```

View File

@ -1,59 +0,0 @@
datasource db {
provider = "sqlite"
url = "***"
}
generator client {
provider = "prisma-client-js"
output = "../server/node_modules/.prisma/client"
}
model User {
id Int @id @default(autoincrement())
username String @unique
email String @unique
password String
bio String?
profilePictureUrl String?
articles Article[]
comments Comment[]
favoriteArticles Article[] @relation("FavoritedArticles")
followedBy User[] @relation("FollowedUser", references: [id])
following User[] @relation("FollowedUser", references: [id])
}
model Article {
id Int @id @default(autoincrement())
slug String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String
description String
markdownContent String
user User @relation(fields: [userId], references: [id])
userId Int
comments Comment[]
tags ArticleTag[]
favoritedBy User[] @relation("FavoritedArticles")
}
model Comment {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
content String
user User @relation(fields: [userId], references: [id])
userId Int
article Article @relation(fields: [articleId], references: [id])
articleId Int
}
model ArticleTag {
name String @id
articles Article[]
}

View File

@ -1,97 +0,0 @@
{
"version": "0.3.14-fixed",
"steps": [
{
"tag": "CreateField",
"model": "User",
"field": "followedBy",
"type": "User",
"arity": "List"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "followedBy"
},
"directive": "relation"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "User",
"field": "followedBy"
},
"directive": "relation"
},
"argument": "",
"value": "\"FollowedUser\""
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "User",
"field": "followedBy"
},
"directive": "relation"
},
"argument": "references",
"value": "[id]"
},
{
"tag": "CreateField",
"model": "User",
"field": "following",
"type": "User",
"arity": "List"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "following"
},
"directive": "relation"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "User",
"field": "following"
},
"directive": "relation"
},
"argument": "",
"value": "\"FollowedUser\""
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "User",
"field": "following"
},
"directive": "relation"
},
"argument": "references",
"value": "[id]"
}
]
}

View File

@ -0,0 +1,115 @@
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"username" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
"bio" TEXT,
"profilePictureUrl" TEXT,
PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Article" (
"id" SERIAL NOT NULL,
"slug" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"markdownContent" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Comment" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"content" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"articleId" INTEGER NOT NULL,
PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ArticleTag" (
"name" TEXT NOT NULL,
PRIMARY KEY ("name")
);
-- CreateTable
CREATE TABLE "_FavoritedArticles" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL
);
-- CreateTable
CREATE TABLE "_FollowedUser" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL
);
-- CreateTable
CREATE TABLE "_ArticleToArticleTag" (
"A" INTEGER NOT NULL,
"B" TEXT NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "User.username_unique" ON "User"("username");
-- CreateIndex
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Article.slug_unique" ON "Article"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "_FavoritedArticles_AB_unique" ON "_FavoritedArticles"("A", "B");
-- CreateIndex
CREATE INDEX "_FavoritedArticles_B_index" ON "_FavoritedArticles"("B");
-- CreateIndex
CREATE UNIQUE INDEX "_FollowedUser_AB_unique" ON "_FollowedUser"("A", "B");
-- CreateIndex
CREATE INDEX "_FollowedUser_B_index" ON "_FollowedUser"("B");
-- CreateIndex
CREATE UNIQUE INDEX "_ArticleToArticleTag_AB_unique" ON "_ArticleToArticleTag"("A", "B");
-- CreateIndex
CREATE INDEX "_ArticleToArticleTag_B_index" ON "_ArticleToArticleTag"("B");
-- AddForeignKey
ALTER TABLE "Article" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Comment" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Comment" ADD FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_FavoritedArticles" ADD FOREIGN KEY ("A") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_FavoritedArticles" ADD FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_FollowedUser" ADD FOREIGN KEY ("A") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_FollowedUser" ADD FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ArticleToArticleTag" ADD FOREIGN KEY ("A") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ArticleToArticleTag" ADD FOREIGN KEY ("B") REFERENCES "ArticleTag"("name") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,4 +0,0 @@
# Prisma Migrate lockfile v1
20201127131647-init
20201127145135-added-following-of-other-users

View File

@ -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"

View File

@ -1,33 +1,34 @@
module Command.Db.Migrate
( migrateSave
, migrateUp
( migrateDev
, copyDbMigrationsDir
, MigrationDirCopyDirection(..)
) where
import Control.Monad.Catch (catch)
import Control.Monad.Except (throwError)
import Control.Monad.IO.Class (liftIO)
import qualified Path as P
import qualified Path.IO as PathIO
import Control.Monad.Catch (catch)
import Control.Monad.Except (throwError)
import Control.Monad.IO.Class (liftIO)
import qualified Path as P
import qualified Path.IO as PathIO
import StrongPath ((</>), Abs, Dir, Path)
import qualified StrongPath as SP
import Command (Command, CommandError(..))
import Command.Common (findWaspProjectRootDirFromCwd, waspSaysC)
import Command (Command, CommandError (..))
import Command.Common (findWaspProjectRootDirFromCwd,
waspSaysC)
import Common (WaspProjectDir)
import qualified Cli.Common
import Common (WaspProjectDir)
import StrongPath (Abs, Dir, Path, (</>))
import qualified StrongPath as SP
-- Wasp generator interface.
import Generator.Common (ProjectRootDir)
import Generator.DbGenerator (dbRootDirInProjectRootDir)
import qualified Generator.DbGenerator.Operations as DbOps
import Generator.DbGenerator (dbRootDirInProjectRootDir)
import Generator.Common (ProjectRootDir)
migrateSave :: String -> Command ()
migrateSave migrationName = do
migrateDev :: Command ()
migrateDev = do
waspProjectDir <- findWaspProjectRootDirFromCwd
let genProjectRootDir = waspProjectDir </> Cli.Common.dotWaspDirInWaspProjectDir
let genProjectRootDir = waspProjectDir
</> Cli.Common.dotWaspDirInWaspProjectDir
</> Cli.Common.generatedCodeDirInDotWaspDir
-- TODO(matija): It might make sense that this (copying migrations folder from source to
@ -35,66 +36,31 @@ migrateSave migrationName = do
-- considered part of a "source" code, then generator could take care of it and this command
-- wouldn't have to deal with it. We opened an issue on Github about this.
--
-- NOTE(matija): we need to copy migrations down before running "migrate save" to make sure
-- NOTE(matija): we need to copy migrations down before running "migrate dev" to make sure
-- all the latest migrations are in the generated project (e.g. Wasp dev checked out something
-- new) - otherwise "save" would create a new migration for that and we would end up with two
-- new) - otherwise "dev" would create a new migration for that and we would end up with two
-- migrations doing the same thing (which might result in conflict, e.g. during db creation).
waspSaysC "Copying migrations folder from Wasp to Prisma project..."
copyDbMigDirDownResult <- liftIO $ copyDbMigrationsDir CopyMigDirDown waspProjectDir
genProjectRootDir
case copyDbMigDirDownResult of
Nothing -> waspSaysC "Done."
Just err -> throwError $ CommandError $ "Copying migration folder failed: " ++ err
copyDbMigrationDir waspProjectDir genProjectRootDir CopyMigDirDown
waspSaysC "Checking for changes in schema to save..."
migrateSaveResult <- liftIO $ DbOps.migrateSave genProjectRootDir migrationName
case migrateSaveResult of
Left migrateSaveError -> throwError $ CommandError $ "Migrate save failed: "
++ migrateSaveError
Right () -> waspSaysC "Done."
waspSaysC "Performing migration..."
migrateResult <- liftIO $ DbOps.migrateDev genProjectRootDir
case migrateResult of
Left migrateError ->
throwError $ CommandError $ "Migrate dev failed: " <> migrateError
Right () -> waspSaysC "Migration done."
waspSaysC "Copying migrations folder from Prisma to Wasp project..."
copyDbMigDirUpResult <- liftIO $ copyDbMigrationsDir CopyMigDirUp waspProjectDir
genProjectRootDir
case copyDbMigDirUpResult of
Nothing -> waspSaysC "Done."
Just err -> throwError $ CommandError $ "Copying migration folder failed: " ++ err
applyAvailableMigrationsAndGenerateClient genProjectRootDir
copyDbMigrationDir waspProjectDir genProjectRootDir CopyMigDirUp
waspSaysC "All done!"
migrateUp :: Command ()
migrateUp = do
waspProjectDir <- findWaspProjectRootDirFromCwd
let genProjectRootDir = waspProjectDir </> Cli.Common.dotWaspDirInWaspProjectDir
</> Cli.Common.generatedCodeDirInDotWaspDir
waspSaysC "Copying migrations folder from Wasp to Prisma project..."
copyDbMigDirResult <- liftIO $ copyDbMigrationsDir CopyMigDirDown waspProjectDir
genProjectRootDir
case copyDbMigDirResult of
Nothing -> waspSaysC "Done."
Just err -> throwError $ CommandError $ "Copying migration folder failed: " ++ err
applyAvailableMigrationsAndGenerateClient genProjectRootDir
waspSaysC "All done!"
applyAvailableMigrationsAndGenerateClient :: Path Abs (Dir ProjectRootDir) -> Command ()
applyAvailableMigrationsAndGenerateClient genProjectRootDir = do
waspSaysC "Checking for migrations to apply..."
migrateUpResult <- liftIO $ DbOps.migrateUp genProjectRootDir
case migrateUpResult of
Left migrateUpError -> throwError $ CommandError $ "Migrate up failed: " ++ migrateUpError
Right () -> waspSaysC "Done."
waspSaysC "Generating Prisma client..."
genClientResult <- liftIO $ DbOps.generateClient genProjectRootDir
case genClientResult of
Left genClientError -> throwError $ CommandError $ "Generating client failed: " ++
genClientError
Right () -> waspSaysC "Done."
where
copyDbMigrationDir waspProjectDir genProjectRootDir copyDirection = do
copyDbMigDirResult <-
liftIO $ copyDbMigrationsDir copyDirection waspProjectDir genProjectRootDir
case copyDbMigDirResult of
Nothing -> waspSaysC "Done copying migrations folder."
Just err -> throwError $ CommandError $ "Copying migration folder failed: " ++ err
data MigrationDirCopyDirection = CopyMigDirUp | CopyMigDirDown deriving (Eq)

View File

@ -3,10 +3,10 @@ module Main where
import Control.Concurrent (threadDelay)
import qualified Control.Concurrent.Async as Async
import Control.Monad (void)
import Data.Char (isSpace)
import Data.Version (showVersion)
import Paths_waspc (version)
import System.Environment
import Data.Char (isSpace)
import Command (runCommand)
import Command.Build (build)
@ -15,7 +15,7 @@ import Command.Clean (clean)
import Command.Compile (compile)
import Command.CreateNewProject (createNewProject)
import Command.Db (runDbCommand, studio)
import Command.Db.Migrate (migrateSave, migrateUp)
import qualified Command.Db.Migrate
import Command.Start (start)
import qualified Command.Telemetry as Telemetry
import qualified Util.Terminal as Term
@ -74,7 +74,7 @@ printUsage = putStrLn $ unlines
, title "EXAMPLES"
, " wasp new MyApp"
, " wasp start"
, " wasp db migrate-save \"init\""
, " wasp db migrate-dev"
, ""
, Term.applyStyles [Term.Green] "Docs:" ++ " https://wasp-lang.dev/docs"
, Term.applyStyles [Term.Magenta] "Discord (chat):" ++ " https://discord.gg/rzdnErX"
@ -86,10 +86,9 @@ printVersion = putStrLn $ showVersion version
-- TODO(matija): maybe extract to a separate module, e.g. DbCli.hs?
dbCli :: [String] -> IO ()
dbCli args = case args of
["migrate-save", migrationName] -> runDbCommand $ migrateSave migrationName
["migrate-up"] -> runDbCommand migrateUp
["studio"] -> runDbCommand studio
_ -> printDbUsage
["migrate-dev"] -> runDbCommand Command.Db.Migrate.migrateDev
["studio"] -> runDbCommand studio
_ -> printDbUsage
printDbUsage :: IO ()
printDbUsage = putStrLn $ unlines
@ -97,13 +96,16 @@ printDbUsage = putStrLn $ unlines
, " wasp db <command> [command-args]"
, ""
, title "COMMANDS"
, cmd " migrate-save <migration-name> Saves a migration for updating to current schema."
, cmd " migrate-up Applies all migrations, updating your db to the current schema."
, cmd " studio GUI for inspecting your database."
, cmd (
" migrate-dev Ensures dev database corresponds to the current state of schema(entities):\n" <>
" - Generates a new migration if there are changes in the schema.\n" <>
" - Applies any pending migrations to the database."
)
, cmd " studio GUI for inspecting your database."
, ""
, title "EXAMPLES"
, " wasp db migrate-save \"Added description field.\""
, " wasp db migrate-up"
, " wasp db migrate-dev"
, " wasp db studio"
]
title :: String -> String

View File

@ -7,8 +7,8 @@
"scripts": {
"start": "nodemon -r dotenv/config ./src/server.js",
"debug": "DEBUG=server:* npm start",
"db-migrate-save": "prisma --experimental migrate save --schema=../db/schema.prisma",
"db-migrate": "prisma --experimental migrate up --schema=../db/schema.prisma",
"db-migrate-prod": "prisma migrate deploy --schema=../db/schema.prisma",
"db-migrate-dev": "prisma migrate dev --schema=../db/schema.prisma",
"start-production": "{=& startProductionScript =}",
"standard": "standard"
},
@ -22,6 +22,6 @@
"devDependencies": {
"nodemon": "^2.0.4",
"standard": "^14.3.4",
"@prisma/cli": "2.12.1"
"prisma": "2.21.0"
}
}

View File

@ -1,63 +0,0 @@
# Migration `20201028133236-init`
This migration has been generated by Matija Sosic at 10/28/2020, 2:32:36 PM.
You can check out the [state of the schema](./schema.prisma) after the migration.
## Database Steps
```sql
CREATE TABLE "User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL
)
CREATE TABLE "Task" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"description" TEXT NOT NULL,
"isDone" BOOLEAN NOT NULL DEFAULT false,
"userId" INTEGER NOT NULL,
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE
)
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email")
```
## Changes
```diff
diff --git schema.prisma schema.prisma
migration ..20201028133236-init
--- datamodel.dml
+++ datamodel.dml
@@ -1,0 +1,26 @@
+
+datasource db {
+ provider = "sqlite"
+ url = "***"
+}
+
+generator client {
+ provider = "prisma-client-js"
+ output = "../server/node_modules/.prisma/client"
+}
+
+model User {
+ id Int @id @default(autoincrement())
+ email String @unique
+ password String
+ tasks Task[]
+}
+
+model Task {
+ id Int @id @default(autoincrement())
+ description String
+ isDone Boolean @default(false)
+ user User @relation(fields: [userId], references: [id])
+ userId Int
+}
+
```

View File

@ -1,26 +0,0 @@
datasource db {
provider = "sqlite"
url = "***"
}
generator client {
provider = "prisma-client-js"
output = "../server/node_modules/.prisma/client"
}
model User {
id Int @id @default(autoincrement())
email String @unique
password String
tasks Task[]
}
model Task {
id Int @id @default(autoincrement())
description String
isDone Boolean @default(false)
user User @relation(fields: [userId], references: [id])
userId Int
}

View File

@ -1,245 +0,0 @@
{
"version": "0.3.14-fixed",
"steps": [
{
"tag": "CreateSource",
"source": "db"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Source",
"source": "db"
},
"argument": "provider",
"value": "\"sqlite\""
},
{
"tag": "CreateArgument",
"location": {
"tag": "Source",
"source": "db"
},
"argument": "url",
"value": "\"***\""
},
{
"tag": "CreateModel",
"model": "User"
},
{
"tag": "CreateField",
"model": "User",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "CreateField",
"model": "User",
"field": "email",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "email"
},
"directive": "unique"
}
},
{
"tag": "CreateField",
"model": "User",
"field": "password",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "User",
"field": "tasks",
"type": "Task",
"arity": "List"
},
{
"tag": "CreateModel",
"model": "Task"
},
{
"tag": "CreateField",
"model": "Task",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Task",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Task",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Task",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "CreateField",
"model": "Task",
"field": "description",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "Task",
"field": "isDone",
"type": "Boolean",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Task",
"field": "isDone"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Task",
"field": "isDone"
},
"directive": "default"
},
"argument": "",
"value": "false"
},
{
"tag": "CreateField",
"model": "Task",
"field": "user",
"type": "User",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Task",
"field": "user"
},
"directive": "relation"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Task",
"field": "user"
},
"directive": "relation"
},
"argument": "fields",
"value": "[userId]"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Task",
"field": "user"
},
"directive": "relation"
},
"argument": "references",
"value": "[id]"
},
{
"tag": "CreateField",
"model": "Task",
"field": "userId",
"type": "Int",
"arity": "Required"
}
]
}

View File

@ -1,3 +0,0 @@
# Prisma Migrate lockfile v1
20201028133236-init

View File

@ -6,6 +6,7 @@ module Generator.DbGenerator
import Data.Aeson (object, (.=))
import qualified Path as P
import Data.Maybe (fromMaybe)
import CompileOptions (CompileOptions)
import Generator.Common (ProjectRootDir)
@ -17,6 +18,7 @@ import StrongPath (Dir, File, Path, Rel, (</>))
import qualified StrongPath as SP
import Wasp (Wasp)
import qualified Wasp
import qualified Wasp.Db
import Wasp.Entity (Entity)
import qualified Wasp.Entity
@ -61,9 +63,13 @@ genPrismaSchema wasp = createTemplateFileDraft dstPath tmplSrcPath (Just templat
, "datasourceUrl" .= (datasourceUrl :: String)
]
isBuild = Wasp.getIsBuild wasp
(datasourceProvider, datasourceUrl) = if isBuild then ("postgresql", "env(\"DATABASE_URL\")")
else ("sqlite", "\"file:./dev.db\"")
dbSystem = fromMaybe Wasp.Db.SQLite $ Wasp.Db._system <$> Wasp.getDb wasp
(datasourceProvider, datasourceUrl) = case dbSystem of
Wasp.Db.PostgreSQL -> ("postgresql", "env(\"DATABASE_URL\")")
-- TODO: Report this error with some better mechanism, not `error`.
Wasp.Db.SQLite -> if Wasp.getIsBuild wasp
then error "SQLite is not supported in production. Set db.system to smth else."
else ("sqlite", "\"file:./dev.db\"")
entityToPslModelSchema :: Entity -> String
entityToPslModelSchema entity = Psl.Generator.Model.generateModel $

View File

@ -1,7 +1,5 @@
module Generator.DbGenerator.Jobs
( migrateSave
, migrateUp
, generateClient
( migrateDev
, runStudio
) where
@ -13,43 +11,16 @@ import qualified StrongPath as SP
import Generator.ServerGenerator.Common (serverRootDirInProjectRootDir)
import Generator.DbGenerator (dbSchemaFileInProjectRootDir)
-- | Runs `prisma migrate save` - creates migration folder for the latest schema changes.
migrateSave :: Path Abs (Dir ProjectRootDir) -> String -> J.Job
migrateSave projectDir migrationName = do
migrateDev :: Path Abs (Dir ProjectRootDir) -> J.Job
migrateDev projectDir = do
let serverDir = projectDir </> serverRootDirInProjectRootDir
let schemaFile = projectDir </> dbSchemaFileInProjectRootDir
-- NOTE(matija): We are running this command from server's root dir since that is where
-- Prisma packages (cli and client) are currently installed.
runNodeCommandAsJob serverDir "npx"
[ "prisma", "migrate", "save"
, "--schema", SP.toFilePath schemaFile
, "--name", migrationName
, "--create-db" -- Creates db if it doesn't already exist. Otherwise would stop and ask.
, "--experimental"
] J.Db
-- | Runs `prisma migrate up` - applies all the available migrations.
migrateUp :: Path Abs (Dir ProjectRootDir) -> J.Job
migrateUp projectDir = do
let serverDir = projectDir </> serverRootDirInProjectRootDir
let schemaFile = projectDir </> dbSchemaFileInProjectRootDir
runNodeCommandAsJob serverDir "npx"
[ "prisma", "migrate", "up"
, "--schema", SP.toFilePath schemaFile
, "--create-db" -- Creates db if it doesn't already exist. Otherwise would stop and ask.
, "--experimental"
] J.Db
-- | Runs `prisma generate` - (re)generates db client api.
generateClient :: Path Abs (Dir ProjectRootDir) -> J.Job
generateClient projectDir = do
let serverDir = projectDir </> serverRootDirInProjectRootDir
let schemaFile = projectDir </> dbSchemaFileInProjectRootDir
runNodeCommandAsJob serverDir "npx"
[ "prisma", "generate"
[ "prisma", "migrate", "dev"
, "--schema", SP.toFilePath schemaFile
] J.Db
@ -62,6 +33,4 @@ runStudio projectDir = do
runNodeCommandAsJob serverDir "npx"
[ "prisma", "studio"
, "--schema", SP.toFilePath schemaFile
, "--experimental"
] J.Db

View File

@ -1,7 +1,5 @@
module Generator.DbGenerator.Operations
( migrateSave
, migrateUp
, generateClient
( migrateDev
) where
import Control.Concurrent (Chan, newChan, readChan)
@ -22,32 +20,11 @@ printJobMsgsUntilExitReceived chan = do
J.JobOutput {} -> printJobMessage jobMsg >> printJobMsgsUntilExitReceived chan
J.JobExit {} -> return ()
-- | Checks for the changes in db schema file and creates and saves db migration info, but it
-- does not execute it.
migrateSave :: Path Abs (Dir ProjectRootDir) -> String -> IO (Either String ())
migrateSave projectDir migrationName = do
migrateDev :: Path Abs (Dir ProjectRootDir) -> IO (Either String ())
migrateDev projectDir = do
chan <- newChan
(_, dbExitCode) <- concurrently (printJobMsgsUntilExitReceived chan)
(DbJobs.migrateSave projectDir migrationName chan)
(DbJobs.migrateDev projectDir chan)
case dbExitCode of
ExitSuccess -> return (Right ())
ExitFailure code -> return $ Left $ "Migrate save failed with exit code: " ++ show code
migrateUp :: Path Abs (Dir ProjectRootDir) -> IO (Either String ())
migrateUp projectDir = do
chan <- newChan
(_, dbExitCode) <- concurrently (printJobMsgsUntilExitReceived chan)
(DbJobs.migrateUp projectDir chan)
case dbExitCode of
ExitSuccess -> return (Right ())
ExitFailure code -> return $ Left $ "Migrate up failed with exit code: " ++ show code
generateClient :: Path Abs (Dir ProjectRootDir) -> IO (Either String ())
generateClient projectDir = do
chan <- newChan
(_, dbExitCode) <- concurrently (printJobMsgsUntilExitReceived chan)
(DbJobs.generateClient projectDir chan)
case dbExitCode of
ExitSuccess -> return (Right ())
ExitFailure code -> return $ Left $ "Client generation failed with exit code: " ++ show code
ExitFailure code -> return $ Left $ "Migrate (dev) failed with exit code: " ++ show code

View File

@ -88,7 +88,7 @@ genPackageJson wasp waspDeps = C.makeTemplateFD
, "depsChunk" .= toPackageJsonDependenciesString (resolvedWaspDeps ++ resolvedUserDeps)
, "nodeVersion" .= nodeVersionAsText
, "startProductionScript" .= concat
[ if not (null $ Wasp.getPSLEntities wasp) then "npm run db-migrate && " else ""
[ if not (null $ Wasp.getPSLEntities wasp) then "npm run db-migrate-prod && " else ""
, "NODE_ENV=production node ./src/server.js"
]
])
@ -108,7 +108,7 @@ waspNpmDeps = ND.fromList
, ("debug", "~2.6.9")
, ("express", "~4.16.1")
, ("morgan", "~1.9.1")
, ("@prisma/client", "2.12.1")
, ("@prisma/client", "2.21.0")
, ("jsonwebtoken", "^8.5.1")
, ("secure-password", "^4.0.0")
, ("dotenv", "8.2.0")

View File

@ -28,6 +28,9 @@ reservedNameEntity = "entity"
reservedNameAuth :: String
reservedNameAuth = "auth"
reservedNameDb :: String
reservedNameDb = "db"
reservedNameQuery :: String
reservedNameQuery = "query"

View File

@ -11,6 +11,7 @@ import Lexer
import Parser.App (app)
import Parser.Auth (auth)
import Parser.Db (db)
import Parser.Route (route)
import Parser.Page (page)
import Parser.Entity (entity)
@ -26,6 +27,7 @@ waspElement
= waspElementApp
<|> waspElementAuth
<|> waspElementPage
<|> waspElementDb
<|> waspElementRoute
<|> waspElementEntity
<|> waspElementQuery
@ -38,6 +40,9 @@ waspElementApp = Wasp.WaspElementApp <$> app
waspElementAuth :: Parser Wasp.WaspElement
waspElementAuth = Wasp.WaspElementAuth <$> auth
waspElementDb :: Parser Wasp.WaspElement
waspElementDb = Wasp.WaspElementDb <$> db
waspElementPage :: Parser Wasp.WaspElement
waspElementPage = Wasp.WaspElementPage <$> page

36
waspc/src/Parser/Db.hs Normal file
View File

@ -0,0 +1,36 @@
module Parser.Db
( db
) where
import Text.Parsec.String (Parser)
import Text.Parsec ((<|>), try)
import Data.Maybe (listToMaybe, fromMaybe)
import qualified Wasp.Db
import qualified Parser.Common as P
import qualified Lexer as L
db :: Parser Wasp.Db.Db
db = do
L.reserved L.reservedNameDb
dbProperties <- P.waspClosure (L.commaSep1 dbProperty)
system <- fromMaybe (fail "'system' property is required!") $ return <$>
listToMaybe [p | DbPropertySystem p <- dbProperties]
return Wasp.Db.Db
{ Wasp.Db._system = system
}
data DbProperty
= DbPropertySystem Wasp.Db.DbSystem
dbProperty :: Parser DbProperty
dbProperty
= dbPropertySystem
dbPropertySystem :: Parser DbProperty
dbPropertySystem = DbPropertySystem <$> (P.waspProperty "system" dbPropertySystemValue)
where
dbPropertySystemValue = try (L.symbol "PostgreSQL" >> return Wasp.Db.PostgreSQL)
<|> try (L.symbol "SQLite" >> return Wasp.Db.SQLite)

View File

@ -50,10 +50,14 @@ getEntityFields (PslModel.Body pslElements) = map pslFieldToEntityField pslField
pslFieldTypeToScalar :: PslModel.FieldType -> Entity.Scalar
pslFieldTypeToScalar fType = case fType of
PslModel.String -> Entity.String
PslModel.Boolean -> Entity.Boolean
PslModel.Int -> Entity.Int
PslModel.Float -> Entity.Float
PslModel.DateTime -> Entity.DateTime
PslModel.Json -> Entity.Json
PslModel.UserType name -> Entity.UserType name
PslModel.String -> Entity.String
PslModel.Boolean -> Entity.Boolean
PslModel.Int -> Entity.Int
PslModel.BigInt -> Entity.BigInt
PslModel.Float -> Entity.Float
PslModel.Decimal -> Entity.Decimal
PslModel.DateTime -> Entity.DateTime
PslModel.Json -> Entity.Json
PslModel.Bytes -> Entity.Bytes
PslModel.UserType typeName -> Entity.UserType typeName
PslModel.Unsupported typeName -> Entity.Unsupported typeName

View File

@ -22,12 +22,27 @@ data Field = Field
}
deriving (Show, Eq)
data FieldType = String | Boolean | Int | Float | DateTime | Json | UserType String
data FieldType = String
| Boolean
| Int
| BigInt
| Float
| Decimal
| DateTime
| Json
| Bytes
| Unsupported String
| UserType String
deriving (Show, Eq)
data FieldTypeModifier = List | Optional
deriving (Show, Eq)
-- NOTE: We don't differentiate "native database type" attributes from normal attributes right now,
-- they are all represented with `data Attribute`.
-- We just represent them as a normal attribute with attrName being e.g. "db.VarChar".
-- TODO: In the future, we might want to be "smarter" about this and actually have a special representation
-- for them -> but let's see if that will be needed.
data Attribute = Attribute
{ _attrName :: String
, _attrArgs :: [AttributeArg]

View File

@ -23,13 +23,17 @@ generateElement (Ast.ElementBlockAttribute attribute) =
generateFieldType :: Ast.FieldType -> String
generateFieldType fieldType = case fieldType of
Ast.String -> "String"
Ast.Boolean -> "Boolean"
Ast.Int -> "Int"
Ast.Float -> "Float"
Ast.DateTime -> "DateTime"
Ast.Json -> "Json"
Ast.UserType label -> label
Ast.String -> "String"
Ast.Boolean -> "Boolean"
Ast.Int -> "Int"
Ast.BigInt -> "BigInt"
Ast.Float -> "Float"
Ast.Decimal -> "Decimal"
Ast.DateTime -> "DateTime"
Ast.Json -> "Json"
Ast.Bytes -> "Bytes"
Ast.UserType label -> label
Ast.Unsupported typeName -> "Unsupported(" ++ show typeName ++ ")"
generateFieldTypeModifier :: Ast.FieldTypeModifier -> String
generateFieldTypeModifier typeModifier = case typeModifier of
@ -49,12 +53,12 @@ generateAttributeArg (Ast.AttrArgUnnamed value) = generateAttrArgValue value
generateAttrArgValue :: Ast.AttrArgValue -> String
generateAttrArgValue value = case value of
Ast.AttrArgString strValue -> show strValue
Ast.AttrArgString strValue -> show strValue
Ast.AttrArgIdentifier identifier -> identifier
Ast.AttrArgFunc funcName -> funcName ++ "()"
Ast.AttrArgFieldRefList refs -> "[" ++ intercalate ", " refs ++ "]"
Ast.AttrArgNumber numberStr -> numberStr
Ast.AttrArgUnknown unknownStr -> unknownStr
Ast.AttrArgFunc funcName -> funcName ++ "()"
Ast.AttrArgFieldRefList refs -> "[" ++ intercalate ", " refs ++ "]"
Ast.AttrArgNumber numberStr -> numberStr
Ast.AttrArgUnknown unknownStr -> unknownStr
-- TODO: I should make sure to skip attributes that are not known in prisma.
-- Or maybe it would be better if that was done in previous step, where

View File

@ -61,11 +61,15 @@ field = do
[ ("String", Model.String)
, ("Boolean", Model.Boolean)
, ("Int", Model.Int)
, ("BigInt", Model.BigInt)
, ("Float", Model.Float)
, ("Decimal", Model.Decimal)
, ("DateTime", Model.DateTime)
, ("Json", Model.Json)
, ("Bytes", Model.Bytes)
]
)
<|> (try $ Model.Unsupported <$> (T.symbol lexer "Unsupported" >> T.parens lexer (T.stringLiteral lexer)))
<|> Model.UserType <$> T.identifier lexer
-- NOTE: As is Prisma currently implemented, there can be only one type modifier at one time: [] or ?.
@ -79,9 +83,22 @@ attribute :: Parser Model.Attribute
attribute = do
_ <- char '@'
name <- T.identifier lexer
-- NOTE: we support potential "selector" in order to support native database type attributes.
-- These have names with single . in them, like this: @db.VarChar(200), @db.TinyInt(1), ... .
-- We are not trying to be very smart here though: we don't check that "db" part matches
-- the name of the datasource block name (as it should), and we don't check that "VarChar" part is PascalCase
-- (as it should be) or that it is one of the valid values.
-- We just treat it as any other attribute, where "db.VarChar" becomes an attribute name.
-- In case that we wanted to be smarter, we could expand the AST to have special representation for it.
-- Also, we could do some additional checks here in parser (PascalCase), and some additional checks
-- in th generator ("db" matching the datasource block name).
maybeSelector <- optionMaybe $ try $ char '.' >> T.identifier lexer
maybeArgs <- optionMaybe (T.parens lexer (T.commaSep1 lexer (try attrArgument)))
return $ Model.Attribute
{ Model._attrName = name
{ Model._attrName = case maybeSelector of
Just selector -> name ++ "." ++ selector
Nothing -> name
, Model._attrArgs = fromMaybe [] maybeArgs
}

View File

@ -15,6 +15,8 @@ module Wasp
, getAuth
, getPSLEntities
, getDb
, module Wasp.Page
, getPages
, addPage
@ -49,6 +51,7 @@ import qualified Util as U
import qualified Wasp.Action
import Wasp.App
import qualified Wasp.Auth
import qualified Wasp.Db
import Wasp.Entity
import Wasp.JsImport
import Wasp.NpmDependencies (NpmDependencies)
@ -71,6 +74,7 @@ data Wasp = Wasp
data WaspElement
= WaspElementApp !App
| WaspElementAuth !Wasp.Auth.Auth
| WaspElementDb !Wasp.Db.Db
| WaspElementPage !Page
| WaspElementNpmDependencies !NpmDependencies
| WaspElementRoute !Route
@ -144,12 +148,21 @@ fromApp app = fromWaspElems [WaspElementApp app]
-- * Auth
getAuth :: Wasp -> Maybe Wasp.Auth.Auth
getAuth wasp = let auths = [a | WaspElementAuth a <- waspElements wasp] in
getAuth wasp = let auths = [a | WaspElementAuth a <- waspElements wasp] in
case auths of
[] -> Nothing
[a] -> Just a
_ -> error "Wasp can't contain more than one WaspElementAuth element!"
-- * Db
getDb :: Wasp -> Maybe Wasp.Db.Db
getDb wasp = let dbs = [db | WaspElementDb db <- waspElements wasp] in
case dbs of
[] -> Nothing
[db] -> Just db
_ -> error "Wasp can't contain more than one Db element!"
-- * NpmDependencies
getNpmDependencies :: Wasp -> NpmDependencies

13
waspc/src/Wasp/Db.hs Normal file
View File

@ -0,0 +1,13 @@
module Wasp.Db
( Db (..)
, DbSystem (..)
) where
data Db = Db
{ _system :: !DbSystem
} deriving (Show, Eq)
data DbSystem
= PostgreSQL
| SQLite
deriving (Show, Eq)

View File

@ -34,13 +34,17 @@ data Scalar
= String
| Boolean
| Int
| BigInt
| Float
| Decimal
| DateTime
| Json
| Bytes
-- | Name of the user-defined type.
-- This could be another entity, or maybe an enum,
-- we don't know here yet.
| UserType String
| Unsupported String
deriving (Show, Eq)
instance ToJSON Entity where

View File

@ -0,0 +1,24 @@
module Parser.DbTest where
import Test.Tasty.Hspec
import Data.Either (isLeft)
import Parser.Common (runWaspParser)
import Parser.Db (db)
import qualified Wasp.Db
spec_parseDb :: Spec
spec_parseDb =
describe "Parsing db declaration" $ do
let parseDb input = runWaspParser db input
it "When given a valid db declaration, returns correct AST" $ do
parseDb "db { system: PostgreSQL }"
`shouldBe` Right (Wasp.Db.Db { Wasp.Db._system = Wasp.Db.PostgreSQL })
parseDb "db { system: SQLite }"
`shouldBe` Right (Wasp.Db.Db { Wasp.Db._system = Wasp.Db.SQLite })
it "When given db wasp declaration without 'db', should return Left" $ do
isLeft (parseDb "db { }") `shouldBe` True

View File

@ -49,7 +49,6 @@ spec_parsePage =
, Wasp.Page._component = expectedPageComponentImport
, Wasp.Page._authRequired = Just True
})
it "When given page wasp declaration without 'page', should return Left" $ do
isLeft (parsePage "Landing { component: import Main from \"@ext/pages/Main\" }") `shouldBe` True

View File

@ -8,8 +8,9 @@ sampleBodySchema :: String
sampleBodySchema =
unlines
[ " id Int @id @default(value: autoincrement())"
, " email String?"
, " email String? @db.VarChar(200)"
, " posts Post[] @relation(\"UserPosts\", references: [id]) @customattr"
, " weirdType Unsupported(\"weird\")"
, ""
, " @@someattr([id, email], 2 + 4, [posts])"
]
@ -42,7 +43,14 @@ sampleBodyAst =
{ AST._name = "email"
, AST._type = AST.String
, AST._typeModifiers = [AST.Optional]
, AST._attrs = []
, AST._attrs =
[ AST.Attribute
{ AST._attrName = "db.VarChar"
, AST._attrArgs =
[ AST.AttrArgUnnamed (AST.AttrArgNumber "200")
]
}
]
}
)
, AST.ElementField
@ -65,6 +73,14 @@ sampleBodyAst =
]
}
)
, AST.ElementField
( AST.Field
{ AST._name = "weirdType"
, AST._type = AST.Unsupported "weird"
, AST._typeModifiers = []
, AST._attrs = []
}
)
, AST.ElementBlockAttribute
( AST.Attribute
{ AST._attrName = "someattr"

View File

@ -1,4 +1,5 @@
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Psl.Generator.ModelTest where
@ -24,9 +25,6 @@ prop_generatePslModel :: Property
prop_generatePslModel = mapSize (const 100) $ \modelAst -> within 1000000 $
runWaspParser Psl.Parser.Model.model (generateModel modelAst) `shouldBe` Right modelAst
-- TODO: Figure out what to do with these orphand Arbitrary instances.
-- Should they go into src/Psl/Ast/Model.hs?
instance Arbitrary AST.Model where
arbitrary = AST.Model <$> arbitraryIdentifier <*> arbitrary
@ -59,9 +57,13 @@ instance Arbitrary AST.FieldType where
[ return AST.String
, return AST.Boolean
, return AST.Int
, return AST.BigInt
, return AST.Float
, return AST.Decimal
, return AST.DateTime
, return AST.Json
, return AST.Bytes
, AST.Unsupported . show <$> arbitraryIdentifier
, AST.UserType <$> arbitraryIdentifier ]
instance Arbitrary AST.FieldTypeModifier where
@ -69,7 +71,10 @@ instance Arbitrary AST.FieldTypeModifier where
instance Arbitrary AST.Attribute where
arbitrary = do
name <- arbitraryIdentifier
name <- frequency
[ (2, arbitraryIdentifier)
, (1, ("db." ++) <$> arbitraryIdentifier)
]
args <- scale (const 5) arbitrary
return $ AST.Attribute { AST._attrName = name, AST._attrArgs = args }