├── .dockerignore
├── public
├── logo.png
├── favicon.ico
└── vercel.svg
├── assets
└── images
│ └── 2.jpeg
├── markdown
├── 第二篇文章.md
└── 第一篇博客.md
├── next.config.js
├── custom.d.ts
├── .babelrc
├── pages
├── api
│ ├── hello.js
│ └── v1
│ │ ├── posts.tsx
│ │ ├── sessions.tsx
│ │ ├── users.tsx
│ │ └── posts
│ │ └── [id].tsx
├── _app.js
├── posts
│ ├── first-post.js
│ ├── new.tsx
│ ├── [id]
│ │ └── edit.tsx
│ ├── [id].tsx
│ └── index.tsx
├── index.tsx
├── sign_up.tsx
└── sign_in.tsx
├── styles
└── global.css
├── Dockerfile
├── lib
├── withSession.tsx
├── getDatabaseConnection.tsx
└── post.tsx
├── next-env.d.ts
├── bin
└── deploy.sh
├── .gitignore
├── ormconfig.simple.json
├── src
├── entity
│ ├── Comment.ts
│ ├── Post.ts
│ └── User.ts
├── migration
│ ├── 1592561070934-AddUniqueUsernameToUsers.ts
│ ├── 1592201997493-CreateUsers.ts
│ ├── 1592205680555-CreatePosts.ts
│ ├── 1592206097026-CreateComments.ts
│ ├── 1592208800876-RenameColumns.ts
│ └── 1592206406201-AddCreatedAtAndUpdateAt.ts
├── seed.ts
└── model
│ └── SignIn.ts
├── hooks
├── usePosts.tsx
├── usePager.tsx
└── useForm.tsx
├── tsconfig.json
├── nginx.conf
├── migrate.patch
├── package.json
├── README.md
├── dist
├── seed.js
├── migration
│ ├── 1592561070934-AddUniqueUsernameToUsers.js
│ ├── 1592208800876-RenameColumns.js
│ ├── 1592201997493-CreateUsers.js
│ ├── 1592205680555-CreatePosts.js
│ ├── 1592206097026-CreateComments.js
│ └── 1592206406201-AddCreatedAtAndUpdateAt.js
├── entity
│ ├── Comment.js
│ ├── Post.js
│ └── User.js
└── model
│ └── SignIn.js
└── study.md
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 |
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Maricaya/nextjs-blog/HEAD/public/logo.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Maricaya/nextjs-blog/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/assets/images/2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Maricaya/nextjs-blog/HEAD/assets/images/2.jpeg
--------------------------------------------------------------------------------
/markdown/第二篇文章.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 第二篇博客
3 | date: 2020-5-30
4 | ---
5 |
6 | 123
7 | 111111
8 | 1234566
9 |
--------------------------------------------------------------------------------
/markdown/第一篇博客.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 第一篇博客
3 | date: 2020-5-30
4 | ---
5 |
6 | 123
7 | 1111111
8 | 1234566
9 | 文字文字
10 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const withImages = require('next-images')
2 |
3 | module.exports = withImages({
4 | webpack(config, options) {
5 | return config
6 | }
7 | })
8 |
--------------------------------------------------------------------------------
/custom.d.ts:
--------------------------------------------------------------------------------
1 | type Post = {
2 | id: string;
3 | date: string;
4 | title: string;
5 | content: string;
6 | htmlContent: string;
7 | }
8 |
9 | type User = {
10 | id: string;
11 | }
12 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": [
4 | [
5 | "@babel/plugin-proposal-decorators",
6 | {
7 | "legacy": true
8 | }
9 | ]
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/pages/api/hello.js:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 |
3 | export default (req, res) => {
4 | res.statusCode = 200
5 | res.json({ name: 'John Doe' })
6 | }
7 |
--------------------------------------------------------------------------------
/styles/global.css:
--------------------------------------------------------------------------------
1 | * {margin: 0;padding: 0;box-sizing: border-box;}
2 | *::after, *::before {box-sizing: border-box;}
3 | a {text-decoration: none; color: #00adb5; border-bottom: 1px solid;}
4 | a, input, button {font: inherit;}
5 | h1 {margin: 8px 0;}
6 | p {margin: 4px 0;}
7 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12
2 | WORKDIR /usr/src/app
3 | COPY package.json ./
4 | COPY yarn.lock ./
5 |
6 | RUN yarn install
7 | COPY . .
8 | EXPOSE 3000
9 | CMD ["yarn", "start"]
10 |
11 | # docker build -t sunnyla/node-web-app .
12 | # docker run -p 3000:3000 -d sunnyla/node-web-app
13 |
--------------------------------------------------------------------------------
/lib/withSession.tsx:
--------------------------------------------------------------------------------
1 | import {withIronSession} from 'next-iron-session';
2 | import {GetServerSideProps, NextApiHandler} from 'next';
3 |
4 | export function withSession(handler: NextApiHandler | GetServerSideProps) {
5 | return withIronSession(handler, {
6 | password: process.env.SECRET,
7 | cookieName: 'blog',
8 | cookieOptions: {secure: false}
9 | })
10 | }
11 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | import * as next from 'next'
4 |
5 | declare module "*.png" {
6 | const value: string;
7 | export default value;
8 | }
9 |
10 | declare module 'next' {
11 | import {Session} from 'next-iron-session';
12 |
13 | interface NextApiRequest {
14 | session: Session
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | import 'styles/global.css'
3 | import 'github-markdown-css'
4 |
5 | export default function App({ Component, pageProps }) {
6 | return
7 |
8 |
第一篇文章
9 |
11 |
12 |
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/bin/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | docker start 54f &&
3 | cd /home/blog/app/nextjs-blog/ &&
4 | git pull &&
5 | yarn install --production=false &&
6 | yarn build &&
7 | git apply migrate.patch;
8 | yarn compile &&
9 | yarn m:run &&
10 | git reset --hard HEAD &&
11 | docker build -t sunnyla/node-web-app . &&
12 | docker kill app &&
13 | docker rm app &&
14 | #:--network=host 会导致端口映射失效,端口直接就是阿里云机器的端口,但这种模式比较容易理解
15 | docker run --name app --network=host -p 3000:3000 -d sunnyla/node-web-app &&
16 | echo "ok!"
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 | /blog-data/
3 | # dependencies
4 | /node_modules
5 | /.idea/
6 | /.vscode/
7 | /.pnp
8 | .pnp.js
9 | */.env.local
10 | ormconfig.json
11 | em.sh
12 | # testing
13 | /coverage
14 |
15 | # next.js
16 | /.next/
17 | /out/
18 |
19 | # production
20 | /build
21 |
22 | # misc
23 | .DS_Store
24 |
25 | # debug
26 | npm-debug.log*
27 | yarn-debug.log*
28 | yarn-error.log*
29 |
30 | # local env files
31 | .env.local
32 | .env.development.local
33 | .env.test.local
34 | .env.production.local
35 |
--------------------------------------------------------------------------------
/ormconfig.simple.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "postgres",
3 | "host": "localhost",
4 | "port": 5432,
5 | "username": "blog",
6 | "password": "",
7 | "database": "blog_development",
8 | "synchronize": false,
9 | "logging": false,
10 | "entities": [
11 | "dist/entity/**/*.js"
12 | ],
13 | "migrations": [
14 | "dist/migration/**/*.js"
15 | ],
16 | "subscribers": [
17 | "dist/subscriber/**/*.js"
18 | ],
19 | "cli": {
20 | "entitiesDir": "src/entity",
21 | "migrationsDir": "src/migration",
22 | "subscribersDir": "src/subscriber"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/entity/Comment.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | CreateDateColumn,
4 | Entity,
5 | ManyToOne,
6 | PrimaryGeneratedColumn,
7 | UpdateDateColumn
8 | } from "typeorm";
9 | import {User} from "./User";
10 | import {Post} from "./Post";
11 |
12 | @Entity('comments')
13 | export class Comment {
14 | @PrimaryGeneratedColumn('increment')
15 | id: number;
16 | @Column('text')
17 | content: string;
18 | @CreateDateColumn()
19 | createdAt: Date;
20 | @UpdateDateColumn()
21 | updateAt: Date;
22 | @ManyToOne('User', 'comments')
23 | user: User;
24 | @ManyToOne('Post', 'comments')
25 | post: Post;
26 | }
27 |
--------------------------------------------------------------------------------
/src/migration/1592561070934-AddUniqueUsernameToUsers.ts:
--------------------------------------------------------------------------------
1 | import {MigrationInterface, QueryRunner, TableIndex} from "typeorm";
2 |
3 | export class AddUniqueUsernameToUsers1592561070934 implements MigrationInterface {
4 |
5 | public async up(queryRunner: QueryRunner): Promise {
6 | await queryRunner.createIndex('users',
7 | new TableIndex({
8 | name: 'users_username', columnNames: ['username'],
9 | isUnique: true
10 | }))
11 | }
12 |
13 | public async down(queryRunner: QueryRunner): Promise {
14 | await queryRunner.dropIndex('users', 'users_username');
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/hooks/usePosts.tsx:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from 'react';
2 | import axios from 'axios';
3 |
4 | export const usePosts = () => {
5 | const [posts, setPosts] = useState([]);
6 | const [isLoading, setIsLoading] = useState(false);
7 | const [isEmpty, setIsEmpty] = useState(false);
8 | useEffect(() => {
9 | setIsLoading(true);
10 | axios.get('/api/v1/posts').then(response => {
11 | setPosts(response.data);
12 | setIsLoading(false);
13 | if (response.data.length === 0) {
14 | setIsEmpty(true);
15 | }
16 | }, () => {
17 | setIsLoading(false);
18 | });
19 | }, []);
20 | return {posts, setPosts, isLoading, setIsLoading, isEmpty, setIsEmpty};
21 | };
22 |
--------------------------------------------------------------------------------
/pages/posts/first-post.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Link from 'next/link'
3 | import Head from 'next/head'
4 |
5 | export default function FirstPost() {
6 | return (
7 |
8 |
9 | 第一篇文章
10 |
12 |
13 | first post
14 |
15 | 回到首页
16 |
17 |
点击这里
18 |
19 |
20 |
21 | )
22 | }
--------------------------------------------------------------------------------
/src/entity/Post.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | CreateDateColumn,
4 | Entity,
5 | ManyToOne,
6 | OneToMany,
7 | PrimaryGeneratedColumn,
8 | UpdateDateColumn
9 | } from "typeorm";
10 | import {User} from "./User";
11 | import {Comment} from "./Comment";
12 |
13 | @Entity('posts')
14 | export class Post {
15 | @PrimaryGeneratedColumn('increment')
16 | id: number;
17 | @Column('varchar')
18 | title: string;
19 | @Column('text')
20 | content: string;
21 | authorId: number;
22 | @CreateDateColumn()
23 | createdAt: Date;
24 | @UpdateDateColumn()
25 | updateAt: Date;
26 | @ManyToOne('User', 'posts')
27 | author: User;
28 | @OneToMany('Comment', 'post')
29 | comments: Comment[];
30 | }
31 |
--------------------------------------------------------------------------------
/src/migration/1592201997493-CreateUsers.ts:
--------------------------------------------------------------------------------
1 | import {MigrationInterface, QueryRunner, Table} from "typeorm";
2 |
3 | export class CreateUsers1592201997493 implements MigrationInterface {
4 |
5 | public async up(queryRunner: QueryRunner): Promise {
6 | return await queryRunner.createTable(new Table({
7 | name: 'users',
8 | columns: [
9 | {name: 'id', isGenerated: true, type: 'int', generationStrategy: 'increment', isPrimary: true},
10 | {name: 'username', type: 'varchar'},
11 | {name: 'passwordDigest', type: 'varchar'}
12 | ]
13 | }))
14 | }
15 |
16 | public async down(queryRunner: QueryRunner): Promise {
17 | return await queryRunner.dropTable('users');
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "emitDecoratorMetadata": true,
4 | "experimentalDecorators": true,
5 | "noImplicitAny": true,
6 | "baseUrl": ".",
7 | "target": "es5",
8 | "module": "esnext",
9 | "strict": false,
10 | "esModuleInterop": true,
11 | "lib": [
12 | "dom",
13 | "dom.iterable",
14 | "esnext"
15 | ],
16 | "allowJs": true,
17 | "skipLibCheck": true,
18 | "forceConsistentCasingInFileNames": true,
19 | "noEmit": true,
20 | "moduleResolution": "node",
21 | "resolveJsonModule": true,
22 | "isolatedModules": true,
23 | "jsx": "preserve"
24 | },
25 | "exclude": [
26 | "node_modules"
27 | ],
28 | "include": [
29 | "next-env.d.ts",
30 | "**/*.ts",
31 | "**/*.tsx"
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/pages/api/v1/posts.tsx:
--------------------------------------------------------------------------------
1 | import {NextApiHandler} from 'next';
2 | import {getDatabaseConnection} from '../../../lib/getDatabaseConnection';
3 | import {Post} from 'src/entity/Post';
4 | import {withSession} from '../../../lib/withSession';
5 |
6 | const Posts: NextApiHandler = withSession(async (req, res) => {
7 | if (req.method === 'POST') {
8 | const {title, content} = req.body;
9 | const post = new Post();
10 | post.title = title;
11 | post.content = content;
12 | const user = req.session.get('currentUser');
13 | if (!user) {
14 | res.statusCode = 401;
15 | res.end();
16 | return;
17 | }
18 | post.author = user;
19 | const connection = await getDatabaseConnection();
20 | await connection.manager.save(post);
21 | res.json(post);
22 | }
23 | });
24 |
25 | export default Posts;
26 |
--------------------------------------------------------------------------------
/pages/api/v1/sessions.tsx:
--------------------------------------------------------------------------------
1 | import {NextApiHandler} from 'next';
2 | import {SignIn} from '../../../src/model/SignIn';
3 | import {withSession} from '../../../lib/withSession';
4 |
5 | const Sessions: NextApiHandler = async (req, res) => {
6 | const {username, password} = req.body;
7 | res.setHeader('Content-Type', 'application/json; charset=utf-8');
8 | const signIn = new SignIn();
9 | signIn.username = username;
10 | signIn.password = password;
11 | await signIn.validate();
12 | if (signIn.hasErrors()) {
13 | res.statusCode = 422;
14 | res.end(JSON.stringify(signIn.errors));
15 | } else {
16 | req.session.set('currentUser', signIn.user);
17 | await req.session.save();
18 | res.statusCode = 200;
19 | res.end(JSON.stringify(signIn.user));
20 | }
21 | };
22 |
23 | export default withSession(Sessions);
24 |
--------------------------------------------------------------------------------
/src/migration/1592205680555-CreatePosts.ts:
--------------------------------------------------------------------------------
1 | import {MigrationInterface, QueryRunner, Table} from "typeorm";
2 |
3 | export class CreatePosts1592205680555 implements MigrationInterface {
4 |
5 | public async up(queryRunner: QueryRunner): Promise {
6 | return await queryRunner.createTable(new Table({
7 | name: 'posts',
8 | columns: [
9 | {name: 'id', isGenerated: true, type: 'int', generationStrategy: 'increment', isPrimary: true},
10 | {name: 'title', type: 'varchar'},
11 | {name: 'content', type: 'text'},
12 | {name: 'author_id', type: 'int'}
13 | ]
14 | }))
15 | }
16 |
17 | public async down(queryRunner: QueryRunner): Promise {
18 | return await queryRunner.dropTable('posts');
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/migration/1592206097026-CreateComments.ts:
--------------------------------------------------------------------------------
1 | import {MigrationInterface, QueryRunner, Table} from "typeorm";
2 |
3 | export class CreateComments1592206097026 implements MigrationInterface {
4 |
5 | public async up(queryRunner: QueryRunner): Promise {
6 | return await queryRunner.createTable(new Table({
7 | name: 'comments',
8 | columns: [
9 | {name: 'id', isGenerated: true, type: 'int', generationStrategy: 'increment', isPrimary: true},
10 | {name: 'user_id', type: 'int'},
11 | {name: 'post_id', type: 'int'},
12 | {name: 'content', type: 'text'}
13 | ]
14 | }))
15 | }
16 |
17 | public async down(queryRunner: QueryRunner): Promise {
18 | return await queryRunner.dropTable('comments');
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | listen [::]:80;
4 | server_name localhost;
5 | gzip on;
6 | gzip_disable "msie6";
7 |
8 | gzip_comp_level 6;
9 | gzip_min_length 1100;
10 | gzip_buffers 16 8k;
11 | gzip_proxied any;
12 | gzip_types
13 | text/plain
14 | text/css
15 | text/js
16 | text/xml
17 | text/javascript
18 | application/javascript
19 | application/x-javascript
20 | application/json
21 | application/xml
22 | application/rss+xml
23 | image/svg+xml/javascript;
24 | location ~ ^/nextjs-blog/_next/static/ {
25 | root /usr/share/nginx/html/;
26 | expires 30d;
27 | }
28 |
29 | location / {
30 | proxy_pass http://0.0.0.0:3000;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/seed.ts:
--------------------------------------------------------------------------------
1 | import "reflect-metadata";
2 | import {createConnection} from "typeorm";
3 | import {User} from "./entity/User";
4 | import {Post} from "./entity/Post";
5 | import {Comment} from "./entity/Comment";
6 |
7 | createConnection().then(async connection => {
8 | const {manager} = connection;
9 | // 创建 user1
10 | const u1 = new User();
11 | u1.username = 'frank';
12 | u1.passwordDigest = 'xxx';
13 | await manager.save(u1);
14 | // // 创建 post 1
15 | const p1 = new Post();
16 | p1.title = 'Post 1';
17 | p1.content = 'My First Post';
18 | p1.author = u1;
19 | await manager.save(p1);
20 | const c1 = new Comment();
21 | c1.user = u1;
22 | c1.post = p1;
23 | c1.content = 'Awesome!';
24 | await manager.save(c1);
25 | await connection.close();
26 | console.log('OK!');
27 | })
28 | .catch(error => console.log(error));
29 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import {NextPage} from 'next';
2 | import * as React from 'react';
3 | import Link from 'next/link';
4 |
5 | const Home: NextPage = () => {
6 | return (
7 | <>
8 |
9 |

10 |
Marica 的个人博客
11 |
我是一个热爱编程的人
12 |
13 |
14 | 文章列表
15 |
16 |
17 |
18 |
33 | >
34 | );
35 | };
36 |
37 | export default Home;
38 |
--------------------------------------------------------------------------------
/pages/api/v1/users.tsx:
--------------------------------------------------------------------------------
1 | import {NextApiHandler} from 'next';
2 | import {getDatabaseConnection} from '../../../lib/getDatabaseConnection';
3 | import {User} from 'src/entity/User';
4 |
5 | const Users: NextApiHandler = async (req, res) => {
6 | const {username, password, passwordConfirmation} = req.body;
7 | const connection = await getDatabaseConnection(); // 第一次链接能不能用 get
8 |
9 | const user = new User();
10 | user.username = username.trim();
11 | user.username = username;
12 | user.password = password;
13 | user.passwordConfirmation = passwordConfirmation;
14 |
15 | await user.validate();
16 |
17 | if (user.hasErrors()) {
18 | res.statusCode = 422;
19 | res.write(JSON.stringify(user.errors));
20 | } else {
21 | await connection.manager.save(user);
22 | res.statusCode = 200;
23 | res.write(JSON.stringify(user));
24 | }
25 | res.end();
26 | };
27 |
28 | export default Users;
29 |
--------------------------------------------------------------------------------
/src/model/SignIn.ts:
--------------------------------------------------------------------------------
1 | import {getDatabaseConnection} from '../../lib/getDatabaseConnection';
2 | import {User} from '../entity/User';
3 | import md5 from 'md5';
4 |
5 | export class SignIn {
6 | username: string;
7 | password: string;
8 | user: User;
9 |
10 | errors = {
11 | username: [] as string[], password: [] as string[]
12 | }
13 | async validate() {
14 | if (this.username.trim() === '') {
15 | this.errors.username.push('请填写用户名');
16 | }
17 | const connection = await getDatabaseConnection();
18 | const user = await connection.manager.findOne(User, {where: {username: this.username}})
19 | this.user = user;
20 | if (user) {
21 | if (user.passwordDigest === md5(this.password)) {
22 | this.errors.password.push('密码与用户名不匹配');
23 | }
24 | } else {
25 | this.errors.username.push('用户名不存在');
26 | }
27 | }
28 | hasErrors() {
29 | return !!Object.values(this.errors).find(v => v.length > 0);
30 | }
31 | }
--------------------------------------------------------------------------------
/pages/sign_up.tsx:
--------------------------------------------------------------------------------
1 | import {NextPage} from "next";
2 | import * as React from "react";
3 | import axios from 'axios';
4 | import {useForm} from '../hooks/useForm';
5 |
6 | const signUp: NextPage = () => {
7 | const {form} = useForm({
8 | initFormData: { username: '',
9 | password: '',
10 | passwordConfirmation: ''},
11 | fields: [
12 | {label: '用户名', type: 'text', key: 'username'},
13 | {label: '密码', type: 'password', key: 'password'},
14 | {label: '确认密码', type: 'password', key: 'passwordConfirmation'}
15 | ],
16 | buttons: ,
17 | submit: {
18 | request: formData => axios.post(`/api/v1/users`, formData),
19 | success: () => window.alert('注册成功')
20 | }
21 | });
22 | return (
23 | <>
24 | 注册
25 | {form}
26 | >
27 | )
28 | };
29 |
30 | export default signUp;
31 |
--------------------------------------------------------------------------------
/src/migration/1592208800876-RenameColumns.ts:
--------------------------------------------------------------------------------
1 | import {MigrationInterface, QueryRunner} from "typeorm";
2 |
3 | export class RenameColumns1592208800876 implements MigrationInterface {
4 | public async up(queryRunner: QueryRunner): Promise {
5 | // await queryRunner.renameColumn('users', 'password_digest', 'passwordDigest');
6 | await queryRunner.renameColumn('posts', 'author_id', 'authorId');
7 | await queryRunner.renameColumn('comments', 'user_id', 'userId');
8 | await queryRunner.renameColumn('comments', 'post_id', 'postId');
9 | }
10 |
11 | public async down(queryRunner: QueryRunner): Promise {
12 | // await queryRunner.renameColumn('users', 'passwordDigest', 'password_digest');
13 | await queryRunner.renameColumn('posts', 'authorId', 'author_id');
14 | await queryRunner.renameColumn('comments', 'userId', 'user_id');
15 | await queryRunner.renameColumn('comments', 'postId', 'post_id');
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/lib/getDatabaseConnection.tsx:
--------------------------------------------------------------------------------
1 | import {createConnection, getConnectionManager} from 'typeorm';
2 | import 'reflect-metadata';
3 | import {Post} from 'src/entity/Post';
4 | import {User} from 'src/entity/User';
5 | import {Comment} from 'src/entity/Comment';
6 | import config from '../ormconfig.json';
7 |
8 | const create = async () => {
9 | // @ts-ignore
10 | return createConnection({
11 | ...config,
12 | host: process.env.NODE_ENV === 'production' ? 'localhost' : config.host,
13 | database: process.env.NODE_ENV === 'production' ? 'blog_production' : 'blog_development',
14 | entities: [Post, User, Comment]
15 | });
16 | };
17 |
18 | const promise = (async function () {
19 | const manager = getConnectionManager();
20 | const current = manager.has('default') && manager.get('default');
21 | if (current) {await current.close();}
22 | return create();
23 | })();
24 |
25 | export const getDatabaseConnection = async () => {
26 | return promise;
27 | };
28 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pages/api/v1/posts/[id].tsx:
--------------------------------------------------------------------------------
1 | import {NextApiHandler} from 'next';
2 | import {Post} from 'src/entity/Post';
3 | import {withSession} from '../../../../lib/withSession';
4 | import {getDatabaseConnection} from 'lib/getDatabaseConnection';
5 |
6 | const Posts: NextApiHandler = withSession(async (req, res) => {
7 | if (req.method === 'PATCH') {
8 | const {title, content, id} = req.body;
9 | const connection = await getDatabaseConnection();
10 | const post = await connection.manager.findOne('Post', id);
11 | post.title = title;
12 | post.content = content;
13 | const user = req.session.get('currentUser');
14 | if (!user) {
15 | res.statusCode = 401;
16 | res.end();
17 | return;
18 | }
19 | post.author = user;
20 | await connection.manager.save(post);
21 | res.json(post);
22 | } else if (req.method === 'DELETE') {
23 | const id = req.query.id.toString();
24 | const connection = await getDatabaseConnection();
25 | const result = await connection.manager.delete('Post', id);
26 | res.statusCode = result.affected >= 0 ? 200 : 400;
27 | res.end();
28 | }
29 | });
30 |
31 | export default Posts;
32 |
--------------------------------------------------------------------------------
/lib/post.tsx:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import fs, {promises as fsPromise} from 'fs';
3 | import matter from 'gray-matter';
4 | import marked from 'marked';
5 |
6 |
7 | const markdownDir = path.join(process.cwd(), 'markdown');
8 | export const getPosts = async () => {
9 | const fileNames = await fsPromise.readdir(markdownDir);
10 | const posts = fileNames.map(fileName => {
11 | const fullPath = path.join(markdownDir, fileName);
12 | const id = fileName.replace(/\.md$/g, '');
13 | const text = fs.readFileSync(fullPath, 'utf8');
14 | const {data: {title, date}} = matter(text);
15 | return {
16 | id, title, date
17 | };
18 | });
19 | return posts;
20 | };
21 |
22 | export const getPost = async (id: string) => {
23 | const fullPath = path.join(markdownDir, `${id}.md`);
24 | const text = fs.readFileSync(fullPath, 'utf8');
25 | const {data: {title, date}, content} = matter(text);
26 | const htmlContent = marked(content);
27 |
28 | return {
29 | id, title, date, content, htmlContent
30 | };
31 | };
32 |
33 | export const getPostIds = async () => {
34 | const fileNames = await fsPromise.readdir(markdownDir);
35 | return fileNames.map(item => item.replace(/\.md$/g, ''));
36 | };
37 |
--------------------------------------------------------------------------------
/migrate.patch:
--------------------------------------------------------------------------------
1 | diff --git a/src/entity/User.ts b/src/entity/User.ts
2 | index 59ccec2..116ce7a 100644
3 | --- a/src/entity/User.ts
4 | +++ b/src/entity/User.ts
5 | @@ -8,7 +8,7 @@ import {
6 | } from 'typeorm';
7 | import {Post} from './Post';
8 | import {Comment} from './Comment';
9 | -import {getDatabaseConnection} from '../../lib/getDatabaseConnection';
10 | +// import {getDatabaseConnection} from '../../lib/getDatabaseConnection';
11 | import md5 from 'md5';
12 | import _ from 'lodash';
13 |
14 | @@ -49,11 +49,11 @@ export class User {
15 | if (this.username.trim().length <= 3) {
16 | this.errors.username.push('太短');
17 | }
18 | - const found = await (await getDatabaseConnection()).manager.find(
19 | - User, {username: this.username});
20 | - if (found.length > 0) {
21 | - this.errors.username.push('已存在,不能重复注册');
22 | - }
23 | + // const found = await (await getDatabaseConnection()).manager.find(
24 | + // User, {username: this.username});
25 | + // if (found.length > 0) {
26 | + // this.errors.username.push('已存在,不能重复注册');
27 | + // }
28 | if (this.password === '') {
29 | this.errors.password.push('不能为空');
30 | }
31 |
--------------------------------------------------------------------------------
/pages/posts/new.tsx:
--------------------------------------------------------------------------------
1 | import {NextPage} from 'next';
2 | import * as React from 'react';
3 | import axios from 'axios';
4 | import {useForm} from '../../hooks/useForm';
5 |
6 | const PostsNew: NextPage = () => {
7 | // 类型是静态分析,不受代码顺序影响
8 | const {form} = useForm({
9 | initFormData: {title: '', content: ''},
10 | fields: [
11 | {label: '标题', type: 'text', key: 'title'},
12 | {label: '内容', type: 'textarea', key: 'content'}
13 | ],
14 | buttons:
15 |
16 |
,
17 | submit: {
18 | request: formData => axios.post(`/api/v1/posts`, formData),
19 | success: () => {
20 | window.alert('提交成功');
21 | window.location.href = '/posts';
22 | }
23 | }
24 | });
25 | return (
26 |
27 |
28 | {form}
29 |
30 |
48 |
49 | );
50 | };
51 | export default PostsNew;
52 |
--------------------------------------------------------------------------------
/src/migration/1592206406201-AddCreatedAtAndUpdateAt.ts:
--------------------------------------------------------------------------------
1 | import {MigrationInterface, QueryRunner, TableColumn} from "typeorm";
2 |
3 | export class AddCreatedAtAndUpdateAt1592206406201 implements MigrationInterface {
4 |
5 | public async up(queryRunner: QueryRunner): Promise {
6 | await queryRunner.addColumns('users', [
7 | new TableColumn({name: 'createdAt', type: 'time', isNullable: false, default: 'now()'}),
8 | new TableColumn({name: 'updateAt', type: 'time', isNullable: false, default: 'now()'})
9 | ]);
10 | await queryRunner.addColumns('posts', [
11 | new TableColumn({name: 'createdAt', type: 'time', isNullable: false, default: 'now()'}),
12 | new TableColumn({name: 'updateAt', type: 'time', isNullable: false, default: 'now()'})
13 | ]);
14 | await queryRunner.addColumns('comments', [
15 | new TableColumn({name: 'createdAt', type: 'time', isNullable: false, default: 'now()'}),
16 | new TableColumn({name: 'updateAt', type: 'time', isNullable: false, default: 'now()'})
17 | ])
18 | }
19 |
20 | public async down(queryRunner: QueryRunner): Promise {
21 | await queryRunner.dropColumn('users', 'createdAt');
22 | await queryRunner.dropColumn('users', 'updateAt');
23 | await queryRunner.dropColumn('posts', 'createdAt');
24 | await queryRunner.dropColumn('posts', 'updateAt');
25 | await queryRunner.dropColumn('comments', 'createdAt');
26 | await queryRunner.dropColumn('comments', 'updateAt');
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/pages/sign_in.tsx:
--------------------------------------------------------------------------------
1 | import {GetServerSideProps, GetServerSidePropsContext, NextPage} from 'next';
2 | import * as React from "react";
3 | import axios from 'axios'
4 | import {withSession} from '../lib/withSession';
5 | import {User} from '../src/entity/User';
6 | import {useForm} from '../hooks/useForm';
7 | import qs from 'querystring';
8 |
9 | const SignIn: NextPage<{ user: User }> = (props) => {
10 | const {form} = useForm({
11 | initFormData: { username: '', password: ''},
12 | fields: [
13 | {label: '用户名', type: 'text', key: 'username'},
14 | {label: '密码', type: 'password', key: 'password'}
15 | ],
16 | buttons: ,
17 | submit: {
18 | request: formData =>
19 | axios.post(`/api/v1/sessions`, formData),
20 | success: () => {
21 | window.alert('登录成功');
22 | const query = qs.parse(window.location.search.substr(1));
23 | window.location.href = query.returnTo?.toString() || '/';
24 | }
25 | }
26 | });
27 | return (
28 | <>
29 | {props.user && 当前登录用户为 {props.user.username}
}
30 | 登录
31 | {form}
32 | >
33 | );
34 | };
35 |
36 | export default SignIn;
37 |
38 | export const getServerSideProps: GetServerSideProps = withSession(async (context: GetServerSidePropsContext) => {
39 | // @ts-ignore
40 | const user = context.req.session.get('currentUser') || '';
41 | return {
42 | props: {
43 | user: JSON.parse(JSON.stringify(user))
44 | }
45 | };
46 | });
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs-blog",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "concurrently \"next dev\" \"yarn compile:watch\"",
7 | "compile:watch": "babel -w ./src --out-dir dist --extensions .ts,.tsx",
8 | "compile": "babel ./src --out-dir dist --extensions .ts,.tsx",
9 | "build": "next build",
10 | "start": "next start",
11 | "m:create": "typeorm migration:create",
12 | "m:run": "typeorm migration:run",
13 | "m:revert": "typeorm migration:revert",
14 | "e:create": "typeorm entity:create"
15 | },
16 | "dependencies": {
17 | "@babel/cli": "^7.10.1",
18 | "@types/axios": "^0.14.0",
19 | "axios": "^0.19.2",
20 | "classnames": "^2.2.6",
21 | "github-markdown-css": "^4.0.0",
22 | "gray-matter": "^4.0.2",
23 | "lodash": "^4.17.15",
24 | "marked": "^1.1.0",
25 | "md5": "^2.2.1",
26 | "next": "9.4.1",
27 | "next-images": "^1.4.0",
28 | "next-iron-session": "^4.1.7",
29 | "pg": "^8.2.1",
30 | "react": "16.13.1",
31 | "react-dom": "16.13.1",
32 | "reflect-metadata": "^0.1.13",
33 | "typeorm": "^0.2.25",
34 | "ua-parser-js": "^0.7.21"
35 | },
36 | "devDependencies": {
37 | "@babel/plugin-proposal-decorators": "^7.10.1",
38 | "@types/classnames": "^2.2.10",
39 | "@types/lodash": "^4.14.155",
40 | "@types/marked": "^0.7.4",
41 | "@types/md5": "^2.2.0",
42 | "@types/node": "^14.0.11",
43 | "@types/react": "^16.9.35",
44 | "@types/react-dom": "^16.9.8",
45 | "@types/ua-parser-js": "^0.7.33",
46 | "concurrently": "^5.2.0",
47 | "file-loader": "^6.0.0",
48 | "typescript": "^3.9.3"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 项目创建过程
2 | - [Next.js + TypeScript 入门之项目搭建、三种渲染方式(BSR、SSG、SSR)](https://juejin.im/post/6855917901090652174)
3 | - [TS + TypeORM 踩坑实践 (一) hello ORM](https://juejin.im/post/6857391336929263624)
4 | - [TS + TypeORM 踩坑实践 (二) 操作数据表](https://juejin.im/post/6858509402798817294)
5 | - [从 0 开始部署你的 Node 应用(Ubantu、docker、nginx)](https://juejin.im/post/6864785804066029575)
6 |
7 | # 代码使用
8 |
9 | 请下载本代码,然后用 WebStorm 或者 VSCode 打开。
10 |
11 | ## 启动数据库
12 |
13 | 如果你没有创建过数据库,请运行
14 | ```bash
15 | mkdir blog-data
16 | docker run -v "$PWD/blog-data":/var/lib/postgresql/data -p 5432:5432 -e POSTGRES_USER=blog -e POSTGRES_HOST_AUTH_METHOD=trust -d postgres:12.2
17 |
18 | 或者旧版 Windows Docker 客户端运行下面的代码
19 |
20 | docker run -v "blog-data":/var/lib/postgresql/data -p 5432:5432 -e POSTGRES_USER=blog -e POSTGRES_HOST_AUTH_METHOD=trust -d postgres:12.2
21 | ```
22 |
23 | 如果你创建过数据库,请运行
24 |
25 | ```bash
26 | docker ps -a
27 | docker restart 容器id
28 | ```
29 |
30 | ## 创建数据库
31 |
32 | ```
33 | docker exec -it bash
34 | psql -U blog
35 | CREATE DATABASE blog_development ENCODING 'UTF8' LC_COLLATE 'en_US.utf8' LC_CTYPE 'en_US.utf8';
36 | ```
37 |
38 | ## 数据表
39 |
40 | 首先修改 ormconfig.json 中的 host,然后运行
41 |
42 | ```
43 | yarn m:run
44 | ```
45 |
46 | ## 开发
47 |
48 | ```bash
49 | yarn dev
50 | # or
51 | npm run dev
52 | ```
53 |
54 | ## 部署
55 |
56 | ```bash
57 | # 执行本地脚本
58 | git push
59 | ssh blog@dev1 'bash -s' < bin/deploy.sh
60 | # 执行服务器上脚本
61 | ssh blog@dev1 'sh /home/blog/app/nextjs-blog/bin/deploy.sh'
62 | ```
63 |
64 | ## NGINX 配置
65 | ```bash
66 | docker run --name nginx1 --network=host -v /home/blog/nginx.conf:/etc/nginx/conf.d/default.conf -v /home/blog/app/nextjs-blog/.next/static/:/usr/share/nginx/html/_next/static/ -d nginx:1.19.1
67 | ```
68 |
--------------------------------------------------------------------------------
/hooks/usePager.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import * as React from 'react';
3 | import _ from 'lodash';
4 |
5 | type usePagerOptions = {
6 | page: number,
7 | totalPage: number,
8 | urlMaker?: (n: number) => string
9 | }
10 | const defaultUrlMaker = (n:number) => `?page=${n}`;
11 |
12 | export const usePager = (options: usePagerOptions) => {
13 | const {page, totalPage, urlMaker: _urlMaker} = options;
14 | const urlMaker = _urlMaker || defaultUrlMaker;
15 | const numbers = [];
16 | numbers.push(1);
17 | for (let i = page - 3; i <= page + 3; i++) {
18 | numbers.push(i);
19 | }
20 | numbers.push(totalPage);
21 | const pageNumbers = _.uniq(numbers).sort().filter(n => n >= 1 && n <= totalPage).reduce((result, n) => n - (result[result.length - 1] || 0) === 1 ?
22 | result.concat(n) : result.concat(-1, n), []);
23 |
24 | const pager = totalPage > 1 ? (
25 |
26 | {page !== 1 &&
27 |
上一页
28 | }
29 | {pageNumbers.map(n => n === -1 ?
30 |
... :
31 |
{n}
32 | )}
33 |
34 | {page < totalPage &&
35 |
下一页
36 | }
37 | 第 {page}/{totalPage} 页
38 |
39 |
48 |
49 | ) : null;
50 | return {pager};
51 | };
52 |
--------------------------------------------------------------------------------
/pages/posts/[id]/edit.tsx:
--------------------------------------------------------------------------------
1 | import {GetServerSideProps, NextPage} from 'next';
2 | import * as React from 'react';
3 | import {getDatabaseConnection} from '../../../lib/getDatabaseConnection';
4 | import {useForm} from 'hooks/useForm';
5 | import axios from 'axios';
6 |
7 | type Props = {
8 | id: number;
9 | post: Post;
10 | }
11 |
12 | const PostsEdit: NextPage = (props) => {
13 | const {post, id} = props;
14 | console.log('post');
15 | console.log(post);
16 | const {form} = useForm({
17 | initFormData: {title: post.title, content: post.content},
18 | fields: [
19 | {label: '大标题', type: 'text', key: 'title'},
20 | {label: '内容', type: 'textarea', key: 'content'},
21 | ],
22 | buttons:
23 |
24 |
,
25 | submit: {
26 | request: formData => axios.patch(`/api/v1/posts/${id}`, {...formData, id}),
27 | success: () => {
28 | window.alert('提交成功');
29 | }
30 | }
31 | });
32 | return (
33 |
34 |
35 | {form}
36 |
37 |
55 |
56 | );
57 | };
58 |
59 | export default PostsEdit;
60 |
61 | export const getServerSideProps: GetServerSideProps = async (context) => {
62 | const {id} = context.params;
63 |
64 | const connection = await getDatabaseConnection();
65 | const post = await connection.manager.findOne('Post', context.params.id);
66 |
67 | return {
68 | props: {
69 | id: parseInt(id.toString()),
70 | post: JSON.parse(JSON.stringify(post))
71 | // currentUser
72 | }
73 | };
74 | };
75 |
--------------------------------------------------------------------------------
/dist/seed.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4 |
5 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
6 |
7 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
8 |
9 | require("reflect-metadata");
10 |
11 | var _typeorm = require("typeorm");
12 |
13 | var _User = require("./entity/User");
14 |
15 | var _Post = require("./entity/Post");
16 |
17 | var _Comment = require("./entity/Comment");
18 |
19 | (0, _typeorm.createConnection)().then( /*#__PURE__*/function () {
20 | var _ref = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(connection) {
21 | var manager, u1, p1, c1;
22 | return _regenerator["default"].wrap(function _callee$(_context) {
23 | while (1) {
24 | switch (_context.prev = _context.next) {
25 | case 0:
26 | manager = connection.manager; // 创建 user1
27 |
28 | u1 = new _User.User();
29 | u1.username = 'frank';
30 | u1.passwordDigest = 'xxx';
31 | _context.next = 6;
32 | return manager.save(u1);
33 |
34 | case 6:
35 | // // 创建 post 1
36 | p1 = new _Post.Post();
37 | p1.title = 'Post 1';
38 | p1.content = 'My First Post';
39 | p1.author = u1;
40 | _context.next = 12;
41 | return manager.save(p1);
42 |
43 | case 12:
44 | c1 = new _Comment.Comment();
45 | c1.user = u1;
46 | c1.post = p1;
47 | c1.content = 'Awesome!';
48 | _context.next = 18;
49 | return manager.save(c1);
50 |
51 | case 18:
52 | _context.next = 20;
53 | return connection.close();
54 |
55 | case 20:
56 | console.log('OK!');
57 |
58 | case 21:
59 | case "end":
60 | return _context.stop();
61 | }
62 | }
63 | }, _callee);
64 | }));
65 |
66 | return function (_x) {
67 | return _ref.apply(this, arguments);
68 | };
69 | }())["catch"](function (error) {
70 | return console.log(error);
71 | });
--------------------------------------------------------------------------------
/src/entity/User.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | CreateDateColumn,
4 | UpdateDateColumn,
5 | Entity,
6 | PrimaryGeneratedColumn,
7 | OneToMany, BeforeInsert
8 | } from 'typeorm';
9 | import {Post} from './Post';
10 | import {Comment} from './Comment';
11 | import {getDatabaseConnection} from '../../lib/getDatabaseConnection';
12 | import md5 from 'md5';
13 | import _ from 'lodash';
14 |
15 | @Entity('users')
16 | export class User {
17 | @PrimaryGeneratedColumn('increment')
18 | id: number;
19 | @Column('varchar')
20 | username: string;
21 | @Column('varchar')
22 | passwordDigest: string;
23 | @CreateDateColumn()
24 | createdAt: Date;
25 | @UpdateDateColumn()
26 | updateAt: Date;
27 | @OneToMany('Post', 'author')
28 | posts: Post[];
29 | @OneToMany('Comment', 'user')
30 | comments: Comment[];
31 | errors = {
32 | username: [] as string[],
33 | password: [] as string[],
34 | passwordConfirmation: [] as string[]
35 | };
36 | password: string;
37 | passwordConfirmation: string;
38 |
39 | async validate() {
40 | if (this.username.trim() === '') {
41 | this.errors.username.push('不能为空');
42 | }
43 | if (!/[a-zA-Z0-9]/.test(this.username.trim())) {
44 | this.errors.username.push('格式不合法');
45 | }
46 | if (this.username.trim().length > 42) {
47 | this.errors.username.push('太长');
48 | }
49 | if (this.username.trim().length <= 3) {
50 | this.errors.username.push('太短');
51 | }
52 | const found = await (await getDatabaseConnection()).manager.find(
53 | User, {username: this.username});
54 | if (found.length > 0) {
55 | this.errors.username.push('已存在,不能重复注册');
56 | }
57 | if (this.password === '') {
58 | this.errors.password.push('不能为空');
59 | }
60 | if (this.password !== this.passwordConfirmation) {
61 | this.errors.passwordConfirmation.push('密码不匹配');
62 | }
63 | }
64 |
65 | hasErrors() {
66 | return !!Object.values(this.errors).find(v => v.length > 0);
67 | }
68 |
69 | @BeforeInsert()
70 | generatePasswordDigest() {
71 | this.passwordDigest = md5(this.password);
72 | }
73 |
74 | toJSON() {
75 | return _.omit(this, ['password', 'passwordConfirmation', 'passwordDigest', 'errors']);
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/pages/posts/[id].tsx:
--------------------------------------------------------------------------------
1 | import React, {useCallback} from 'react';
2 | import {GetServerSideProps, GetServerSidePropsContext, NextPage} from 'next';
3 | import {getDatabaseConnection} from '../../lib/getDatabaseConnection';
4 | import {Post} from '../../src/entity/Post';
5 | import marked from 'marked';
6 | import Link from 'next/link';
7 | import {withSession} from '../../lib/withSession';
8 | import axios from 'axios';
9 | import {useRouter} from 'next/router';
10 |
11 | type Props = {
12 | post: Post,
13 | currentUser: User | null,
14 | id: number
15 | }
16 |
17 | const postsShow: NextPage = (props) => {
18 | const {post, currentUser, id} = props;
19 | const router = useRouter();
20 |
21 | const onRemove = useCallback(() => {
22 | axios.delete(`/api/v1/posts/${id}`).then(() => {
23 | window.alert('删除成功');
24 | router.push('/posts')
25 | }, () => {
26 | window.alert('删除失败')
27 | })
28 | }, [id]);
29 |
30 | return (
31 | <>
32 |
33 |
34 | {post.title}
35 | {currentUser &&
36 |
37 | 编辑
38 | 删除
39 |
40 | }
41 |
42 |
43 |
44 |
45 |
59 | >
60 | );
61 | };
62 | export default postsShow;
63 |
64 | export const getServerSideProps: GetServerSideProps = withSession((async (context: GetServerSidePropsContext) => {
65 | const connection = await getDatabaseConnection();
66 | const id = context.params.id;
67 | const post = await connection.manager.findOne('Post', context.params.id);
68 |
69 | const currentUser = (context.req as any).session.get('currentUser') || null;
70 |
71 | return {
72 | props: {
73 | id: parseInt(id.toString()),
74 | post: JSON.parse(JSON.stringify(post)),
75 | currentUser
76 | }
77 | };
78 | }));
79 |
--------------------------------------------------------------------------------
/dist/migration/1592561070934-AddUniqueUsernameToUsers.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports.AddUniqueUsernameToUsers1592561070934 = void 0;
9 |
10 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
11 |
12 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
13 |
14 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
15 |
16 | var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
17 |
18 | var _typeorm = require("typeorm");
19 |
20 | var AddUniqueUsernameToUsers1592561070934 = /*#__PURE__*/function () {
21 | function AddUniqueUsernameToUsers1592561070934() {
22 | (0, _classCallCheck2["default"])(this, AddUniqueUsernameToUsers1592561070934);
23 | }
24 |
25 | (0, _createClass2["default"])(AddUniqueUsernameToUsers1592561070934, [{
26 | key: "up",
27 | value: function () {
28 | var _up = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(queryRunner) {
29 | return _regenerator["default"].wrap(function _callee$(_context) {
30 | while (1) {
31 | switch (_context.prev = _context.next) {
32 | case 0:
33 | _context.next = 2;
34 | return queryRunner.createIndex('users', new _typeorm.TableIndex({
35 | name: 'users_username',
36 | columnNames: ['username'],
37 | isUnique: true
38 | }));
39 |
40 | case 2:
41 | case "end":
42 | return _context.stop();
43 | }
44 | }
45 | }, _callee);
46 | }));
47 |
48 | function up(_x) {
49 | return _up.apply(this, arguments);
50 | }
51 |
52 | return up;
53 | }()
54 | }, {
55 | key: "down",
56 | value: function () {
57 | var _down = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2(queryRunner) {
58 | return _regenerator["default"].wrap(function _callee2$(_context2) {
59 | while (1) {
60 | switch (_context2.prev = _context2.next) {
61 | case 0:
62 | _context2.next = 2;
63 | return queryRunner.dropIndex('users', 'users_username');
64 |
65 | case 2:
66 | case "end":
67 | return _context2.stop();
68 | }
69 | }
70 | }, _callee2);
71 | }));
72 |
73 | function down(_x2) {
74 | return _down.apply(this, arguments);
75 | }
76 |
77 | return down;
78 | }()
79 | }]);
80 | return AddUniqueUsernameToUsers1592561070934;
81 | }();
82 |
83 | exports.AddUniqueUsernameToUsers1592561070934 = AddUniqueUsernameToUsers1592561070934;
--------------------------------------------------------------------------------
/dist/entity/Comment.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports.Comment = void 0;
9 |
10 | var _initializerDefineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/initializerDefineProperty"));
11 |
12 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
13 |
14 | var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
15 |
16 | var _applyDecoratedDescriptor2 = _interopRequireDefault(require("@babel/runtime/helpers/applyDecoratedDescriptor"));
17 |
18 | var _initializerWarningHelper2 = _interopRequireDefault(require("@babel/runtime/helpers/initializerWarningHelper"));
19 |
20 | var _typeorm = require("typeorm");
21 |
22 | var _dec, _dec2, _dec3, _dec4, _dec5, _dec6, _dec7, _class, _class2, _descriptor, _descriptor2, _descriptor3, _descriptor4, _descriptor5, _descriptor6, _temp;
23 |
24 | var Comment = (_dec = (0, _typeorm.Entity)('comments'), _dec2 = (0, _typeorm.PrimaryGeneratedColumn)('increment'), _dec3 = (0, _typeorm.Column)('text'), _dec4 = (0, _typeorm.CreateDateColumn)(), _dec5 = (0, _typeorm.UpdateDateColumn)(), _dec6 = (0, _typeorm.ManyToOne)('User', 'comments'), _dec7 = (0, _typeorm.ManyToOne)('Post', 'comments'), _dec(_class = (_class2 = (_temp = function Comment() {
25 | (0, _classCallCheck2["default"])(this, Comment);
26 | (0, _initializerDefineProperty2["default"])(this, "id", _descriptor, this);
27 | (0, _initializerDefineProperty2["default"])(this, "content", _descriptor2, this);
28 | (0, _initializerDefineProperty2["default"])(this, "createdAt", _descriptor3, this);
29 | (0, _initializerDefineProperty2["default"])(this, "updateAt", _descriptor4, this);
30 | (0, _initializerDefineProperty2["default"])(this, "user", _descriptor5, this);
31 | (0, _initializerDefineProperty2["default"])(this, "post", _descriptor6, this);
32 | }, _temp), (_descriptor = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "id", [_dec2], {
33 | configurable: true,
34 | enumerable: true,
35 | writable: true,
36 | initializer: null
37 | }), _descriptor2 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "content", [_dec3], {
38 | configurable: true,
39 | enumerable: true,
40 | writable: true,
41 | initializer: null
42 | }), _descriptor3 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "createdAt", [_dec4], {
43 | configurable: true,
44 | enumerable: true,
45 | writable: true,
46 | initializer: null
47 | }), _descriptor4 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "updateAt", [_dec5], {
48 | configurable: true,
49 | enumerable: true,
50 | writable: true,
51 | initializer: null
52 | }), _descriptor5 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "user", [_dec6], {
53 | configurable: true,
54 | enumerable: true,
55 | writable: true,
56 | initializer: null
57 | }), _descriptor6 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "post", [_dec7], {
58 | configurable: true,
59 | enumerable: true,
60 | writable: true,
61 | initializer: null
62 | })), _class2)) || _class);
63 | exports.Comment = Comment;
--------------------------------------------------------------------------------
/dist/migration/1592208800876-RenameColumns.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports.RenameColumns1592208800876 = void 0;
9 |
10 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
11 |
12 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
13 |
14 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
15 |
16 | var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
17 |
18 | var RenameColumns1592208800876 = /*#__PURE__*/function () {
19 | function RenameColumns1592208800876() {
20 | (0, _classCallCheck2["default"])(this, RenameColumns1592208800876);
21 | }
22 |
23 | (0, _createClass2["default"])(RenameColumns1592208800876, [{
24 | key: "up",
25 | value: function () {
26 | var _up = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(queryRunner) {
27 | return _regenerator["default"].wrap(function _callee$(_context) {
28 | while (1) {
29 | switch (_context.prev = _context.next) {
30 | case 0:
31 | _context.next = 2;
32 | return queryRunner.renameColumn('posts', 'author_id', 'authorId');
33 |
34 | case 2:
35 | _context.next = 4;
36 | return queryRunner.renameColumn('comments', 'user_id', 'userId');
37 |
38 | case 4:
39 | _context.next = 6;
40 | return queryRunner.renameColumn('comments', 'post_id', 'postId');
41 |
42 | case 6:
43 | case "end":
44 | return _context.stop();
45 | }
46 | }
47 | }, _callee);
48 | }));
49 |
50 | function up(_x) {
51 | return _up.apply(this, arguments);
52 | }
53 |
54 | return up;
55 | }()
56 | }, {
57 | key: "down",
58 | value: function () {
59 | var _down = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2(queryRunner) {
60 | return _regenerator["default"].wrap(function _callee2$(_context2) {
61 | while (1) {
62 | switch (_context2.prev = _context2.next) {
63 | case 0:
64 | _context2.next = 2;
65 | return queryRunner.renameColumn('posts', 'authorId', 'author_id');
66 |
67 | case 2:
68 | _context2.next = 4;
69 | return queryRunner.renameColumn('comments', 'userId', 'user_id');
70 |
71 | case 4:
72 | _context2.next = 6;
73 | return queryRunner.renameColumn('comments', 'postId', 'post_id');
74 |
75 | case 6:
76 | case "end":
77 | return _context2.stop();
78 | }
79 | }
80 | }, _callee2);
81 | }));
82 |
83 | function down(_x2) {
84 | return _down.apply(this, arguments);
85 | }
86 |
87 | return down;
88 | }()
89 | }]);
90 | return RenameColumns1592208800876;
91 | }();
92 |
93 | exports.RenameColumns1592208800876 = RenameColumns1592208800876;
--------------------------------------------------------------------------------
/pages/posts/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Link from 'next/link';
3 | import {GetServerSideProps, GetServerSidePropsContext, NextPage} from 'next';
4 | import {Post} from 'src/entity/Post';
5 | import {getDatabaseConnection} from 'lib/getDatabaseConnection';
6 | import {UAParser} from 'ua-parser-js';
7 | import qs from 'querystring';
8 | import {usePager} from '../../hooks/usePager';
9 | import {withSession} from '../../lib/withSession';
10 |
11 | type Props = {
12 | posts: Post[],
13 | count: number,
14 | perPage: number,
15 | page: number,
16 | totalPage: number,
17 | currentUser: User | null
18 | }
19 |
20 | const PostsIndex: NextPage = (props) => {
21 | const {posts, page, totalPage, currentUser} = props;
22 | const {pager} = usePager({
23 | page, totalPage
24 | });
25 | return (
26 | <>
27 |
28 |
29 | 文章列表
30 | {
31 | currentUser &&
32 | 新增文章
33 | }
34 |
35 | {posts.map(post =>
36 |
41 | )}
42 |
45 |
46 |
72 | >
73 | );
74 | };
75 |
76 | export default PostsIndex;
77 |
78 | export const getServerSideProps: GetServerSideProps = withSession(
79 | async (context: GetServerSidePropsContext) => {
80 | const index = context.req.url.indexOf('?');
81 | const search = context.req.url.substr(index + 1);
82 | const query = qs.parse(search);
83 |
84 | const page = parseInt(query.page && query.page.toString()) || 1;
85 |
86 | const currentUser = (context.req as any).session.get('currentUser') || null;
87 |
88 | console.log('环境变量', process.env.SECRET);
89 |
90 | const connection = await getDatabaseConnection();
91 | const perPage = 10;
92 | const [posts, count] = await connection.manager.findAndCount(Post, {
93 | skip: (page - 1) * perPage, take: perPage
94 | });
95 | const ua = context.req.headers['user-agent'];
96 |
97 | const result = new UAParser(ua).getResult();
98 | return {
99 | props: {
100 | browser: result.browser,
101 | posts: JSON.parse(JSON.stringify(posts)),
102 | count,
103 | perPage, page,
104 | currentUser,
105 | totalPage: Math.ceil(count / perPage)
106 | }
107 | };
108 | });
109 |
--------------------------------------------------------------------------------
/hooks/useForm.tsx:
--------------------------------------------------------------------------------
1 | // 表示 useForm 里有一个类型,这个类型也是 initFormData 的类型
2 | // 因为不知道 T 是什么,
3 | import {ReactChild, useCallback, useState} from 'react';
4 | import * as React from 'react';
5 | import {AxiosResponse} from 'axios';
6 | import cs from 'classnames';
7 |
8 | type Field = {
9 | label: string,
10 | type: 'text' | 'password' | 'textarea',
11 | key: keyof T,
12 | className?: string
13 | };
14 |
15 | type useFormOptions = {
16 | initFormData: T;
17 | fields: Field[];
18 | buttons: ReactChild;
19 | submit: {
20 | request: (formData: T) => Promise>;
21 | success: () => void;
22 | }
23 | }
24 |
25 | export function useForm(options: useFormOptions) {
26 | // 非受控
27 | const {initFormData, fields, buttons, submit} = options;
28 | const [formData, setFormData] = useState(initFormData);
29 | const [errors, setErrors] = useState(() => {
30 | const e: { [k in keyof T]?: string[] } = {};
31 | (Object.keys(initFormData) as Array)
32 | .map((key) => {e[key] = [];});
33 | return e;
34 | });
35 |
36 | const onChange = useCallback((key: keyof T, value: any) => {
37 | setFormData({...formData, [key]: value});
38 | }, [formData]);
39 |
40 | const _onSubmit = useCallback((e) => {
41 | e.preventDefault();
42 | submit.request(formData).then(() => {
43 | submit.success();
44 | }, (error) => {
45 | if (error.response) {
46 | const response: AxiosResponse = error.response;
47 | if (response.status === 422) {
48 | setErrors(response.data);
49 | } else if (response.status === 401) {
50 | window.alert('请先登录');
51 | window.location.href = `/sign_in?returnTo=${encodeURIComponent(window.location.pathname)}`;
52 | }
53 | }
54 | });
55 | }, [submit, formData]);
56 |
57 | const form = (
58 |
103 | );
104 | return {
105 | form: form, setErrors: setErrors
106 | };
107 | }
108 |
--------------------------------------------------------------------------------
/dist/model/SignIn.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports.SignIn = void 0;
9 |
10 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
11 |
12 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
13 |
14 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
15 |
16 | var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
17 |
18 | var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
19 |
20 | var _getDatabaseConnection = require("../../lib/getDatabaseConnection");
21 |
22 | var _User = require("../entity/User");
23 |
24 | var _md = _interopRequireDefault(require("md5"));
25 |
26 | var SignIn = /*#__PURE__*/function () {
27 | function SignIn() {
28 | (0, _classCallCheck2["default"])(this, SignIn);
29 | (0, _defineProperty2["default"])(this, "username", void 0);
30 | (0, _defineProperty2["default"])(this, "password", void 0);
31 | (0, _defineProperty2["default"])(this, "user", void 0);
32 | (0, _defineProperty2["default"])(this, "errors", {
33 | username: [],
34 | password: []
35 | });
36 | }
37 |
38 | (0, _createClass2["default"])(SignIn, [{
39 | key: "validate",
40 | value: function () {
41 | var _validate = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee() {
42 | var connection, user;
43 | return _regenerator["default"].wrap(function _callee$(_context) {
44 | while (1) {
45 | switch (_context.prev = _context.next) {
46 | case 0:
47 | if (this.username.trim() === '') {
48 | this.errors.username.push('请填写用户名');
49 | }
50 |
51 | _context.next = 3;
52 | return (0, _getDatabaseConnection.getDatabaseConnection)();
53 |
54 | case 3:
55 | connection = _context.sent;
56 | _context.next = 6;
57 | return connection.manager.findOne(_User.User, {
58 | where: {
59 | username: this.username
60 | }
61 | });
62 |
63 | case 6:
64 | user = _context.sent;
65 | this.user = user;
66 |
67 | if (user) {
68 | if (user.passwordDigest === (0, _md["default"])(this.password)) {
69 | this.errors.password.push('密码与用户名不匹配');
70 | }
71 | } else {
72 | this.errors.username.push('用户名不存在');
73 | }
74 |
75 | case 9:
76 | case "end":
77 | return _context.stop();
78 | }
79 | }
80 | }, _callee, this);
81 | }));
82 |
83 | function validate() {
84 | return _validate.apply(this, arguments);
85 | }
86 |
87 | return validate;
88 | }()
89 | }, {
90 | key: "hasErrors",
91 | value: function hasErrors() {
92 | return !!Object.values(this.errors).find(function (v) {
93 | return v.length > 0;
94 | });
95 | }
96 | }]);
97 | return SignIn;
98 | }();
99 |
100 | exports.SignIn = SignIn;
--------------------------------------------------------------------------------
/dist/migration/1592201997493-CreateUsers.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports.CreateUsers1592201997493 = void 0;
9 |
10 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
11 |
12 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
13 |
14 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
15 |
16 | var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
17 |
18 | var _typeorm = require("typeorm");
19 |
20 | var CreateUsers1592201997493 = /*#__PURE__*/function () {
21 | function CreateUsers1592201997493() {
22 | (0, _classCallCheck2["default"])(this, CreateUsers1592201997493);
23 | }
24 |
25 | (0, _createClass2["default"])(CreateUsers1592201997493, [{
26 | key: "up",
27 | value: function () {
28 | var _up = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(queryRunner) {
29 | return _regenerator["default"].wrap(function _callee$(_context) {
30 | while (1) {
31 | switch (_context.prev = _context.next) {
32 | case 0:
33 | _context.next = 2;
34 | return queryRunner.createTable(new _typeorm.Table({
35 | name: 'users',
36 | columns: [{
37 | name: 'id',
38 | isGenerated: true,
39 | type: 'int',
40 | generationStrategy: 'increment',
41 | isPrimary: true
42 | }, {
43 | name: 'username',
44 | type: 'varchar'
45 | }, {
46 | name: 'passwordDigest',
47 | type: 'varchar'
48 | }]
49 | }));
50 |
51 | case 2:
52 | return _context.abrupt("return", _context.sent);
53 |
54 | case 3:
55 | case "end":
56 | return _context.stop();
57 | }
58 | }
59 | }, _callee);
60 | }));
61 |
62 | function up(_x) {
63 | return _up.apply(this, arguments);
64 | }
65 |
66 | return up;
67 | }()
68 | }, {
69 | key: "down",
70 | value: function () {
71 | var _down = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2(queryRunner) {
72 | return _regenerator["default"].wrap(function _callee2$(_context2) {
73 | while (1) {
74 | switch (_context2.prev = _context2.next) {
75 | case 0:
76 | _context2.next = 2;
77 | return queryRunner.dropTable('users');
78 |
79 | case 2:
80 | return _context2.abrupt("return", _context2.sent);
81 |
82 | case 3:
83 | case "end":
84 | return _context2.stop();
85 | }
86 | }
87 | }, _callee2);
88 | }));
89 |
90 | function down(_x2) {
91 | return _down.apply(this, arguments);
92 | }
93 |
94 | return down;
95 | }()
96 | }]);
97 | return CreateUsers1592201997493;
98 | }();
99 |
100 | exports.CreateUsers1592201997493 = CreateUsers1592201997493;
--------------------------------------------------------------------------------
/dist/migration/1592205680555-CreatePosts.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports.CreatePosts1592205680555 = void 0;
9 |
10 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
11 |
12 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
13 |
14 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
15 |
16 | var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
17 |
18 | var _typeorm = require("typeorm");
19 |
20 | var CreatePosts1592205680555 = /*#__PURE__*/function () {
21 | function CreatePosts1592205680555() {
22 | (0, _classCallCheck2["default"])(this, CreatePosts1592205680555);
23 | }
24 |
25 | (0, _createClass2["default"])(CreatePosts1592205680555, [{
26 | key: "up",
27 | value: function () {
28 | var _up = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(queryRunner) {
29 | return _regenerator["default"].wrap(function _callee$(_context) {
30 | while (1) {
31 | switch (_context.prev = _context.next) {
32 | case 0:
33 | _context.next = 2;
34 | return queryRunner.createTable(new _typeorm.Table({
35 | name: 'posts',
36 | columns: [{
37 | name: 'id',
38 | isGenerated: true,
39 | type: 'int',
40 | generationStrategy: 'increment',
41 | isPrimary: true
42 | }, {
43 | name: 'title',
44 | type: 'varchar'
45 | }, {
46 | name: 'content',
47 | type: 'text'
48 | }, {
49 | name: 'author_id',
50 | type: 'int'
51 | }]
52 | }));
53 |
54 | case 2:
55 | return _context.abrupt("return", _context.sent);
56 |
57 | case 3:
58 | case "end":
59 | return _context.stop();
60 | }
61 | }
62 | }, _callee);
63 | }));
64 |
65 | function up(_x) {
66 | return _up.apply(this, arguments);
67 | }
68 |
69 | return up;
70 | }()
71 | }, {
72 | key: "down",
73 | value: function () {
74 | var _down = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2(queryRunner) {
75 | return _regenerator["default"].wrap(function _callee2$(_context2) {
76 | while (1) {
77 | switch (_context2.prev = _context2.next) {
78 | case 0:
79 | _context2.next = 2;
80 | return queryRunner.dropTable('posts');
81 |
82 | case 2:
83 | return _context2.abrupt("return", _context2.sent);
84 |
85 | case 3:
86 | case "end":
87 | return _context2.stop();
88 | }
89 | }
90 | }, _callee2);
91 | }));
92 |
93 | function down(_x2) {
94 | return _down.apply(this, arguments);
95 | }
96 |
97 | return down;
98 | }()
99 | }]);
100 | return CreatePosts1592205680555;
101 | }();
102 |
103 | exports.CreatePosts1592205680555 = CreatePosts1592205680555;
--------------------------------------------------------------------------------
/dist/entity/Post.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports.Post = void 0;
9 |
10 | var _initializerDefineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/initializerDefineProperty"));
11 |
12 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
13 |
14 | var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
15 |
16 | var _applyDecoratedDescriptor2 = _interopRequireDefault(require("@babel/runtime/helpers/applyDecoratedDescriptor"));
17 |
18 | var _initializerWarningHelper2 = _interopRequireDefault(require("@babel/runtime/helpers/initializerWarningHelper"));
19 |
20 | var _typeorm = require("typeorm");
21 |
22 | var _dec, _dec2, _dec3, _dec4, _dec5, _dec6, _dec7, _dec8, _class, _class2, _descriptor, _descriptor2, _descriptor3, _descriptor4, _descriptor5, _descriptor6, _descriptor7, _temp;
23 |
24 | var Post = (_dec = (0, _typeorm.Entity)('posts'), _dec2 = (0, _typeorm.PrimaryGeneratedColumn)('increment'), _dec3 = (0, _typeorm.Column)('varchar'), _dec4 = (0, _typeorm.Column)('text'), _dec5 = (0, _typeorm.CreateDateColumn)(), _dec6 = (0, _typeorm.UpdateDateColumn)(), _dec7 = (0, _typeorm.ManyToOne)('User', 'posts'), _dec8 = (0, _typeorm.OneToMany)('Comment', 'post'), _dec(_class = (_class2 = (_temp = function Post() {
25 | (0, _classCallCheck2["default"])(this, Post);
26 | (0, _initializerDefineProperty2["default"])(this, "id", _descriptor, this);
27 | (0, _initializerDefineProperty2["default"])(this, "title", _descriptor2, this);
28 | (0, _initializerDefineProperty2["default"])(this, "content", _descriptor3, this);
29 | (0, _defineProperty2["default"])(this, "authorId", void 0);
30 | (0, _initializerDefineProperty2["default"])(this, "createdAt", _descriptor4, this);
31 | (0, _initializerDefineProperty2["default"])(this, "updateAt", _descriptor5, this);
32 | (0, _initializerDefineProperty2["default"])(this, "author", _descriptor6, this);
33 | (0, _initializerDefineProperty2["default"])(this, "comments", _descriptor7, this);
34 | }, _temp), (_descriptor = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "id", [_dec2], {
35 | configurable: true,
36 | enumerable: true,
37 | writable: true,
38 | initializer: null
39 | }), _descriptor2 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "title", [_dec3], {
40 | configurable: true,
41 | enumerable: true,
42 | writable: true,
43 | initializer: null
44 | }), _descriptor3 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "content", [_dec4], {
45 | configurable: true,
46 | enumerable: true,
47 | writable: true,
48 | initializer: null
49 | }), _descriptor4 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "createdAt", [_dec5], {
50 | configurable: true,
51 | enumerable: true,
52 | writable: true,
53 | initializer: null
54 | }), _descriptor5 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "updateAt", [_dec6], {
55 | configurable: true,
56 | enumerable: true,
57 | writable: true,
58 | initializer: null
59 | }), _descriptor6 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "author", [_dec7], {
60 | configurable: true,
61 | enumerable: true,
62 | writable: true,
63 | initializer: null
64 | }), _descriptor7 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "comments", [_dec8], {
65 | configurable: true,
66 | enumerable: true,
67 | writable: true,
68 | initializer: null
69 | })), _class2)) || _class);
70 | exports.Post = Post;
--------------------------------------------------------------------------------
/dist/migration/1592206097026-CreateComments.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports.CreateComments1592206097026 = void 0;
9 |
10 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
11 |
12 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
13 |
14 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
15 |
16 | var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
17 |
18 | var _typeorm = require("typeorm");
19 |
20 | var CreateComments1592206097026 = /*#__PURE__*/function () {
21 | function CreateComments1592206097026() {
22 | (0, _classCallCheck2["default"])(this, CreateComments1592206097026);
23 | }
24 |
25 | (0, _createClass2["default"])(CreateComments1592206097026, [{
26 | key: "up",
27 | value: function () {
28 | var _up = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(queryRunner) {
29 | return _regenerator["default"].wrap(function _callee$(_context) {
30 | while (1) {
31 | switch (_context.prev = _context.next) {
32 | case 0:
33 | _context.next = 2;
34 | return queryRunner.createTable(new _typeorm.Table({
35 | name: 'comments',
36 | columns: [{
37 | name: 'id',
38 | isGenerated: true,
39 | type: 'int',
40 | generationStrategy: 'increment',
41 | isPrimary: true
42 | }, {
43 | name: 'user_id',
44 | type: 'int'
45 | }, {
46 | name: 'post_id',
47 | type: 'int'
48 | }, {
49 | name: 'content',
50 | type: 'text'
51 | }]
52 | }));
53 |
54 | case 2:
55 | return _context.abrupt("return", _context.sent);
56 |
57 | case 3:
58 | case "end":
59 | return _context.stop();
60 | }
61 | }
62 | }, _callee);
63 | }));
64 |
65 | function up(_x) {
66 | return _up.apply(this, arguments);
67 | }
68 |
69 | return up;
70 | }()
71 | }, {
72 | key: "down",
73 | value: function () {
74 | var _down = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2(queryRunner) {
75 | return _regenerator["default"].wrap(function _callee2$(_context2) {
76 | while (1) {
77 | switch (_context2.prev = _context2.next) {
78 | case 0:
79 | _context2.next = 2;
80 | return queryRunner.dropTable('comments');
81 |
82 | case 2:
83 | return _context2.abrupt("return", _context2.sent);
84 |
85 | case 3:
86 | case "end":
87 | return _context2.stop();
88 | }
89 | }
90 | }, _callee2);
91 | }));
92 |
93 | function down(_x2) {
94 | return _down.apply(this, arguments);
95 | }
96 |
97 | return down;
98 | }()
99 | }]);
100 | return CreateComments1592206097026;
101 | }();
102 |
103 | exports.CreateComments1592206097026 = CreateComments1592206097026;
--------------------------------------------------------------------------------
/study.md:
--------------------------------------------------------------------------------
1 | # 启动项目
2 | ## 启动数据库
3 | mac/linux/windows 新版 docker
4 | ```bash
5 | mkdir blog-data
6 | docker run -v "$PWD/blog-data":/var/lib/postgresql/data -p 5432:5432 -e POSTGRES_USER=blog -e POSTGRES_HOST_AUTH_METHOD=trust -d postgres:12.2
7 | ```
8 | windows toolbox
9 | ```bash
10 | docker-machine run default
11 | / 假设 default是您的Linux VM /
12 |
13 | docker-machine ssh default
14 |
15 | docker run -v "blog-data":/var/lib/postgresql/data -p 5432:5432 -e POSTGRES_USER=blog -e POSTGRES_HOST_AUTH_METHOD=trust -d postgres:12.2
16 |
17 | sudo sed -i "s|EXTRA_ARGS='|EXTRA_ARGS='--registry-mirror=https://xxxx.mirror.aliyuncs.com |g" /var/lib/boot2docker/profile
18 | ```
19 | ## 清空之前的开发环境
20 | # mac & linux & windows 新版docker
21 | ```bash
22 | docker ps
23 | docker kill 容器id
24 | docker rm 容器id
25 |
26 | rm -rf blog-data
27 |
28 | # 或 windows toolbox
29 | docker container prune
30 | docker volume rm blog-data
31 | ```
32 | ## 创建数据库
33 | ```bash
34 | docker exec -it 容器id bash
35 | psql -U blog -W
36 | CREATE DATABASE blog_development ENCODING 'UTF8' LC_COLLATE 'en_US.utf8' LC_CTYPE 'en_US.utf8';
37 | ```
38 | ## 数据库
39 | 首先ToolBox 需要修改 ormconfig.json 中的 host,
40 | 在控制台输入 docker-machine.exe ip, host 修改为这个值
41 |
42 | 由于我们使用了 TypeScript + typeorm,需要在 User.ts 中,删去 getDatabaseConnection 相关代码。
43 | 然后运行
44 | ```bash
45 | yarn m:run
46 | node dist/seed.js
47 | ```
48 | 不然会报错
49 |
50 | ## 开发
51 | ```bash
52 | yarn dev
53 | ```
54 | ## 部署
55 | ```bash
56 | yarn build
57 | yarn start
58 | ```
59 | ----------------------以下是项目创建过程----------------------
60 | # 完成点击 posts 列表查看文章功能
61 | - 加个 Link>a 标签 嘛
62 |
63 | ## [id].tsx
64 | - 步骤
65 | 实现 PostsShow, 从 props 接收 post 数据
66 | 实现 getStaticProps, 从第一个参数接收 params.id
67 | 实现 getStaticPaths, 返回 id 列表
68 |
69 | - 优化 md 文档样式
70 | yarn add marked
71 |
72 | # 启动 docker
73 | docker run -v "$PWD/blog-data":/var/lib/postgresql/data -p 5432:5432 -e POSTGRES_USER=blog -e POSTGRES_HOST_AUTH_METHOD=trust -d postgres:12.2
74 |
75 | docker ps
76 | 查找到容器id值
77 |
78 | docker exec -it 容器id(前3位) bash
79 |
80 | 进入pg命令行
81 | psql -U blog -W
82 |
83 | 执行pg命令
84 | 创建数据库
85 | CREATE DATABASE blog_development ENCODING 'UTF8' LC_COLLATE 'en_US.utf8' LC_CTYPE 'en_US.utf8';
86 |
87 | # 如何运行 TypeScript
88 | - Next.js 默认使用 babel 来将 TS 编译为 JS(内置功能)
89 | - TypeORM 推荐使用 ts-node 来编译(没有内置)
90 | - babel 和 ts-node 对 TS 的支持并非完全一致
91 | - 所以我们必须进行统一,全部都用 babel
92 |
93 | # 做法
94 | yarn add @babel/cli
95 | 打包
96 | npx babel ./src --out-dir dist --extensions ".ts,.tsx"
97 | yarn add --dev @babel/plugin-proposal-decorators
98 |
99 | # 链接数据库
100 | - 步骤
101 | 选做:升级 Node.js 到 v14
102 | 安装 @babel/cli
103 | npx babel ./src --out-dir dist --extensions ".ts,.tsx"
104 | 根据错误提示搜索答案
105 | yarn add --dev @babel/plugin-proposal-decorators
106 | 去 Next.js 官网查看 .babelrc 默认配置,复制
107 | 创建 .babelrc,添加插件
108 | 重新运行刚刚失败的命令
109 | 得到 dist 里面的 JS,运行 node dist/index.js
110 | 根据错误提示再加上数小时的搜索,修改 ormconfig
111 | 重新运行 node dist/index.js
112 | 成功看见 connection 对象即为成功
113 |
114 | # 重要配置:禁用 sync
115 | - ormconfig
116 | "synchronize": false
117 | 如果为 true,那么在连接数据库时,typeorm会自动根据entity目录来修改数据表
118 | 假设entity里面有User,就会自动创建User表
119 |
120 | # 创建表(通过 migration)
121 | \c blog_development 连接 数据库
122 |
123 | - posts表
124 | npx typeorm migration:create -n CreatePost
125 | 得到 src/migrations/{TIMESTAMP}-CreatePost.ts
126 |
127 | # 数据映射到实体
128 | - 背景
129 | 刚刚只是在数据库里创建了 Post,代码如何读写 Post 呢
130 | 答案: 将数据映射到 Entity(实体)
131 | 命令:typeorm entity:create -n Post
132 |
133 | - 知识点
134 | @PrimaryGeneratedColumn('increment')
135 | @Column('varchar')
136 | @Column('text')
137 |
138 | - 如何使用实体
139 | EntityManager 或 Repository
140 |
141 | # 总结
142 | - migration 数据迁移
143 | - entity 实体
144 | - connection 连接
145 | - manager / repo
146 |
147 | # seed 数据填充
148 |
149 | # 解决最难的问题
150 | 1. createConnection 第二遍会报错
151 | 2. getConnection 第一遍会报错
152 | 3. create x 1 + get x n 保存时会触发create
153 | 4. typeorm getConnectionManager 处于 node_modules,保存时不会重新触发
154 | 5. 自己写mananger没用ctrl+s 触发 manager 还原
155 |
156 | # 图解 session
157 |
158 | # 如何在代码中隐藏 密码/秘钥
159 |
160 | # 博客的增删改查
161 |
162 | # 软件 Cypress 进行自动化测试
163 |
--------------------------------------------------------------------------------
/dist/migration/1592206406201-AddCreatedAtAndUpdateAt.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports.AddCreatedAtAndUpdateAt1592206406201 = void 0;
9 |
10 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
11 |
12 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
13 |
14 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
15 |
16 | var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
17 |
18 | var _typeorm = require("typeorm");
19 |
20 | var AddCreatedAtAndUpdateAt1592206406201 = /*#__PURE__*/function () {
21 | function AddCreatedAtAndUpdateAt1592206406201() {
22 | (0, _classCallCheck2["default"])(this, AddCreatedAtAndUpdateAt1592206406201);
23 | }
24 |
25 | (0, _createClass2["default"])(AddCreatedAtAndUpdateAt1592206406201, [{
26 | key: "up",
27 | value: function () {
28 | var _up = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(queryRunner) {
29 | return _regenerator["default"].wrap(function _callee$(_context) {
30 | while (1) {
31 | switch (_context.prev = _context.next) {
32 | case 0:
33 | _context.next = 2;
34 | return queryRunner.addColumns('users', [new _typeorm.TableColumn({
35 | name: 'createdAt',
36 | type: 'time',
37 | isNullable: false,
38 | "default": 'now()'
39 | }), new _typeorm.TableColumn({
40 | name: 'updateAt',
41 | type: 'time',
42 | isNullable: false,
43 | "default": 'now()'
44 | })]);
45 |
46 | case 2:
47 | _context.next = 4;
48 | return queryRunner.addColumns('posts', [new _typeorm.TableColumn({
49 | name: 'createdAt',
50 | type: 'time',
51 | isNullable: false,
52 | "default": 'now()'
53 | }), new _typeorm.TableColumn({
54 | name: 'updateAt',
55 | type: 'time',
56 | isNullable: false,
57 | "default": 'now()'
58 | })]);
59 |
60 | case 4:
61 | _context.next = 6;
62 | return queryRunner.addColumns('comments', [new _typeorm.TableColumn({
63 | name: 'createdAt',
64 | type: 'time',
65 | isNullable: false,
66 | "default": 'now()'
67 | }), new _typeorm.TableColumn({
68 | name: 'updateAt',
69 | type: 'time',
70 | isNullable: false,
71 | "default": 'now()'
72 | })]);
73 |
74 | case 6:
75 | case "end":
76 | return _context.stop();
77 | }
78 | }
79 | }, _callee);
80 | }));
81 |
82 | function up(_x) {
83 | return _up.apply(this, arguments);
84 | }
85 |
86 | return up;
87 | }()
88 | }, {
89 | key: "down",
90 | value: function () {
91 | var _down = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2(queryRunner) {
92 | return _regenerator["default"].wrap(function _callee2$(_context2) {
93 | while (1) {
94 | switch (_context2.prev = _context2.next) {
95 | case 0:
96 | _context2.next = 2;
97 | return queryRunner.dropColumn('users', 'createdAt');
98 |
99 | case 2:
100 | _context2.next = 4;
101 | return queryRunner.dropColumn('users', 'updateAt');
102 |
103 | case 4:
104 | _context2.next = 6;
105 | return queryRunner.dropColumn('posts', 'createdAt');
106 |
107 | case 6:
108 | _context2.next = 8;
109 | return queryRunner.dropColumn('posts', 'updateAt');
110 |
111 | case 8:
112 | _context2.next = 10;
113 | return queryRunner.dropColumn('comments', 'createdAt');
114 |
115 | case 10:
116 | _context2.next = 12;
117 | return queryRunner.dropColumn('comments', 'updateAt');
118 |
119 | case 12:
120 | case "end":
121 | return _context2.stop();
122 | }
123 | }
124 | }, _callee2);
125 | }));
126 |
127 | function down(_x2) {
128 | return _down.apply(this, arguments);
129 | }
130 |
131 | return down;
132 | }()
133 | }]);
134 | return AddCreatedAtAndUpdateAt1592206406201;
135 | }();
136 |
137 | exports.AddCreatedAtAndUpdateAt1592206406201 = AddCreatedAtAndUpdateAt1592206406201;
--------------------------------------------------------------------------------
/dist/entity/User.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports.User = void 0;
9 |
10 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
11 |
12 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
13 |
14 | var _initializerDefineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/initializerDefineProperty"));
15 |
16 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
17 |
18 | var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
19 |
20 | var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
21 |
22 | var _applyDecoratedDescriptor2 = _interopRequireDefault(require("@babel/runtime/helpers/applyDecoratedDescriptor"));
23 |
24 | var _initializerWarningHelper2 = _interopRequireDefault(require("@babel/runtime/helpers/initializerWarningHelper"));
25 |
26 | var _typeorm = require("typeorm");
27 |
28 | var _getDatabaseConnection = require("../../lib/getDatabaseConnection");
29 |
30 | var _md = _interopRequireDefault(require("md5"));
31 |
32 | var _lodash = _interopRequireDefault(require("lodash"));
33 |
34 | var _dec, _dec2, _dec3, _dec4, _dec5, _dec6, _dec7, _dec8, _dec9, _class, _class2, _descriptor, _descriptor2, _descriptor3, _descriptor4, _descriptor5, _descriptor6, _descriptor7, _temp;
35 |
36 | var User = (_dec = (0, _typeorm.Entity)('users'), _dec2 = (0, _typeorm.PrimaryGeneratedColumn)('increment'), _dec3 = (0, _typeorm.Column)('varchar'), _dec4 = (0, _typeorm.Column)('varchar'), _dec5 = (0, _typeorm.CreateDateColumn)(), _dec6 = (0, _typeorm.UpdateDateColumn)(), _dec7 = (0, _typeorm.OneToMany)('Post', 'author'), _dec8 = (0, _typeorm.OneToMany)('Comment', 'user'), _dec9 = (0, _typeorm.BeforeInsert)(), _dec(_class = (_class2 = (_temp = /*#__PURE__*/function () {
37 | function User() {
38 | (0, _classCallCheck2["default"])(this, User);
39 | (0, _initializerDefineProperty2["default"])(this, "id", _descriptor, this);
40 | (0, _initializerDefineProperty2["default"])(this, "username", _descriptor2, this);
41 | (0, _initializerDefineProperty2["default"])(this, "passwordDigest", _descriptor3, this);
42 | (0, _initializerDefineProperty2["default"])(this, "createdAt", _descriptor4, this);
43 | (0, _initializerDefineProperty2["default"])(this, "updateAt", _descriptor5, this);
44 | (0, _initializerDefineProperty2["default"])(this, "posts", _descriptor6, this);
45 | (0, _initializerDefineProperty2["default"])(this, "comments", _descriptor7, this);
46 | (0, _defineProperty2["default"])(this, "errors", {
47 | username: [],
48 | password: [],
49 | passwordConfirmation: []
50 | });
51 | (0, _defineProperty2["default"])(this, "password", void 0);
52 | (0, _defineProperty2["default"])(this, "passwordConfirmation", void 0);
53 | }
54 |
55 | (0, _createClass2["default"])(User, [{
56 | key: "validate",
57 | value: function () {
58 | var _validate = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee() {
59 | var found;
60 | return _regenerator["default"].wrap(function _callee$(_context) {
61 | while (1) {
62 | switch (_context.prev = _context.next) {
63 | case 0:
64 | if (this.username.trim() === '') {
65 | this.errors.username.push('不能为空');
66 | }
67 |
68 | if (!/[a-zA-Z0-9]/.test(this.username.trim())) {
69 | this.errors.username.push('格式不合法');
70 | }
71 |
72 | if (this.username.trim().length > 42) {
73 | this.errors.username.push('太长');
74 | }
75 |
76 | if (this.username.trim().length <= 3) {
77 | this.errors.username.push('太短');
78 | }
79 |
80 | _context.next = 6;
81 | return (0, _getDatabaseConnection.getDatabaseConnection)();
82 |
83 | case 6:
84 | _context.next = 8;
85 | return _context.sent.manager.find(User, {
86 | username: this.username
87 | });
88 |
89 | case 8:
90 | found = _context.sent;
91 |
92 | if (found.length > 0) {
93 | this.errors.username.push('已存在,不能重复注册');
94 | }
95 |
96 | if (this.password === '') {
97 | this.errors.password.push('不能为空');
98 | }
99 |
100 | if (this.password !== this.passwordConfirmation) {
101 | this.errors.passwordConfirmation.push('密码不匹配');
102 | }
103 |
104 | case 12:
105 | case "end":
106 | return _context.stop();
107 | }
108 | }
109 | }, _callee, this);
110 | }));
111 |
112 | function validate() {
113 | return _validate.apply(this, arguments);
114 | }
115 |
116 | return validate;
117 | }()
118 | }, {
119 | key: "hasErrors",
120 | value: function hasErrors() {
121 | return !!Object.values(this.errors).find(function (v) {
122 | return v.length > 0;
123 | });
124 | }
125 | }, {
126 | key: "generatePasswordDigest",
127 | value: function generatePasswordDigest() {
128 | this.passwordDigest = (0, _md["default"])(this.password);
129 | }
130 | }, {
131 | key: "toJSON",
132 | value: function toJSON() {
133 | return _lodash["default"].omit(this, ['password', 'passwordConfirmation', 'passwordDigest', 'errors']);
134 | }
135 | }]);
136 | return User;
137 | }(), _temp), (_descriptor = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "id", [_dec2], {
138 | configurable: true,
139 | enumerable: true,
140 | writable: true,
141 | initializer: null
142 | }), _descriptor2 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "username", [_dec3], {
143 | configurable: true,
144 | enumerable: true,
145 | writable: true,
146 | initializer: null
147 | }), _descriptor3 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "passwordDigest", [_dec4], {
148 | configurable: true,
149 | enumerable: true,
150 | writable: true,
151 | initializer: null
152 | }), _descriptor4 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "createdAt", [_dec5], {
153 | configurable: true,
154 | enumerable: true,
155 | writable: true,
156 | initializer: null
157 | }), _descriptor5 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "updateAt", [_dec6], {
158 | configurable: true,
159 | enumerable: true,
160 | writable: true,
161 | initializer: null
162 | }), _descriptor6 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "posts", [_dec7], {
163 | configurable: true,
164 | enumerable: true,
165 | writable: true,
166 | initializer: null
167 | }), _descriptor7 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "comments", [_dec8], {
168 | configurable: true,
169 | enumerable: true,
170 | writable: true,
171 | initializer: null
172 | }), (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "generatePasswordDigest", [_dec9], Object.getOwnPropertyDescriptor(_class2.prototype, "generatePasswordDigest"), _class2.prototype)), _class2)) || _class);
173 | exports.User = User;
--------------------------------------------------------------------------------