From d3f5708a1903cb7a4df095a44b473cb7548e4001 Mon Sep 17 00:00:00 2001 From: Gerome Grignon Date: Fri, 16 Aug 2024 16:27:43 +0200 Subject: [PATCH] feat: init rest-analog project --- typescript/rest-analog/.editorconfig | 16 + typescript/rest-analog/.gitignore | 44 +++ typescript/rest-analog/README.md | 291 ++++++++++++++++++ typescript/rest-analog/angular.json | 57 ++++ typescript/rest-analog/index.html | 15 + typescript/rest-analog/package.json | 57 ++++ typescript/rest-analog/prisma/db.ts | 16 + typescript/rest-analog/prisma/schema.prisma | 27 ++ typescript/rest-analog/prisma/seed.js | 72 +++++ typescript/rest-analog/public/.gitkeep | 0 typescript/rest-analog/public/favicon.ico | Bin 0 -> 948 bytes typescript/rest-analog/src/app/app.analog | 11 + .../rest-analog/src/app/app.config.server.ts | 10 + typescript/rest-analog/src/app/app.config.ts | 20 ++ .../src/app/components/header.analog | 55 ++++ .../app/components/post/post.component.css | 13 + .../app/components/post/post.component.html | 3 + .../components/post/post.component.spec.ts | 31 ++ .../src/app/components/post/post.component.ts | 13 + .../src/app/models/author.model.ts | 3 + .../rest-analog/src/app/models/post.model.ts | 9 + .../src/app/pages/create/index.page.ts | 136 ++++++++ .../src/app/pages/drafts/index.page.ts | 71 +++++ .../src/app/pages/drafts/index.server.ts | 6 + .../rest-analog/src/app/pages/index.page.ts | 66 ++++ .../rest-analog/src/app/pages/index.server.ts | 6 + .../src/app/pages/post/[id].page.ts | 108 +++++++ .../src/app/pages/post/[id].server.ts | 9 + .../src/app/pages/signup/index.page.ts | 95 ++++++ .../src/app/services/auth.service.ts | 17 + .../src/app/services/post.service.ts | 21 ++ typescript/rest-analog/src/main.server.ts | 32 ++ typescript/rest-analog/src/main.ts | 7 + .../rest-analog/src/server/routes/author.ts | 17 + .../src/server/routes/draft-list.get.ts | 18 ++ .../rest-analog/src/server/routes/feed.get.ts | 18 ++ .../src/server/routes/filterPosts.get.ts | 28 ++ .../src/server/routes/post/[id].delete.ts | 18 ++ .../src/server/routes/post/[id].get.ts | 25 ++ .../src/server/routes/post/index.ts | 24 ++ .../src/server/routes/publish/[id].put.ts | 20 ++ .../rest-analog/src/server/routes/user.ts | 18 ++ .../rest-analog/src/server/routes/v1/hello.ts | 3 + typescript/rest-analog/src/styles.css | 31 ++ typescript/rest-analog/src/test-setup.ts | 12 + typescript/rest-analog/src/vite-env.d.ts | 1 + typescript/rest-analog/tsconfig.app.json | 10 + typescript/rest-analog/tsconfig.json | 31 ++ typescript/rest-analog/tsconfig.spec.json | 11 + typescript/rest-analog/vite.config.ts | 34 ++ 50 files changed, 1656 insertions(+) create mode 100644 typescript/rest-analog/.editorconfig create mode 100644 typescript/rest-analog/.gitignore create mode 100644 typescript/rest-analog/README.md create mode 100644 typescript/rest-analog/angular.json create mode 100644 typescript/rest-analog/index.html create mode 100644 typescript/rest-analog/package.json create mode 100644 typescript/rest-analog/prisma/db.ts create mode 100644 typescript/rest-analog/prisma/schema.prisma create mode 100644 typescript/rest-analog/prisma/seed.js create mode 100644 typescript/rest-analog/public/.gitkeep create mode 100644 typescript/rest-analog/public/favicon.ico create mode 100644 typescript/rest-analog/src/app/app.analog create mode 100644 typescript/rest-analog/src/app/app.config.server.ts create mode 100644 typescript/rest-analog/src/app/app.config.ts create mode 100644 typescript/rest-analog/src/app/components/header.analog create mode 100644 typescript/rest-analog/src/app/components/post/post.component.css create mode 100644 typescript/rest-analog/src/app/components/post/post.component.html create mode 100644 typescript/rest-analog/src/app/components/post/post.component.spec.ts create mode 100644 typescript/rest-analog/src/app/components/post/post.component.ts create mode 100644 typescript/rest-analog/src/app/models/author.model.ts create mode 100644 typescript/rest-analog/src/app/models/post.model.ts create mode 100644 typescript/rest-analog/src/app/pages/create/index.page.ts create mode 100644 typescript/rest-analog/src/app/pages/drafts/index.page.ts create mode 100644 typescript/rest-analog/src/app/pages/drafts/index.server.ts create mode 100644 typescript/rest-analog/src/app/pages/index.page.ts create mode 100644 typescript/rest-analog/src/app/pages/index.server.ts create mode 100644 typescript/rest-analog/src/app/pages/post/[id].page.ts create mode 100644 typescript/rest-analog/src/app/pages/post/[id].server.ts create mode 100644 typescript/rest-analog/src/app/pages/signup/index.page.ts create mode 100644 typescript/rest-analog/src/app/services/auth.service.ts create mode 100644 typescript/rest-analog/src/app/services/post.service.ts create mode 100644 typescript/rest-analog/src/main.server.ts create mode 100644 typescript/rest-analog/src/main.ts create mode 100644 typescript/rest-analog/src/server/routes/author.ts create mode 100644 typescript/rest-analog/src/server/routes/draft-list.get.ts create mode 100644 typescript/rest-analog/src/server/routes/feed.get.ts create mode 100644 typescript/rest-analog/src/server/routes/filterPosts.get.ts create mode 100644 typescript/rest-analog/src/server/routes/post/[id].delete.ts create mode 100644 typescript/rest-analog/src/server/routes/post/[id].get.ts create mode 100644 typescript/rest-analog/src/server/routes/post/index.ts create mode 100644 typescript/rest-analog/src/server/routes/publish/[id].put.ts create mode 100644 typescript/rest-analog/src/server/routes/user.ts create mode 100644 typescript/rest-analog/src/server/routes/v1/hello.ts create mode 100644 typescript/rest-analog/src/styles.css create mode 100644 typescript/rest-analog/src/test-setup.ts create mode 100644 typescript/rest-analog/src/vite-env.d.ts create mode 100644 typescript/rest-analog/tsconfig.app.json create mode 100644 typescript/rest-analog/tsconfig.json create mode 100644 typescript/rest-analog/tsconfig.spec.json create mode 100644 typescript/rest-analog/vite.config.ts diff --git a/typescript/rest-analog/.editorconfig b/typescript/rest-analog/.editorconfig new file mode 100644 index 000000000000..59d9a3a3e73f --- /dev/null +++ b/typescript/rest-analog/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/typescript/rest-analog/.gitignore b/typescript/rest-analog/.gitignore new file mode 100644 index 000000000000..bf149eb7cc0e --- /dev/null +++ b/typescript/rest-analog/.gitignore @@ -0,0 +1,44 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +/.nx/cache +/.nx/workspace-data +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/typescript/rest-analog/README.md b/typescript/rest-analog/README.md new file mode 100644 index 000000000000..a49027b883c6 --- /dev/null +++ b/typescript/rest-analog/README.md @@ -0,0 +1,291 @@ +# Fullstack Example with Analog (REST API) + +This example shows how to implement a **fullstack app with [Analog](https://analogjs.org/)** using [Prisma Client](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client). It uses a SQLite database file with some initial dummy data which you can find at [`./prisma/dev.db`](./prisma/dev.db). + +## Getting started + +### 1. Download example and install dependencies + +Download this example: + +``` +npx try-prisma@latest --template typescript/rest-analog +``` + +Install npm dependencies: + +``` +cd rest-analog +npm install +``` + +
Alternative: Clone the entire repo + +Clone this repository: + +``` +git clone git@github.com:prisma/prisma-examples.git --depth=1 +``` + +Install npm dependencies: + +``` +cd prisma-examples/typescript/rest-analog +npm install +``` + +
+ +### 2. Create and seed the database + +Run the following command to create your SQLite database file. This also creates the `User` and `Post` tables that are defined in [`prisma/schema.prisma`](./prisma/schema.prisma): + +``` +npx prisma migrate dev --name init +``` + +When `npx prisma migrate dev` is executed against a newly created database, seeding is also triggered. The seed file in [`prisma/seed.ts`](./prisma/seed.ts) will be executed and your database will be populated with the sample data. + +### 3. Start the app + +``` +npm run dev +``` + +The app is now running, navigate to [`http://localhost:5173/`](http://localhost:5173/) in your browser to explore its UI. + +
Expand for a tour through the UI of the app + +
+ +**Blog** (located in [`./src/app/pages/index.page.ts`](./src/app/pages/index.page.ts) + +![](https://imgur.com/eepbOUO.png) + +**Signup** (located in [`./rc/app/pages/signup.page.ts`](./src/app/pages/signup.page.ts)) + +![](https://imgur.com/iE6OaBI.png) + +**Create post (draft)** (located in [`./src/app/pages/create.page.ts`](./src/app/pages/create.page.ts)) + +![](https://imgur.com/olCWRNv.png) + +**Drafts** (located in [`./src/app/pages/drafts.page.ts`](./src/app/pages/drafts.page.ts)) + +![](https://imgur.com/PSMzhcd.png) + +**View post** (located in [`./src/app/pages/p/_id.vue`](src/app/pages/post/_id.vue)) (delete or publish here) + +![](https://imgur.com/zS1B11O.png) + +
+ +## Using the REST API + +You can also access the REST API of the API server directly. It is running on the same host machine and port and can be accessed via the `/api` route (in this case that is `localhost:3000/api/`, so you can e.g. reach the API with [`localhost:3000/api/feed`](http://localhost:3000/api/feed)). + +### `GET` + +- `/api/post/:id`: Fetch a single post by its `id` +- `/api/feed`: Fetch all _published_ posts +- `/api/filterPosts?searchString={searchString}`: Filter posts by `title` or `content` + +### `POST` + +- `/api/post`: Create a new post + - Body: + - `title: String` (required): The title of the post + - `content: String` (optional): The content of the post + - `authorEmail: String` (required): The email of the user that creates the post +- `/api/user`: Create a new user + - Body: + - `email: String` (required): The email address of the user + - `name: String` (optional): The name of the user + +### `PUT` + +- `/api/publish/:id`: Publish a post by its `id` + +### `DELETE` + +- `/api/post/:id`: Delete a post by its `id` + +## Switch to another database (e.g. PostgreSQL, MySQL, SQL Server, MongoDB) + +If you want to try this example with another database than SQLite, you can adjust the the database connection in [`prisma/schema.prisma`](./prisma/schema.prisma) by reconfiguring the `datasource` block. + +Learn more about the different connection configurations in the [docs](https://www.prisma.io/docs/reference/database-reference/connection-urls). + +
Expand for an overview of example configurations with different databases + +### PostgreSQL + +For PostgreSQL, the connection URL has the following structure: + +```prisma +datasource db { + provider = "postgresql" + url = "postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=SCHEMA" +} +``` + +Here is an example connection string with a local PostgreSQL database: + +```prisma +datasource db { + provider = "postgresql" + url = "postgresql://janedoe:mypassword@localhost:5432/notesapi?schema=public" +} +``` + +### MySQL + +For MySQL, the connection URL has the following structure: + +```prisma +datasource db { + provider = "mysql" + url = "mysql://USER:PASSWORD@HOST:PORT/DATABASE" +} +``` + +Here is an example connection string with a local MySQL database: + +```prisma +datasource db { + provider = "mysql" + url = "mysql://janedoe:mypassword@localhost:3306/notesapi" +} +``` + +### Microsoft SQL Server + +Here is an example connection string with a local Microsoft SQL Server database: + +```prisma +datasource db { + provider = "sqlserver" + url = "sqlserver://localhost:1433;initial catalog=sample;user=sa;password=mypassword;" +} +``` + +### MongoDB + +Here is an example connection string with a local MongoDB database: + +```prisma +datasource db { + provider = "mongodb" + url = "mongodb://USERNAME:PASSWORD@HOST/DATABASE?authSource=admin&retryWrites=true&w=majority" +} +``` + +
+ +## Evolving the app + +Evolving the application typically requires three steps: + +1. Migrate your database using Prisma Migrate +1. Update your server-side application code +1. Build new UI features in Vue + +For the following example scenario, assume you want to add a "profile" feature to the app where users can create a profile and write a short bio about themselves. + +### 1. Migrate your database using Prisma Migrate + +The first step is to add a new table, e.g. called `Profile`, to the database. You can do this by adding a new model to your [Prisma schema file](./prisma/schema.prisma) file and then running a migration afterwards: + +```diff +// schema.prisma + +model Post { + id Int @default(autoincrement()) @id + title String + content String? + published Boolean @default(false) + author User? @relation(fields: [authorId], references: [id]) + authorId Int +} + +model User { + id Int @default(autoincrement()) @id + name String? + email String @unique + posts Post[] ++ profile Profile? +} + ++model Profile { ++ id Int @default(autoincrement()) @id ++ bio String? ++ userId Int @unique ++ user User @relation(fields: [userId], references: [id]) ++} +``` + +Once you've updated your data model, you can execute the changes against your database with the following command: + +``` +npx prisma migrate dev --name add-profile +``` + +### 2. Update your application code + +You can now use your `PrismaClient` instance to perform operations against the new `Profile` table. Here are some examples: + +#### Create a new profile for an existing user + +```ts +const profile = await prisma.profile.create({ + data: { + bio: 'Hello World', + user: { + connect: { email: 'alice@prisma.io' }, + }, + }, +}) +``` + +#### Create a new user with a new profile + +```ts +const user = await prisma.user.create({ + data: { + email: 'john@prisma.io', + name: 'John', + profile: { + create: { + bio: 'Hello World', + }, + }, + }, +}) +``` + +#### Update the profile of an existing user + +```ts +const userWithUpdatedProfile = await prisma.user.update({ + where: { email: 'alice@prisma.io' }, + data: { + profile: { + update: { + bio: 'Hello Friends', + }, + }, + }, +}) +``` + +### 5. Build new UI features in Vue + +Once you have added a new endpoint to the API (e.g. `/api/profile` with `/POST`, `/PUT` and `GET` operations), you can start building a new UI component in Vue. It could e.g. be called `profile.vue` and would be located in the `pages` directory. + +In the application code, you can access the new endpoint via `fetch` operations and populate the UI with the data you receive from the API calls. + +## Next steps + +- Check out the [Prisma docs](https://www.prisma.io/docs) +- Share your feedback on the [Prisma Discord](https://pris.ly/discord/) +- Create issues and ask questions on [GitHub](https://github.com/prisma/prisma/) diff --git a/typescript/rest-analog/angular.json b/typescript/rest-analog/angular.json new file mode 100644 index 000000000000..d8c37010b799 --- /dev/null +++ b/typescript/rest-analog/angular.json @@ -0,0 +1,57 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "my-app": { + "projectType": "application", + "root": ".", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@analogjs/platform:vite", + "options": { + "configFile": "vite.config.ts", + "main": "src/main.ts", + "outputPath": "dist/client", + "tsConfig": "tsconfig.app.json" + }, + "defaultConfiguration": "production", + "configurations": { + "development": { + "mode": "development" + }, + "production": { + "sourcemap": false, + "mode": "production" + } + } + }, + "serve": { + "builder": "@analogjs/platform:vite-dev-server", + "defaultConfiguration": "development", + "options": { + "buildTarget": "my-app:build", + "port": 5173 + }, + "configurations": { + "development": { + "buildTarget": "my-app:build:development", + "hmr": true + }, + "production": { + "buildTarget": "my-app:build:production" + } + } + }, + "test": { + "builder": "@analogjs/vitest-angular:test" + } + } + } + }, + "cli": { + "analytics": false + } +} diff --git a/typescript/rest-analog/index.html b/typescript/rest-analog/index.html new file mode 100644 index 000000000000..8cdd0748c8fc --- /dev/null +++ b/typescript/rest-analog/index.html @@ -0,0 +1,15 @@ + + + + + My App + + + + + + + + + + diff --git a/typescript/rest-analog/package.json b/typescript/rest-analog/package.json new file mode 100644 index 000000000000..8038436ab935 --- /dev/null +++ b/typescript/rest-analog/package.json @@ -0,0 +1,57 @@ +{ + "name": "rest-analog", + "version": "0.0.0", + "type": "module", + "engines": { + "node": ">=18.19.1" + }, + "scripts": { + "ng": "ng", + "dev": "ng serve", + "start": "pnpm run dev", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@analogjs/content": "^1.7.1", + "@analogjs/router": "^1.7.1", + "@angular/animations": "^18.0.0", + "@angular/common": "^18.0.0", + "@angular/compiler": "^18.0.0", + "@angular/core": "^18.0.0", + "@angular/forms": "^18.0.0", + "@angular/platform-browser": "^18.0.0", + "@angular/platform-browser-dynamic": "^18.0.0", + "@angular/platform-server": "^18.0.0", + "@angular/router": "^18.0.0", + "@prisma/client": "^5.18.0", + "front-matter": "^4.0.2", + "marked": "^5.0.2", + "marked-gfm-heading-id": "^3.1.0", + "marked-highlight": "^2.0.1", + "marked-mangle": "^1.1.7", + "prismjs": "^1.29.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.14.3" + }, + "devDependencies": { + "@analogjs/platform": "^1.7.1", + "@analogjs/vite-plugin-angular": "^1.7.1", + "@analogjs/vitest-angular": "^1.7.1", + "@angular-devkit/build-angular": "^18.0.0", + "@angular/cli": "^18.0.0", + "@angular/compiler-cli": "^18.0.0", + "jsdom": "^22.0.0", + "prisma": "^5.18.0", + "typescript": "~5.4.2", + "vite": "^5.0.0", + "vite-tsconfig-paths": "^4.2.0", + "vitest": "^1.3.1" + }, + "prisma": { + "seed": "node prisma/seed.js" + } +} diff --git a/typescript/rest-analog/prisma/db.ts b/typescript/rest-analog/prisma/db.ts new file mode 100644 index 000000000000..a356dd972dce --- /dev/null +++ b/typescript/rest-analog/prisma/db.ts @@ -0,0 +1,16 @@ +// https://www.prisma.io/docs/guides/database/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices +import { PrismaClient } from '@prisma/client' + +const prismaClientSingleton = () => { + return new PrismaClient() +} + +type PrismaClientSingleton = ReturnType + +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClientSingleton | undefined +} + +export const prisma = globalForPrisma.prisma ?? prismaClientSingleton() + +if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma diff --git a/typescript/rest-analog/prisma/schema.prisma b/typescript/rest-analog/prisma/schema.prisma new file mode 100644 index 000000000000..0c02acd6eb76 --- /dev/null +++ b/typescript/rest-analog/prisma/schema.prisma @@ -0,0 +1,27 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = "file:./dev.db" +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String + content String? + published Boolean @default(false) + viewCount Int @default(0) + author User? @relation(fields: [authorId], references: [id]) + authorId Int? +} diff --git a/typescript/rest-analog/prisma/seed.js b/typescript/rest-analog/prisma/seed.js new file mode 100644 index 000000000000..50448b28efe6 --- /dev/null +++ b/typescript/rest-analog/prisma/seed.js @@ -0,0 +1,72 @@ +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +const userData = [ + { + name: 'Alice', + email: 'alice@prisma.io', + posts: { + create: [ + { + title: 'Join the Prisma Discord', + content: 'https://pris.ly/discord', + published: true, + }, + ], + }, + }, + { + name: 'Nilu', + email: 'nilu@prisma.io', + posts: { + create: [ + { + title: 'Follow Prisma on Twitter', + content: 'https://www.twitter.com/prisma', + published: true, + viewCount: 42, + }, + ], + }, + }, + { + name: 'Mahmoud', + email: 'mahmoud@prisma.io', + posts: { + create: [ + { + title: 'Ask a question about Prisma on GitHub', + content: 'https://www.github.com/prisma/prisma/discussions', + published: true, + viewCount: 128, + }, + { + title: 'Prisma on YouTube', + content: 'https://pris.ly/youtube', + }, + ], + }, + }, +] + +async function main() { + console.log(`Start seeding ...`) + for (const u of userData) { + const user = await prisma.user.create({ + data: u, + }) + console.log(`Created user with id: ${user.id}`) + } + console.log(`Seeding finished.`) +} + +main() + .then(async () => { + await prisma.$disconnect() + }) + .catch(async (e) => { + console.error(e) + await prisma.$disconnect() + process.exit(1) + }) diff --git a/typescript/rest-analog/public/.gitkeep b/typescript/rest-analog/public/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/typescript/rest-analog/public/favicon.ico b/typescript/rest-analog/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..997406ad22c29aae95893fb3d666c30258a09537 GIT binary patch literal 948 zcmV;l155mgP)CBYU7IjCFmI-B}4sMJt3^s9NVg!P0 z6hDQy(L`XWMkB@zOLgN$4KYz;j0zZxq9KKdpZE#5@k0crP^5f9KO};h)ZDQ%ybhht z%t9#h|nu0K(bJ ztIkhEr!*UyrZWQ1k2+YkGqDi8Z<|mIN&$kzpKl{cNP=OQzXHz>vn+c)F)zO|Bou>E z2|-d_=qY#Y+yOu1a}XI?cU}%04)zz%anD(XZC{#~WreV!a$7k2Ug`?&CUEc0EtrkZ zL49MB)h!_K{H(*l_93D5tO0;BUnvYlo+;yss%n^&qjt6fZOa+}+FDO(~2>G z2dx@=JZ?DHP^;b7*Y1as5^uphBsh*s*z&MBd?e@I>-9kU>63PjP&^#5YTOb&x^6Cf z?674rmSHB5Fk!{Gv7rv!?qX#ei_L(XtwVqLX3L}$MI|kJ*w(rhx~tc&L&xP#?cQow zX_|gx$wMr3pRZIIr_;;O|8fAjd;1`nOeu5K(pCu7>^3E&D2OBBq?sYa(%S?GwG&_0-s%_v$L@R!5H_fc)lOb9ZoOO#p`Nn`KU z3LTTBtjwo`7(HA6 z7gmO$yTR!5L>Bsg!X8616{JUngg_@&85%>W=mChTR;x4`P=?PJ~oPuy5 zU-L`C@_!34D21{fD~Y8NVnR3t;aqZI3fIhmgmx}$oc-dKDC6Ap$Gy>a!`A*x2L1v0 WcZ@i?LyX}70000 +import HeaderComponent from './components/header.analog'; +import { RouterOutlet } from '@angular/router' + +defineMetadata({ imports: [RouterOutlet, HeaderComponent] }); + + + diff --git a/typescript/rest-analog/src/app/app.config.server.ts b/typescript/rest-analog/src/app/app.config.server.ts new file mode 100644 index 000000000000..a8002f197f6d --- /dev/null +++ b/typescript/rest-analog/src/app/app.config.server.ts @@ -0,0 +1,10 @@ +import { mergeApplicationConfig, ApplicationConfig } from '@angular/core' +import { provideServerRendering } from '@angular/platform-server' + +import { appConfig } from './app.config' + +const serverConfig: ApplicationConfig = { + providers: [provideServerRendering()], +} + +export const config = mergeApplicationConfig(appConfig, serverConfig) diff --git a/typescript/rest-analog/src/app/app.config.ts b/typescript/rest-analog/src/app/app.config.ts new file mode 100644 index 000000000000..cd3473969270 --- /dev/null +++ b/typescript/rest-analog/src/app/app.config.ts @@ -0,0 +1,20 @@ +import { + provideHttpClient, + withFetch, + withInterceptors, +} from '@angular/common/http' +import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core' +import { provideClientHydration } from '@angular/platform-browser' +import { provideFileRouter, requestContextInterceptor } from '@analogjs/router' + +export const appConfig: ApplicationConfig = { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideFileRouter(), + provideHttpClient( + withFetch(), + withInterceptors([requestContextInterceptor]), + ), + provideClientHydration(), + ], +} diff --git a/typescript/rest-analog/src/app/components/header.analog b/typescript/rest-analog/src/app/components/header.analog new file mode 100644 index 000000000000..51904183c9cf --- /dev/null +++ b/typescript/rest-analog/src/app/components/header.analog @@ -0,0 +1,55 @@ + + + + + diff --git a/typescript/rest-analog/src/app/components/post/post.component.css b/typescript/rest-analog/src/app/components/post/post.component.css new file mode 100644 index 000000000000..11662f669ff3 --- /dev/null +++ b/typescript/rest-analog/src/app/components/post/post.component.css @@ -0,0 +1,13 @@ +:host { + display: block; + padding: 1rem; + background: white; +} + +h2 { + margin-block: 0; +} + +:host:hover { + box-shadow: 1px 1px 3px #aaa; +} diff --git a/typescript/rest-analog/src/app/components/post/post.component.html b/typescript/rest-analog/src/app/components/post/post.component.html new file mode 100644 index 000000000000..bb6ee3b40e64 --- /dev/null +++ b/typescript/rest-analog/src/app/components/post/post.component.html @@ -0,0 +1,3 @@ +

{{ post().title }}

+By {{ post().author.name }} +

diff --git a/typescript/rest-analog/src/app/components/post/post.component.spec.ts b/typescript/rest-analog/src/app/components/post/post.component.spec.ts new file mode 100644 index 000000000000..9884c7a36424 --- /dev/null +++ b/typescript/rest-analog/src/app/components/post/post.component.spec.ts @@ -0,0 +1,31 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { PostComponent } from './post.component' + +describe('PostComponent', () => { + let component: PostComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PostComponent], + }).compileComponents() + + fixture = TestBed.createComponent(PostComponent) + component = fixture.componentInstance + }) + + it('should create', () => { + fixture.componentRef.setInput('post', { + id: 1, + title: 'title', + content: 'content', + author: { + name: 'Gerome', + }, + }) + + fixture.detectChanges() + expect(component).toBeTruthy() + }) +}) diff --git a/typescript/rest-analog/src/app/components/post/post.component.ts b/typescript/rest-analog/src/app/components/post/post.component.ts new file mode 100644 index 000000000000..5b8e7da55eae --- /dev/null +++ b/typescript/rest-analog/src/app/components/post/post.component.ts @@ -0,0 +1,13 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core' +import { Post } from '../../models/post.model' + +@Component({ + selector: 'app-post', + standalone: true, + templateUrl: './post.component.html', + styleUrl: './post.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PostComponent { + post = input.required() +} diff --git a/typescript/rest-analog/src/app/models/author.model.ts b/typescript/rest-analog/src/app/models/author.model.ts new file mode 100644 index 000000000000..3ff8005996f5 --- /dev/null +++ b/typescript/rest-analog/src/app/models/author.model.ts @@ -0,0 +1,3 @@ +export type Author = { + name: string +} diff --git a/typescript/rest-analog/src/app/models/post.model.ts b/typescript/rest-analog/src/app/models/post.model.ts new file mode 100644 index 000000000000..e106192834cd --- /dev/null +++ b/typescript/rest-analog/src/app/models/post.model.ts @@ -0,0 +1,9 @@ +import { Author } from './author.model' + +export type Post = { + id: number + title: string + author: Author + content: string + published: boolean +} diff --git a/typescript/rest-analog/src/app/pages/create/index.page.ts b/typescript/rest-analog/src/app/pages/create/index.page.ts new file mode 100644 index 000000000000..f1bae9440f1d --- /dev/null +++ b/typescript/rest-analog/src/app/pages/create/index.page.ts @@ -0,0 +1,136 @@ +import { Component, DestroyRef, inject } from '@angular/core' +import { + AbstractControl, + AsyncValidatorFn, + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms' +import { Router, RouterLink } from '@angular/router' +import { PostService } from '../../services/post.service' +import { AuthService } from '../../services/auth.service' +import { map } from 'rxjs' +import { takeUntilDestroyed } from '@angular/core/rxjs-interop' + +@Component({ + selector: 'app-create', + standalone: true, + template: ` +
+
+

Create Draft

+ + + @if ( + form.value.authorEmail && + form.controls.authorEmail.errors?.authorExists + ) { + '{{ form.value.authorEmail }}' does not exists in our + database. + } + + + or Cancel +
+
+ `, + styles: [ + ` + .page { + background: white; + padding: 3rem; + display: flex; + justify-content: center; + align-items: center; + } + + input[type='text'], + textarea { + width: 100%; + padding: 0.5rem; + margin: 0.5rem 0; + border-radius: 0.25rem; + border: 0.125rem solid rgba(0, 0, 0, 0.2); + } + + input[type='submit'] { + background: #ececec; + border: 0; + padding: 1rem 2rem; + } + + .back { + margin-left: 1rem; + } + + span { + color: red; + } + + .primary { + background: blue !important; + color: white; + } + `, + ], + imports: [ReactiveFormsModule, RouterLink], +}) +export default class CreateComponent { + #postService = inject(PostService) + #authService = inject(AuthService) + #router = inject(Router) + #destroyRef = inject(DestroyRef) + + form = new FormGroup({ + title: new FormControl('', [Validators.required]), + authorEmail: new FormControl('', { + asyncValidators: this.validateEmail(), + updateOn: 'blur', + }), + content: new FormControl(''), + }) + + createDraft(): void { + this.#postService + .createDraft( + this.form.value.title, + this.form.value.authorEmail, + this.form.value.content, + ) + .pipe(takeUntilDestroyed(this.#destroyRef)) + .subscribe({ + next: () => this.#router.navigate(['/drafts']), + error: (err) => console.error(err), + }) + } + + validateEmail(): AsyncValidatorFn { + return (control: AbstractControl) => { + return this.#authService + .findAuthor(control.value) + .pipe(map((res) => (Boolean(res) ? null : { authorExists: true }))) + } + } +} diff --git a/typescript/rest-analog/src/app/pages/drafts/index.page.ts b/typescript/rest-analog/src/app/pages/drafts/index.page.ts new file mode 100644 index 000000000000..cd4b5d1b6f08 --- /dev/null +++ b/typescript/rest-analog/src/app/pages/drafts/index.page.ts @@ -0,0 +1,71 @@ +import { Component, signal } from '@angular/core' +import { PostComponent } from '../../components/post/post.component' +import { load } from './index.server' +import { toSignal } from '@angular/core/rxjs-interop' +import { injectLoad } from '@analogjs/router' +import { RouterLink } from '@angular/router' +import { catchError, EMPTY, tap } from 'rxjs' + +@Component({ + selector: 'app-drafts', + standalone: true, + template: ` +
+

Drafts

+
+ @if (pending()) { +

+ +

+ } @else if (error()) { +

Error while fetching feed 💔

+ } @else { +
    + @for (post of posts(); track post.id) { +
  • + + + +
  • + } +
+ } +
+
+ `, + styles: [ + ` + .page { + padding-inline: 2rem; + } + + .post-list { + list-style-type: none; + padding: 0; + display: grid; + gap: 1rem; + } + + a { + color: inherit; + text-decoration: none; + } + `, + ], + imports: [PostComponent, RouterLink], +}) +export default class DraftsComponent { + pending = signal(true) + error = signal('') + + posts = toSignal( + injectLoad().pipe( + tap(() => this.pending.set(false)), + catchError((error) => { + this.error.set(error.message) + return EMPTY + }), + ), + { requireSync: true }, + ) +} diff --git a/typescript/rest-analog/src/app/pages/drafts/index.server.ts b/typescript/rest-analog/src/app/pages/drafts/index.server.ts new file mode 100644 index 000000000000..7f494a5f87d5 --- /dev/null +++ b/typescript/rest-analog/src/app/pages/drafts/index.server.ts @@ -0,0 +1,6 @@ +import { PageServerLoad } from '@analogjs/router' +import { Post } from '../../models/post.model' + +export const load = async ({ fetch }: PageServerLoad): Promise => { + return await fetch('/draft-list') +} diff --git a/typescript/rest-analog/src/app/pages/index.page.ts b/typescript/rest-analog/src/app/pages/index.page.ts new file mode 100644 index 000000000000..9ad48306f027 --- /dev/null +++ b/typescript/rest-analog/src/app/pages/index.page.ts @@ -0,0 +1,66 @@ +import { Component, signal } from '@angular/core' +import { PostComponent } from '../components/post/post.component' +import { load } from './index.server' +import { toSignal } from '@angular/core/rxjs-interop' +import { injectLoad } from '@analogjs/router' +import { catchError, EMPTY, tap } from 'rxjs' + +@Component({ + selector: 'app-home', + standalone: true, + template: ` +
+

My Blog

+
+ @if (pending()) { +

+ +

+ } @else if (error()) { +

Error while fetching feed 💔

+ } @else { + @for (post of posts(); track post.id) { + + } + } +
+
+ `, + styles: [ + ` + .page { + padding-inline: 3rem; + } + + .post { + background: white; + transition: box-shadow 0.1s ease-in; + } + + .post:hover { + box-shadow: 1px 1px 3px #aaa; + } + + .post, + .post { + margin-top: 2rem; + } + `, + ], + imports: [PostComponent], +}) +export default class HomeComponent { + pending = signal(true) + error = signal('') + + posts = toSignal( + injectLoad().pipe( + tap(() => this.pending.set(false)), + catchError((error) => { + this.error.set(error.message) + return EMPTY + }), + ), + { requireSync: true }, + ) +} diff --git a/typescript/rest-analog/src/app/pages/index.server.ts b/typescript/rest-analog/src/app/pages/index.server.ts new file mode 100644 index 000000000000..2cd07e341674 --- /dev/null +++ b/typescript/rest-analog/src/app/pages/index.server.ts @@ -0,0 +1,6 @@ +import { PageServerLoad } from '@analogjs/router' +import { Post } from '../models/post.model' + +export const load = async ({ fetch }: PageServerLoad): Promise => { + return await fetch('/api/feed') +} diff --git a/typescript/rest-analog/src/app/pages/post/[id].page.ts b/typescript/rest-analog/src/app/pages/post/[id].page.ts new file mode 100644 index 000000000000..dd92173a88cb --- /dev/null +++ b/typescript/rest-analog/src/app/pages/post/[id].page.ts @@ -0,0 +1,108 @@ +import { Component, DestroyRef, inject, signal } from '@angular/core' +import { load } from './[id].server' +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop' +import { injectLoad } from '@analogjs/router' +import { catchError, EMPTY, tap } from 'rxjs' +import { PostService } from '../../services/post.service' +import { Router } from '@angular/router' + +@Component({ + selector: 'app-post-item', + standalone: true, + template: ` +
+ @if (pending()) { +

+ +

+ } @else if (error()) { +

Error while fetching feed 💔

+ } @else { +
+

+ {{ article().title }} ({{ + article().published ? 'Published' : 'Draft' + }}) +

+

By {{ article().author.name }}

+
+
+ @if (!article().published) { + + } + +
+
+ } +
+ `, + styles: [ + ` + .page { + background: white; + padding: 2rem; + } + + .actions { + margin-top: 2rem; + } + + button { + margin: 0.5rem; + background: #ececec; + border: 1px black solid; + border-radius: 0.125rem; + padding: 1rem 2rem; + } + + button button { + margin-left: 1rem; + } + + .btn-wrapper { + display: flex; + justify-content: center; + width: fit-content; + margin-top: 1rem; + } + `, + ], + imports: [], +}) +export default class PostItemComponent { + #postService = inject(PostService) + #router = inject(Router) + #destroyRef = inject(DestroyRef) + + pending = signal(true) + error = signal('') + + article = toSignal( + injectLoad().pipe( + tap(() => this.pending.set(false)), + catchError((error) => { + this.error.set(error.message) + return EMPTY + }), + ), + { requireSync: true }, + ) + + destroy(): void { + this.#postService + .destroy(this.article().id) + .pipe(takeUntilDestroyed(this.#destroyRef)) + .subscribe(() => { + void this.#router.navigate(['/']) + }) + } + + publish(): void { + this.#postService + .publish(this.article().id) + .pipe(takeUntilDestroyed(this.#destroyRef)) + .subscribe(() => { + void this.#router.navigate(['/']) + }) + } +} diff --git a/typescript/rest-analog/src/app/pages/post/[id].server.ts b/typescript/rest-analog/src/app/pages/post/[id].server.ts new file mode 100644 index 000000000000..035672ab82bb --- /dev/null +++ b/typescript/rest-analog/src/app/pages/post/[id].server.ts @@ -0,0 +1,9 @@ +import { PageServerLoad } from '@analogjs/router' +import { Post } from '../../models/post.model' + +export const load = async ({ + fetch, + params, +}: PageServerLoad): Promise => { + return await fetch(`/api/post/${params?.['id']}`) +} diff --git a/typescript/rest-analog/src/app/pages/signup/index.page.ts b/typescript/rest-analog/src/app/pages/signup/index.page.ts new file mode 100644 index 000000000000..33386cbc35cf --- /dev/null +++ b/typescript/rest-analog/src/app/pages/signup/index.page.ts @@ -0,0 +1,95 @@ +import { Component, DestroyRef, inject } from '@angular/core' +import { Router, RouterLink } from '@angular/router' +import { + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms' +import { AuthService } from '../../services/auth.service' +import { takeUntilDestroyed } from '@angular/core/rxjs-interop' + +type AuthForm = { + name: FormControl + email: FormControl +} + +@Component({ + selector: 'app-signup', + standalone: true, + template: ` +
+
+

Signup user

+ + + + or Cancel +
+
+ `, + styles: [ + ` + .page { + background: white; + padding: 3rem; + display: flex; + justify-content: center; + } + + input[type='text'] { + width: 100%; + padding: 0.5rem; + margin: 0.5rem 0; + border-radius: 0.25rem; + border: 0.125rem solid rgba(0, 0, 0, 0.2); + } + + input[type='submit'] { + background: #ececec; + border: 0; + padding: 1rem 2rem; + } + + .back { + margin-left: 1rem; + } + `, + ], + imports: [RouterLink, ReactiveFormsModule], +}) +export default class SignupComponent { + #authService = inject(AuthService) + #router = inject(Router) + #destroyRef = inject(DestroyRef) + + form = new FormGroup({ + name: new FormControl('', { + validators: [Validators.required], + nonNullable: true, + }), + email: new FormControl('', { + validators: [Validators.required], + nonNullable: true, + }), + }) + + signup(): void { + this.#authService + .signup(this.form.value.name, this.form.value.email) + .pipe(takeUntilDestroyed(this.#destroyRef)) + .subscribe({ + next: () => this.#router.navigate(['/']), + error: (error) => console.error(error), + }) + } +} diff --git a/typescript/rest-analog/src/app/services/auth.service.ts b/typescript/rest-analog/src/app/services/auth.service.ts new file mode 100644 index 000000000000..5eea3865d49e --- /dev/null +++ b/typescript/rest-analog/src/app/services/auth.service.ts @@ -0,0 +1,17 @@ +import { inject, Injectable } from '@angular/core' +import { HttpClient } from '@angular/common/http' + +@Injectable({ + providedIn: 'root', +}) +export class AuthService { + #http = inject(HttpClient) + + signup(name: string, email: string) { + return this.#http.post('/api/user', { name, email }) + } + + findAuthor(email: string) { + return this.#http.post(`/api/author`, { email }) + } +} diff --git a/typescript/rest-analog/src/app/services/post.service.ts b/typescript/rest-analog/src/app/services/post.service.ts new file mode 100644 index 000000000000..499b7c81dd22 --- /dev/null +++ b/typescript/rest-analog/src/app/services/post.service.ts @@ -0,0 +1,21 @@ +import { inject, Injectable } from '@angular/core' +import { HttpClient } from '@angular/common/http' + +@Injectable({ + providedIn: 'root', +}) +export class PostService { + #http = inject(HttpClient) + + createDraft(title: string, authorEmail: string, content: string) { + return this.#http.post('/api/post', { title, authorEmail, content }) + } + + publish(id: number) { + return this.#http.put(`/api/publish/${id}`, {}) + } + + destroy(id: number) { + return this.#http.delete(`/api/post/${id}`) + } +} diff --git a/typescript/rest-analog/src/main.server.ts b/typescript/rest-analog/src/main.server.ts new file mode 100644 index 000000000000..66f02a3f5d22 --- /dev/null +++ b/typescript/rest-analog/src/main.server.ts @@ -0,0 +1,32 @@ +import 'zone.js/node' +import '@angular/platform-server/init' +import { enableProdMode } from '@angular/core' +import { bootstrapApplication } from '@angular/platform-browser' +import { renderApplication } from '@angular/platform-server' +import { provideServerContext } from '@analogjs/router/server' +import { ServerContext } from '@analogjs/router/tokens' + +import { config } from './app/app.config.server' +import App from './app/app.analog' + +if (import.meta.env.PROD) { + enableProdMode() +} + +export function bootstrap() { + return bootstrapApplication(App, config) +} + +export default async function render( + url: string, + document: string, + serverContext: ServerContext, +) { + const html = await renderApplication(bootstrap, { + document, + url, + platformProviders: [provideServerContext(serverContext)], + }) + + return html +} diff --git a/typescript/rest-analog/src/main.ts b/typescript/rest-analog/src/main.ts new file mode 100644 index 000000000000..6aed47879242 --- /dev/null +++ b/typescript/rest-analog/src/main.ts @@ -0,0 +1,7 @@ +import 'zone.js' +import { bootstrapApplication } from '@angular/platform-browser' + +import App from './app/app.analog'; +import { appConfig } from './app/app.config' + +bootstrapApplication(App, appConfig) diff --git a/typescript/rest-analog/src/server/routes/author.ts b/typescript/rest-analog/src/server/routes/author.ts new file mode 100644 index 000000000000..c4660609db81 --- /dev/null +++ b/typescript/rest-analog/src/server/routes/author.ts @@ -0,0 +1,17 @@ +import { prisma } from '../../../prisma/db' + +export default defineEventHandler(async (event) => { + const { email } = await readBody(event) + + try { + const user = await prisma.user.findFirst({ + where: { + email: email, + }, + }) + + return user + } catch (error) { + console.error(error) + } +}) diff --git a/typescript/rest-analog/src/server/routes/draft-list.get.ts b/typescript/rest-analog/src/server/routes/draft-list.get.ts new file mode 100644 index 000000000000..9019b46a7bcd --- /dev/null +++ b/typescript/rest-analog/src/server/routes/draft-list.get.ts @@ -0,0 +1,18 @@ +import { prisma } from '../../../prisma/db' + +export default defineEventHandler(async () => { + const posts = await prisma.post + .findMany({ + where: { + published: false, + }, + include: { + author: true, + }, + }) + .catch((error) => { + console.error(error) + }) + + return posts +}) diff --git a/typescript/rest-analog/src/server/routes/feed.get.ts b/typescript/rest-analog/src/server/routes/feed.get.ts new file mode 100644 index 000000000000..4df02d9be041 --- /dev/null +++ b/typescript/rest-analog/src/server/routes/feed.get.ts @@ -0,0 +1,18 @@ +import { prisma } from '../../../prisma/db' + +export default defineEventHandler(async () => { + const feed = await prisma.post + .findMany({ + where: { + published: true, + }, + include: { + author: true, + }, + }) + .catch((error) => { + console.error(error) + }) + + return feed +}) diff --git a/typescript/rest-analog/src/server/routes/filterPosts.get.ts b/typescript/rest-analog/src/server/routes/filterPosts.get.ts new file mode 100644 index 000000000000..048fa0e7a58f --- /dev/null +++ b/typescript/rest-analog/src/server/routes/filterPosts.get.ts @@ -0,0 +1,28 @@ +import { prisma } from '../../../prisma/db' + +export default defineEventHandler(async (event) => { + const { searchString } = getQuery(event) + + const draftPosts = await prisma.post + .findMany({ + where: { + OR: [ + { + title: { + contains: searchString, + }, + }, + { + content: { + contains: searchString, + }, + }, + ], + }, + }) + .catch((error) => { + console.error(error) + }) + + return draftPosts +}) diff --git a/typescript/rest-analog/src/server/routes/post/[id].delete.ts b/typescript/rest-analog/src/server/routes/post/[id].delete.ts new file mode 100644 index 000000000000..3b7e3677a50d --- /dev/null +++ b/typescript/rest-analog/src/server/routes/post/[id].delete.ts @@ -0,0 +1,18 @@ +import { prisma } from '../../../../prisma/db' + +export default defineEventHandler(async (event) => { + const id = event.context.params.id + + const deletePost = await prisma.post + .delete({ + where: { + //@ts-ignore + id: parseInt(id), + }, + }) + .catch((error) => { + console.error(error) + }) + + return deletePost +}) diff --git a/typescript/rest-analog/src/server/routes/post/[id].get.ts b/typescript/rest-analog/src/server/routes/post/[id].get.ts new file mode 100644 index 000000000000..2897cfda62e9 --- /dev/null +++ b/typescript/rest-analog/src/server/routes/post/[id].get.ts @@ -0,0 +1,25 @@ +import { prisma } from '../../../../prisma/db' + +export default defineEventHandler(async (event) => { + const { + context: { + params: { id }, + }, + } = event + + const getPost = await prisma.post + .findUnique({ + where: { + //@ts-ignore + id: parseInt(id), + }, + include: { + author: true, + }, + }) + .catch((error) => { + console.error(error) + }) + + return getPost +}) diff --git a/typescript/rest-analog/src/server/routes/post/index.ts b/typescript/rest-analog/src/server/routes/post/index.ts new file mode 100644 index 000000000000..d6d1c652e336 --- /dev/null +++ b/typescript/rest-analog/src/server/routes/post/index.ts @@ -0,0 +1,24 @@ +import { prisma } from '../../../../prisma/db' + +export default defineEventHandler(async (event) => { + const { title, content, authorEmail } = await readBody(event) + + const createPost = await prisma.post + .create({ + data: { + title, + content, + published: false, + author: { + connect: { + email: authorEmail, + }, + }, + }, + }) + .catch((error) => { + console.error(error) + }) + + return createPost +}) diff --git a/typescript/rest-analog/src/server/routes/publish/[id].put.ts b/typescript/rest-analog/src/server/routes/publish/[id].put.ts new file mode 100644 index 000000000000..3c8581df0fd9 --- /dev/null +++ b/typescript/rest-analog/src/server/routes/publish/[id].put.ts @@ -0,0 +1,20 @@ +import { prisma } from '../../../../prisma/db' + +export default defineEventHandler(async (event) => { + const id = event.context.params.id + + const updatePost = await prisma.post + .update({ + where: { + id: parseInt(id), + }, + data: { + published: true, + }, + }) + .catch((error) => { + console.error(error) + }) + + return updatePost +}) diff --git a/typescript/rest-analog/src/server/routes/user.ts b/typescript/rest-analog/src/server/routes/user.ts new file mode 100644 index 000000000000..fb9def089fb9 --- /dev/null +++ b/typescript/rest-analog/src/server/routes/user.ts @@ -0,0 +1,18 @@ +import { prisma } from '../../../prisma/db' + +export default defineEventHandler(async (event) => { + const { name, email } = await readBody(event) + + const createUser = await prisma.user + .create({ + data: { + name, + email, + }, + }) + .catch((error) => { + console.error(error) + }) + + return createUser +}) diff --git a/typescript/rest-analog/src/server/routes/v1/hello.ts b/typescript/rest-analog/src/server/routes/v1/hello.ts new file mode 100644 index 000000000000..594c5d716df1 --- /dev/null +++ b/typescript/rest-analog/src/server/routes/v1/hello.ts @@ -0,0 +1,3 @@ +import { defineEventHandler } from 'h3'; + +export default defineEventHandler(() => ({ message: 'Hello World' })); diff --git a/typescript/rest-analog/src/styles.css b/typescript/rest-analog/src/styles.css new file mode 100644 index 000000000000..710e162c95c5 --- /dev/null +++ b/typescript/rest-analog/src/styles.css @@ -0,0 +1,31 @@ +html { + box-sizing: border-box; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +body { + margin: 0; + padding: 0; + font-size: 16px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, + Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + background: rgba(0, 0, 0, 0.05); +} + +input, +textarea { + font-size: 16px; +} + +button { + cursor: pointer; +} + +.layout { + padding: 0 2rem; +} diff --git a/typescript/rest-analog/src/test-setup.ts b/typescript/rest-analog/src/test-setup.ts new file mode 100644 index 000000000000..cb5fd340f3d8 --- /dev/null +++ b/typescript/rest-analog/src/test-setup.ts @@ -0,0 +1,12 @@ +import '@analogjs/vite-plugin-angular/setup-vitest' + +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting, +} from '@angular/platform-browser-dynamic/testing' +import { getTestBed } from '@angular/core/testing' + +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), +) diff --git a/typescript/rest-analog/src/vite-env.d.ts b/typescript/rest-analog/src/vite-env.d.ts new file mode 100644 index 000000000000..11f02fe2a006 --- /dev/null +++ b/typescript/rest-analog/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/typescript/rest-analog/tsconfig.app.json b/typescript/rest-analog/tsconfig.app.json new file mode 100644 index 000000000000..03a3f71a8661 --- /dev/null +++ b/typescript/rest-analog/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts", "src/main.server.ts"], + "include": ["src/**/*.d.ts", "src/app/pages/**/*.page.ts"] +} diff --git a/typescript/rest-analog/tsconfig.json b/typescript/rest-analog/tsconfig.json new file mode 100644 index 000000000000..94e11863a100 --- /dev/null +++ b/typescript/rest-analog/tsconfig.json @@ -0,0 +1,31 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022", "dom"], + "useDefineForClassFields": false, + "skipLibCheck": true + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/typescript/rest-analog/tsconfig.spec.json b/typescript/rest-analog/tsconfig.spec.json new file mode 100644 index 000000000000..06eb7cae6180 --- /dev/null +++ b/typescript/rest-analog/tsconfig.spec.json @@ -0,0 +1,11 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "target": "es2016", + "types": ["node", "vitest/globals"] + }, + "files": ["src/test-setup.ts"], + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/typescript/rest-analog/vite.config.ts b/typescript/rest-analog/vite.config.ts new file mode 100644 index 000000000000..9fab67107127 --- /dev/null +++ b/typescript/rest-analog/vite.config.ts @@ -0,0 +1,34 @@ +/// + +import { defineConfig } from 'vite' +import analog from '@analogjs/platform' + +// https://vitejs.dev/config/ +export default defineConfig(({ mode }) => ({ + build: { + target: ['es2020'], + }, + resolve: { + mainFields: ['module'], + }, + plugins: [ + analog({ + vite: { + // Required to use the Analog SFC format + experimental: { + supportAnalogFormat: true, + }, + }, + }), + ], + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['src/test-setup.ts'], + include: ['**/*.spec.ts'], + reporters: ['default'], + }, + define: { + 'import.meta.vitest': mode !== 'production', + }, +}))