From ae8dfaf3639ed1cbbd0e367e4ee81fa0fc21aedb Mon Sep 17 00:00:00 2001 From: sobird Date: Wed, 20 Nov 2024 01:40:17 +0800 Subject: [PATCH] chore: update --- actions/role.ts | 8 +- actions/user.tsx | 9 +- app.d.ts | 1 + app/(authentication)/signin/form.tsx | 7 +- app/(authentication)/signin/page.tsx | 1 + app/api/auth/[...nextauth]/route.ts | 3 +- app/page.tsx | 12 +- app/profile/page.tsx | 3 +- lib/ability.ts | 1 + lib/antd.tsx | 5 +- lib/mailer.tsx | 3 +- lib/sequelize.ts | 38 ++- models/account.ts | 6 +- models/dictionaryItem.ts | 6 +- models/role.ts | 12 +- models/session.ts | 1 + models/user.ts | 51 ++-- models/verificationToken.ts | 3 +- package.json | 5 +- scripts/seed.ts | 19 +- {lib => services/auth}/auth.ts | 17 +- {lib => services/auth}/authPrismaAdapter.ts | 2 +- .../auth}/authSequelizeAdapter.ts | 9 +- test.ts | 51 ++++ test/sequelize.ts | 234 ++++++++++++++++++ zod/user.ts | 4 +- 26 files changed, 427 insertions(+), 84 deletions(-) rename {lib => services/auth}/auth.ts (93%) rename {lib => services/auth}/authPrismaAdapter.ts (98%) rename {lib => services/auth}/authSequelizeAdapter.ts (99%) create mode 100644 test.ts create mode 100644 test/sequelize.ts diff --git a/actions/role.ts b/actions/role.ts index 6f8ab80..53f8a8b 100644 --- a/actions/role.ts +++ b/actions/role.ts @@ -8,10 +8,12 @@ import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; -import { RoleFormZod, RoleFormAttributes } from '@/zod/role'; -import { RoleModel } from '@/models'; -import { getServerAuthToken } from '@/lib/auth'; + import { defineAbilityFor } from '@/lib/ability'; +import { getServerAuthToken } from '@/services/auth/auth'; +import { RoleModel } from '@/models'; +import { RoleFormZod, RoleFormAttributes } from '@/zod/role'; + import { ActionStatus } from '.'; type RoleFormServerActionState = ServerActionState; diff --git a/actions/user.tsx b/actions/user.tsx index 3946b2f..9a88eb2 100644 --- a/actions/user.tsx +++ b/actions/user.tsx @@ -1,17 +1,18 @@ 'use server'; +import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; import { WhereOptions, Op } from 'sequelize'; -import { revalidatePath } from 'next/cache'; -import { UserModel } from '@/models'; -import { generate, verify } from '@/lib/otp'; +import CaptchaEmailBody from '@/components/email-template/captcha'; import { transporter } from '@/lib/mailer'; +import { generate, verify } from '@/lib/otp'; import reactToHtml from '@/lib/reactToHtml'; -import CaptchaEmailBody from '@/components/email-template/captcha'; +import { UserModel } from '@/models'; import { signUpZod, SignUpAttributes, createUserZod, UserAttributes, updateUserZod, } from '@/zod/user'; + import { ActionStatus } from '.'; type UserServerActionState = ServerActionState; diff --git a/app.d.ts b/app.d.ts index b3e33e9..8a01a2f 100644 --- a/app.d.ts +++ b/app.d.ts @@ -1,3 +1,4 @@ +type Optional = Omit & Partial>; /** * 数据列表分页查询参数 */ diff --git a/app/(authentication)/signin/form.tsx b/app/(authentication)/signin/form.tsx index 8737d24..38b476e 100644 --- a/app/(authentication)/signin/form.tsx +++ b/app/(authentication)/signin/form.tsx @@ -1,11 +1,12 @@ 'use client'; -import React, { useState } from 'react'; -import { signIn } from 'next-auth/react'; -import { useRouter } from 'next/navigation'; import { Form, Input, Button, ConfigProvider, message, } from 'antd'; +import { useRouter } from 'next/navigation'; +import { signIn } from 'next-auth/react'; +import React, { useState } from 'react'; + import { isEmail } from '@/utils/validator'; const SigninForm: React.FC = () => { diff --git a/app/(authentication)/signin/page.tsx b/app/(authentication)/signin/page.tsx index 0ca6aab..0ff04aa 100644 --- a/app/(authentication)/signin/page.tsx +++ b/app/(authentication)/signin/page.tsx @@ -1,4 +1,5 @@ import { Metadata } from 'next'; + import SigninForm from './form'; import styles from './page.module.scss'; diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index 0a4c217..f7738da 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -1,5 +1,6 @@ import NextAuth from 'next-auth'; -import { authOptions } from '@/lib/auth'; + +import { authOptions } from '@/services/auth/auth'; const handler = NextAuth(authOptions); diff --git a/app/page.tsx b/app/page.tsx index f5c4347..01ae0a5 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -10,12 +10,12 @@ import { RegisterButton, } from '@/components/buttons'; import { defineAbilityFor } from '@/lib/ability'; -import { getServerAuthToken } from '@/lib/auth'; import { UserModel, SessionModel, AccountModel, sequelize, } from '@/models'; +import { getServerAuthToken } from '@/services/auth/auth'; -const HomePage: IAppPage<{ id: string }> = async () => { +const HomePage = async () => { const token = await getServerAuthToken(); console.log('getServerAuthToken', token); // await sequelize.sync({ force: true }); @@ -41,10 +41,12 @@ const HomePage: IAppPage<{ id: string }> = async () => { }, ]); - console.log('ability', ability.can('read', '/test1')); + console.log('ability', ability); - const users = await UserModel.findAll({ raw: true }); - console.log('users', users); + console.log('ability', ability.can('read', '/test')); + + const users = await UserModel.findAll({ include: [UserModel.associations.Roles] }); + console.log('users', users[0].createdAt); return (
; diff --git a/lib/antd.tsx b/lib/antd.tsx index ac29e8d..0b01be2 100644 --- a/lib/antd.tsx +++ b/lib/antd.tsx @@ -7,9 +7,10 @@ 'use client'; -import React from 'react'; -import { useServerInsertedHTML } from 'next/navigation'; import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs'; +import { useServerInsertedHTML } from 'next/navigation'; +import React from 'react'; + import type Entity from '@ant-design/cssinjs/es/Cache'; const StyledComponentsRegistry = ({ children }: React.PropsWithChildren) => { diff --git a/lib/mailer.tsx b/lib/mailer.tsx index 8da95cd..6e0eda5 100644 --- a/lib/mailer.tsx +++ b/lib/mailer.tsx @@ -1,7 +1,8 @@ import { createTransport, type SendMailOptions } from 'nodemailer'; -import reactToHtml from '@/lib/reactToHtml'; + import Authentication from '@/components/email-template/authentication'; import CaptchaEmailBody from '@/components/email-template/captcha'; +import reactToHtml from '@/lib/reactToHtml'; // 创建发送邮件的对象 export const transporter = createTransport({ diff --git a/lib/sequelize.ts b/lib/sequelize.ts index 9dba917..987e93f 100644 --- a/lib/sequelize.ts +++ b/lib/sequelize.ts @@ -10,15 +10,19 @@ * sobird at 2021/11/16 20:33:20 created. */ -import debug from 'debug'; +import { AbilityTuple, MongoAbility } from '@casl/ability'; +import log4js from 'log4js'; import { - Sequelize, Model, CreationOptional, ModelStatic, InferAttributes, + Sequelize, Model, CreationOptional, ModelStatic, InferAttributes, ModelOptions, DataTypes, } from 'sequelize'; import sqlite3 from 'sqlite3'; -import { AbilityTuple, MongoAbility } from '@casl/ability'; + import { accessibleBy } from '@/casl/toSequelizeQuery'; import type { Models } from '@/models'; +const logger = log4js.getLogger('sequelize'); +logger.level = log4js.levels.DEBUG; + /** 数据库链接实例 */ export const sequelize = new Sequelize({ // The name of the database @@ -114,10 +118,9 @@ export const sequelize = new Sequelize({ // isolationLevel: Transaction.ISOLATION_LEVELS.REPEATABLE_READ logging: (sql, queryObject: any) => { const { type, bind } = queryObject; - const log = debug(`app:sql:${type}`); - log(sql); + logger.debug(type, sql); if (['INSERT', 'UPDATE', 'BULKUPDATE'].includes(type)) { - log(bind); + logger.debug(bind); } }, }); @@ -127,16 +130,20 @@ export const sequelize = new Sequelize({ // console.log('attributes', attributes); // }); +type Initattributes, M extends InstanceType> = Omit>[0], 'updatedAt' | 'createdAt'>; + /** * 模型基类 * * sobird at 2023/12/05 21:08:43 created. */ export class BaseModel extends Model { - declare id?: CreationOptional; + // declare id: CreationOptional; declare createdAt: CreationOptional; + declare updatedAt: CreationOptional; + /** * Helper method for defining associations. * This method is not a part of Sequelize lifecycle. @@ -189,4 +196,21 @@ export class BaseModel extends Model pn, ps, count: 0, rows: [], }; } + + public static define, M extends InstanceType>( + this: MS, + attributes: Initattributes, + options: ModelOptions, + ): MS { + return super.init({ + ...attributes, + + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, + }, { + sequelize, + deletedAt: true, + ...options, + }) as unknown as MS; + } } diff --git a/models/account.ts b/models/account.ts index 4f25fc3..ce7b9a9 100644 --- a/models/account.ts +++ b/models/account.ts @@ -15,7 +15,8 @@ import { DataTypes, type InferAttributes, InferCreationAttributes, CreationOptional, } from 'sequelize'; -import { sequelize, BaseModel } from '@/lib/sequelize'; + +import { BaseModel } from '@/lib/sequelize'; /** These are all the attributes in the Account model */ export type AccountAttributes = InferAttributes; @@ -65,7 +66,7 @@ class Account extends BaseModel { } } -Account.init( +Account.define( { type: { type: DataTypes.STRING, @@ -116,7 +117,6 @@ Account.init( }, }, { - sequelize, modelName: 'Account', }, ); diff --git a/models/dictionaryItem.ts b/models/dictionaryItem.ts index 9e1ce28..4007c53 100644 --- a/models/dictionaryItem.ts +++ b/models/dictionaryItem.ts @@ -8,7 +8,8 @@ import { DataTypes, type InferAttributes, InferCreationAttributes, } from 'sequelize'; -import { sequelize, BaseModel } from '@/lib/sequelize'; + +import { BaseModel } from '@/lib/sequelize'; /** These are all the attributes in the DictionaryItem model */ export type DictionaryItemAttributes = InferAttributes; @@ -28,7 +29,7 @@ class DictionaryItem extends BaseModel; @@ -28,7 +31,7 @@ export type RoleAttributes = InferAttributes; export type RoleCreationAttributes = InferCreationAttributes; class Role extends BaseModel { - declare id?: CreationOptional; + declare id: CreationOptional; declare parentId?: CreationOptional; @@ -78,6 +81,11 @@ class Role extends BaseModel { declare countPermissions: BelongsToManyCountAssociationsMixin; + declare static associations: { + Users: Association; + Permissions: Association; + }; + static associate({ User, UserRole, Permission, RolePermission, }) { diff --git a/models/session.ts b/models/session.ts index d013332..76d7445 100644 --- a/models/session.ts +++ b/models/session.ts @@ -9,6 +9,7 @@ import { DataTypes, type InferAttributes, InferCreationAttributes, } from 'sequelize'; + import { sequelize, BaseModel } from '@/lib/sequelize'; /** These are all the attributes in the Session model */ diff --git a/models/user.ts b/models/user.ts index 9662e24..4afc0ae 100644 --- a/models/user.ts +++ b/models/user.ts @@ -12,9 +12,13 @@ */ import { randomBytes, createHmac } from 'crypto'; + import { DataTypes, Op, - InferAttributes, InferCreationAttributes, CreationOptional, + InferAttributes, + InferCreationAttributes, + CreationOptional, + Association, BelongsToManyGetAssociationsMixin, BelongsToManySetAssociationsMixin, BelongsToManyAddAssociationMixin, @@ -25,18 +29,18 @@ import { BelongsToManyHasAssociationsMixin, BelongsToManyCreateAssociationMixin, BelongsToManyCountAssociationsMixin, + NonAttribute, } from 'sequelize'; -import { sequelize, BaseModel } from '@/lib/sequelize'; -import dayjs from '@/utils/dayjs'; + +import { BaseModel } from '@/lib/sequelize'; + import type Role from './role'; -import { type RoleAttributes } from './role'; /** 隐私属性字段排除 */ export const UserExcludeAttributes = ['salt', 'password', 'emailVerified']; // These are all the attributes in the User model export type UserAttributes = InferAttributes; - // Some attributes are optional in `User.build` and `User.create` calls export type UserCreationAttributes = InferCreationAttributes; // 用户登录属性 @@ -45,6 +49,8 @@ export type UserSigninAttributes = Pick export type UserSignupAttributes = Pick; class User extends BaseModel { + declare id: CreationOptional; + declare username: CreationOptional; declare name: CreationOptional; @@ -75,10 +81,12 @@ class User extends BaseModel { declare updatedBy: CreationOptional; - declare Roles?: CreationOptional; - - // method + declare Roles?: NonAttribute; + // associates method + // Since TS cannot determine model association at compile time + // we have to declare them here purely virtually + // these will not exist until `Model.init` was called. declare getRoles: BelongsToManyGetAssociationsMixin; /** Remove all previous associations and set the new ones */ @@ -112,6 +120,10 @@ class User extends BaseModel { this.belongsToMany(Role, { through: UserRole }); } + declare static associations: { + Roles: Association; + }; + /** 用户注册 */ public static async signUp(attributes: UserSignupAttributes, fields?: (keyof UserAttributes)[]) { const [user, created] = await this.findOrCreate({ @@ -134,7 +146,7 @@ class User extends BaseModel { /** 通过用户名和密码进行用户登录认证 */ public static async signin({ username, password }: UserSigninAttributes) { if (!username || !password) { - return Promise.reject('用户名和密码不能为空'); + throw Error('用户名和密码不能为空'); } const user = await this.findOne({ @@ -143,13 +155,13 @@ class User extends BaseModel { }); if (!user) { - return Promise.reject('用户不存在'); + throw Error('用户不存在'); } if (user.verifyPassword(password)) { return user.get({ plain: true }); } - return Promise.reject('密码不正确'); + throw Error('密码不正确'); } /** @@ -173,8 +185,13 @@ class User extends BaseModel { } } -User.init( +User.define( { + id: { + type: DataTypes.INTEGER.UNSIGNED, + autoIncrement: true, + primaryKey: true, + }, username: { type: DataTypes.STRING(32), unique: true, @@ -243,21 +260,17 @@ User.init( type: DataTypes.INTEGER, }, - createdAt: { - type: DataTypes.DATE, - get() { - return dayjs(this.dataValues.createdAt).format(); - }, - }, + // createdAt: DataTypes.DATE, + // updatedAt: DataTypes.DATE, }, { - sequelize, modelName: 'User', }, ); User.beforeCreate((model) => { if (model.password) { + // eslint-disable-next-line no-param-reassign model.password = User.hashPassword(model.password, model.salt); } // model.ip = fn("INET_ATON", model.ip); // INET_NTOA diff --git a/models/verificationToken.ts b/models/verificationToken.ts index a1fff00..0195690 100644 --- a/models/verificationToken.ts +++ b/models/verificationToken.ts @@ -14,6 +14,7 @@ import { DataTypes, type InferAttributes, InferCreationAttributes, } from 'sequelize'; + import { sequelize, BaseModel } from '@/lib/sequelize'; /** These are all the attributes in the VerificationToken model */ @@ -50,7 +51,7 @@ VerificationToken.init( identifier: { type: DataTypes.STRING, allowNull: false, - comment: 'user id', + comment: 'User id', }, expires: { type: DataTypes.DATE, diff --git a/package.json b/package.json index c1f01db..23473fa 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "dev": "DEBUG=app:* next dev", "start": "next start", "build": "next build", + "db:seed": "NODE_OPTIONS=--import=./node_modules/@sobird/ts-node/register.js node scripts/seed", "lint": "next lint", "test": "jest" }, @@ -25,8 +26,9 @@ "debug": "^4.3.4", "js-cookie": "^3.0.5", "lodash": "^4.17.21", + "log4js": "^6.9.1", "next": "^15.0.3", - "next-auth": "^4.24.5", + "next-auth": "^4.24.10", "nodemailer": "^6.9.7", "otplib": "^12.0.1", "prisma": "^5.7.0", @@ -39,6 +41,7 @@ "zod": "^3.22.4" }, "devDependencies": { + "@sobird/ts-node": "^1.0.0", "@types/js-cookie": "^3.0.6", "@types/lodash": "^4.14.202", "@types/node": "20.10.0", diff --git a/scripts/seed.ts b/scripts/seed.ts index c2a4ee8..89f0ee4 100644 --- a/scripts/seed.ts +++ b/scripts/seed.ts @@ -1,17 +1,4 @@ -import { PrismaClient } from '@prisma/client'; +import { sequelize } from '@/models'; -const prisma = new PrismaClient(); - -async function main() { - // ... you will write your Prisma Client queries here -} - -main() - .then(async () => { - await prisma.$disconnect(); - }) - .catch(async (e) => { - console.error(e); - await prisma.$disconnect(); - process.exit(1); - }); +const res = await sequelize.sync({ alter: true }); +console.log('install models:', res.models); diff --git a/lib/auth.ts b/services/auth/auth.ts similarity index 93% rename from lib/auth.ts rename to services/auth/auth.ts index 1602299..eb2f2fc 100644 --- a/lib/auth.ts +++ b/services/auth/auth.ts @@ -1,5 +1,5 @@ /** - * auth.ts + * Next-Auth Config * * * @see https://authjs.dev/getting-started/typescript#module-augmentation @@ -19,12 +19,13 @@ import { import { encode, getToken } from 'next-auth/jwt'; import CredentialsProvider from 'next-auth/providers/credentials'; import EmailProvider from 'next-auth/providers/email'; +import GithubProvider from 'next-auth/providers/github'; import { v4 as uuidv4 } from 'uuid'; import User from '@/models/user'; import AuthAdapter from './authSequelizeAdapter'; -import { sendVerificationRequest } from './mailer'; +import { sendVerificationRequest } from '../../lib/mailer'; // const cookiesOptions: Partial = { // sessionToken: { @@ -51,7 +52,7 @@ import { sendVerificationRequest } from './mailer'; // }, // }; -const sessionOptions:AuthOptions['session'] = { +const sessionOptions: AuthOptions['session'] = { strategy: 'jwt', // default: database maxAge: 30 * 24 * 60 * 60, // 30 days updateAge: 24 * 60 * 60, // 24 hours @@ -101,15 +102,19 @@ export const authOptions: AuthOptions = { return { name: user.username, email: user.email, - id: user.id, + id: user.id as unknown as string, image: 'image', - role: 'admin', + role: 'manage', }; } catch (e) { throw Error(e); } }, }), + GithubProvider({ + clientId: 'sobird', + clientSecret: 'sobird', + }), /** * The Email authentication provider can only be used if a database is configured. * This is required to store the verification token. Please see the email provider for more details. @@ -191,7 +196,7 @@ export const authOptions: AuthOptions = { }, }, pages: { - // signIn: '/signin', + signIn: '/signin', verifyRequest: '/signin/verify', }, }; diff --git a/lib/authPrismaAdapter.ts b/services/auth/authPrismaAdapter.ts similarity index 98% rename from lib/authPrismaAdapter.ts rename to services/auth/authPrismaAdapter.ts index c3adc3d..91fec66 100644 --- a/lib/authPrismaAdapter.ts +++ b/services/auth/authPrismaAdapter.ts @@ -5,11 +5,11 @@ * sobird at 2023/11/29 10:18:40 created. */ +import prisma from '../../lib/prisma'; import type { Adapter, AdapterAccount, AdapterUser, AdapterSession, } from '@auth/core/adapters'; import type { Prisma } from '@prisma/client'; -import prisma from './prisma'; const AuthAdapter: Adapter = { async createUser(data) { diff --git a/lib/authSequelizeAdapter.ts b/services/auth/authSequelizeAdapter.ts similarity index 99% rename from lib/authSequelizeAdapter.ts rename to services/auth/authSequelizeAdapter.ts index 6b76980..ca59dce 100644 --- a/lib/authSequelizeAdapter.ts +++ b/services/auth/authSequelizeAdapter.ts @@ -1,14 +1,15 @@ /** - * Custom Adapter + * Next-Auth Custom Adapter * * sobird at 2023/11/29 10:18:40 created. */ -import type { Adapter } from '@auth/core/adapters'; -import User from '@/models/user'; -import VerificationToken from '@/models/verificationToken'; import Account from '@/models/account'; import Session from '@/models/session'; +import User from '@/models/user'; +import VerificationToken from '@/models/verificationToken'; + +import type { Adapter } from '@auth/core/adapters'; const AuthAdapter: Adapter = { async createUser(record) { diff --git a/test.ts b/test.ts new file mode 100644 index 0000000..48af921 --- /dev/null +++ b/test.ts @@ -0,0 +1,51 @@ +/* eslint-disable max-classes-per-file */ +import { Model, ModelStatic, Optional } from 'sequelize'; + +type NonConstructorKeys = ({ [P in keyof T]: T[P] extends new () => any ? never : P })[keyof T]; +type NonConstructor = Pick>; + +class MyClass { + constructor() {} + + prop1: string; + + static prop2: number; + + method() {} +} + +type ClassKeys = ({ [P in keyof T]: P })[keyof T]; + +type MyClassNonConstructor = ModelStatic; + +interface UserAttributes { + username: string; +} + +interface UserCreationAttributes extends UserAttributes {} + +export class User extends Model { + static init(sequelize: Sequelize, DataTypes: typeof DataTypes) { + super.init({ + username: DataTypes.STRING, + }, { + sequelize, + tableName: 'users', + }); + } +} + +export type UserModelStatic = ModelStatic; + +export type DD = InstanceType; + +interface Parent { + name: string; + age: number; + gender: string; + desc?: string; +} + +type Test = Optional; + +const test:Test; diff --git a/test/sequelize.ts b/test/sequelize.ts new file mode 100644 index 0000000..2355307 --- /dev/null +++ b/test/sequelize.ts @@ -0,0 +1,234 @@ +/* eslint-disable max-classes-per-file */ +import { + Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, + HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, + HasManySetAssociationsMixin, HasManyAddAssociationsMixin, HasManyHasAssociationsMixin, + HasManyRemoveAssociationMixin, HasManyRemoveAssociationsMixin, Model, ModelDefined, Optional, + Sequelize, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute, +} from 'sequelize'; + +const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb'); + +// 'projects' is excluded as it's not an attribute, it's an association. +class User extends Model, InferCreationAttributes> { + // id can be undefined during creation when using `autoIncrement` + declare id: CreationOptional; + + declare name: string; + + declare preferredName: string | null; // for nullable fields + + // timestamps! + // createdAt can be undefined during creation + declare createdAt: CreationOptional; + + // updatedAt can be undefined during creation + declare updatedAt: CreationOptional; + + // Since TS cannot determine model association at compile time + // we have to declare them here purely virtually + // these will not exist until `Model.init` was called. + declare getProjects: HasManyGetAssociationsMixin; // Note the null assertions! + + declare addProject: HasManyAddAssociationMixin; + + declare addProjects: HasManyAddAssociationsMixin; + + declare setProjects: HasManySetAssociationsMixin; + + declare removeProject: HasManyRemoveAssociationMixin; + + declare removeProjects: HasManyRemoveAssociationsMixin; + + declare hasProject: HasManyHasAssociationMixin; + + declare hasProjects: HasManyHasAssociationsMixin; + + declare countProjects: HasManyCountAssociationsMixin; + + declare createProject: HasManyCreateAssociationMixin; + + // You can also pre-declare possible inclusions, these will only be populated if you + // actively include a relation. + declare projects?: NonAttribute; // Note this is optional since it's only populated when explicitly requested in code + + // getters that are not attributes should be tagged using NonAttribute + // to remove them from the model's Attribute Typings. + get fullName(): NonAttribute { + return this.name; + } + + declare static associations: { + projects: Association; + }; +} + +class Project extends Model, InferCreationAttributes> { + // id can be undefined during creation when using `autoIncrement` + declare id: CreationOptional; + + declare ownerId: number; + + declare name: string; + + // `owner` is an eagerly-loaded association. + // We tag it as `NonAttribute` + declare owner?: NonAttribute; + + // createdAt can be undefined during creation + declare createdAt: CreationOptional; + + // updatedAt can be undefined during creation + declare updatedAt: CreationOptional; +} + +class Address extends Model, InferCreationAttributes
> { + declare userId: number; + + declare address: string; + + // createdAt can be undefined during creation + declare createdAt: CreationOptional; + + // updatedAt can be undefined during creation + declare updatedAt: CreationOptional; +} + +Project.init( + { + id: { + type: DataTypes.INTEGER.UNSIGNED, + autoIncrement: true, + primaryKey: true, + }, + ownerId: { + type: DataTypes.INTEGER.UNSIGNED, + allowNull: false, + }, + name: { + type: new DataTypes.STRING(128), + allowNull: false, + }, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, + }, + { + sequelize, + tableName: 'projects', + }, +); + +User.init( + { + id: { + type: DataTypes.INTEGER.UNSIGNED, + autoIncrement: true, + primaryKey: true, + }, + name: { + type: new DataTypes.STRING(128), + allowNull: false, + }, + preferredName: { + type: new DataTypes.STRING(128), + allowNull: true, + }, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, + }, + { + tableName: 'users', + sequelize, // passing the `sequelize` instance is required + }, +); + +Address.init( + { + userId: { + type: DataTypes.INTEGER.UNSIGNED, + }, + address: { + type: new DataTypes.STRING(128), + allowNull: false, + }, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, + }, + { + tableName: 'address', + sequelize, // passing the `sequelize` instance is required + }, +); + +// You can also define modules in a functional way +interface NoteAttributes { + id: number; + title: string; + content: string; +} + +// You can also set multiple attributes optional at once +type NoteCreationAttributes = Optional; + +// And with a functional approach defining a module looks like this +const Note: ModelDefined = sequelize.define( + 'Note', + { + id: { + type: DataTypes.INTEGER.UNSIGNED, + autoIncrement: true, + primaryKey: true, + }, + title: { + type: new DataTypes.STRING(64), + defaultValue: 'Unnamed Note', + }, + content: { + type: new DataTypes.STRING(4096), + allowNull: false, + }, + }, + { + tableName: 'notes', + }, +); + +console.log('Note', Note); + +// Here we associate which actually populates out pre-declared `association` static and other methods. +User.hasMany(Project, { + sourceKey: 'id', + foreignKey: 'ownerId', + as: 'projects', // this determines the name in `associations`! +}); + +Address.belongsTo(User, { targetKey: 'id' }); +User.hasOne(Address, { sourceKey: 'id' }); + +async function doStuffWithUser() { + const newUser = await User.create({ + name: 'Johnny', + preferredName: 'John', + }); + console.log(newUser.id, newUser.name, newUser.preferredName); + + const project = await newUser.createProject({ + name: 'first!', + }); + + console.log('project', project); + + const ourUser = await User.findByPk(1, { + include: [User.associations.projects], + rejectOnEmpty: true, // Specifying true here removes `null` from the return type! + }); + + // Note the `!` null assertion since TS can't know if we included + // the model or not + console.log(ourUser.projects![0].name); +} + +(async () => { + await sequelize.sync(); + await doStuffWithUser(); +})(); diff --git a/zod/user.ts b/zod/user.ts index 723e4db..899d446 100644 --- a/zod/user.ts +++ b/zod/user.ts @@ -1,8 +1,10 @@ import { z } from 'zod'; -import { createFormRule } from '.'; + import { existsAction, verifyVerificationCode } from '@/actions/user'; import { analyzePassword, isChineseName, isMobilePhone } from '@/utils/validator'; +import { createFormRule } from '.'; + const usernameZod = z .object({ id: z.number().optional(),