Jay Chou
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee
    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee
  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    # Deep Dive: NestJS + Zod Integration Architecture ## Table of Contents 1. [Project Overview](#project-overview) 2. [TypeScript Decorators](#typescript-decorators) 3. [Metadata Reflection System](#metadata-reflection-system) 4. [Auto-Validation Mechanism](#auto-validation-mechanism) 5. [createZodDto Deep Analysis](#createzoddto-deep-analysis) 6. [Complete Integration Flow](#complete-integration-flow) ## Project Overview ### Repository Structure This is a monorepo containing NestJS + Zod validation utilities: ``` packages/ ├── nestjs-zod/src/ # Core NestJS + Zod integration library │ ├── dto.ts # createZodDto implementation │ ├── pipe.ts # ZodValidationPipe │ ├── guard.ts # ZodGuard for early validation │ ├── exception.ts # Custom validation exceptions │ ├── serializer.ts # Output serialization interceptor │ ├── validate.ts # Core validation logic │ └── openapi/ # OpenAPI/Swagger integration ├── z/src/ # Extended Zod features (deprecated) └── example/src/ # Example application ``` ### Core Features - `createZodDto` - Create DTO classes from Zod schemas - `ZodValidationPipe` - Validate `body`/`query`/`params` using Zod DTOs - `ZodGuard` - Guard routes by validating before other guards - OpenAPI/Swagger support with accurate schema generation ## TypeScript Decorators ### What Are Decorators? Decorators are a design pattern that allows adding extra functionality to classes, methods, properties, or parameters **without modifying the original code**. ### Four Types of Decorators #### 1. Class Decorator ```typescript function MyClassDecorator<T extends { new(...args: any[]): {} }>(constructor: T) { return class extends constructor { newProperty = "added by decorator"; } } @MyClassDecorator class MyClass {} ``` #### 2. Method Decorator ```typescript function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = function(...args: any[]) { console.log(`Calling ${propertyKey} with args:`, args); const result = originalMethod.apply(this, args); console.log(`Result:`, result); return result; }; } class Example { @Log getData(id: number) { return `data-${id}`; } } ``` #### 3. Parameter Decorator ```typescript function ValidateParam(target: any, propertyKey: string, parameterIndex: number) { // Store metadata for other decorators to use const existingValidatedParams = Reflect.getMetadata('validated_params', target, propertyKey) || []; existingValidatedParams.push(parameterIndex); Reflect.defineMetadata('validated_params', existingValidatedParams, target, propertyKey); } class Controller { getData(@ValidateParam id: number) { return `data-${id}`; } } ``` ### NestJS Project Examples #### `UseZodGuard` - Composite Decorator (`guard.ts:46-47`) ```typescript export const UseZodGuard = (source: Source, schemaOrDto: ZodSchema | ZodDto) => UseGuards(new ZodGuard(source, schemaOrDto)) ``` **How it works**: - This is a **Decorator Factory** - Takes parameters and returns an actual decorator - Internally uses NestJS's `UseGuards` decorator #### `ZodSerializerDto` - Metadata Decorator (`serializer.ts:23-24`) ```typescript export const ZodSerializerDto = (dto: ZodDto | ZodSchema) => SetMetadata(ZodSerializerDtoOptions, dto) ``` **How it works**: - Uses `SetMetadata` to store schema information as metadata - Later read by `ZodSerializerInterceptor` ### Decorator Factory Pattern Most practical decorators use the **factory pattern** to accept parameters: ```typescript // Factory function returns the actual decorator function Timeout(ms: number) { return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = async function(...args: any[]) { return Promise.race([ originalMethod.apply(this, args), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms) ) ]); }; }; } class ApiService { @Timeout(5000) // 5 second timeout async fetchData() { // API call } } ``` ## Metadata Reflection System ### Not NestJS Reflector, but `reflect-metadata`! The metadata reflection mentioned is a lower-level mechanism than NestJS's `Reflector` service. ### Core Components #### 1. `reflect-metadata` Library - `reflect-metadata` is an **independent npm package** - Provides JavaScript's **Reflection API** - Allows reading class, method, and property type information at runtime #### 2. TypeScript Configuration ```json // tsconfig.json { "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true // ← Key setting! } } ``` ### How `emitDecoratorMetadata` Works When this option is enabled, TypeScript compiler **automatically generates type information**: #### Before Compilation (TypeScript) ```typescript import 'reflect-metadata'; class UserDto { name: string; email: string; } class UserController { @Post() createUser(@Body() userData: UserDto) { return userData; } } ``` #### After Compilation (JavaScript) ```javascript // TypeScript compiler automatically inserts these metadata! __decorate([ Post(), __metadata("design:type", Function), __metadata("design:paramtypes", [UserDto]), // ← Parameter types! __metadata("design:returntype", void 0) ], UserController.prototype, "createUser", null); ``` ### Reading Metadata at Runtime #### How NestJS Reads Parameter Types ```typescript import 'reflect-metadata'; // 1. NestJS uses Reflect.getMetadata to read type information function getParameterTypes(target: any, propertyKey: string) { return Reflect.getMetadata('design:paramtypes', target, propertyKey); } // 2. Actual usage const UserController = class { createUser(@Body() userData: UserDto) { } }; const paramTypes = getParameterTypes(UserController.prototype, 'createUser'); console.log(paramTypes); // [UserDto] ← This is the type information! ``` ### Three Levels of Reflection #### Level 1: `reflect-metadata` Basic API ```typescript import 'reflect-metadata'; // Manually set metadata Reflect.defineMetadata('custom-key', 'custom-value', target, propertyKey); // Read metadata const value = Reflect.getMetadata('custom-key', target, propertyKey); ``` #### Level 2: TypeScript Auto-Generated ```typescript // TypeScript compiler automatically generates these keys: // - 'design:type' Method return type // - 'design:paramtypes' Parameter type array ← Key! // - 'design:returntype' Return value type ``` #### Level 3: NestJS's Reflector Service ```typescript // NestJS's Reflector (higher-level abstraction) @Injectable() class MyService { constructor(private reflector: Reflector) {} someMethod(context: ExecutionContext) { // Read custom metadata const metadata = this.reflector.get('custom-key', context.getHandler()); } } ``` ### Why `reflect-metadata` is Needed JavaScript natively **lacks** runtime type information: ```javascript // Pure JavaScript - can't know parameter types function createUser(userData) { // What type is userData? No idea console.log(typeof userData); // Can only get "object" } ``` While `reflect-metadata` + TypeScript allows us to: ```typescript // TypeScript + reflect-metadata - can know exact types function createUser(@Body() userData: UserDto) { // At runtime, we can use Reflect.getMetadata to know userData should be UserDto type } ``` ## Auto-Validation Mechanism ### Why Does `@Body() userData: UserDto` Automatically Validate? The answer lies in the perfect combination of: **Global Pipe + TypeScript Reflection + Zod Validation**. ### Complete Flow Analysis #### 1. Global Pipe Registration (`app.module.ts:16-18`) ```typescript { provide: APP_PIPE, useClass: ZodValidationPipe, // ← Key! Global registration } ``` This makes `ZodValidationPipe` a **global pipe** that automatically processes all route parameters. #### 2. Controller Method Definition ```typescript @Post() createUser(@Body() userData: UserDto) { // ← TypeScript type annotation return userData; } ``` #### 3. NestJS Execution Flow When an HTTP request arrives: ```typescript // 1. NestJS parses @Body() decorator, gets request.body const bodyData = request.body; // 2. NestJS gets parameter type through TypeScript metadata const parameterType = UserDto; // ← Retrieved from TypeScript reflection // 3. Execute global ZodValidationPipe.transform() const validatedData = zodValidationPipe.transform(bodyData, { metatype: UserDto, // ← This is key! type: 'body', data: undefined }); ``` #### 4. ZodValidationPipe Core Logic (`pipe.ts:22-34`) ```typescript public transform(value: unknown, metadata: ArgumentMetadata) { const { metatype } = metadata; // ← UserDto class // Check if it's a ZodDto if (!isZodDto(metatype)) { return value; // Not a ZodDto, return directly } // Use ZodDto's built-in schema for validation return validate(value, metatype.schema, createValidationException); } ``` #### 5. Key Check: `isZodDto()` (`dto.ts:33-35`) ```typescript export function isZodDto(metatype: any): metatype is ZodDto<unknown> { return metatype?.isZodDto; // ← Check identifier } ``` #### 6. Validation Execution (`validate.ts:14-22`) ```typescript export function validate(value: unknown, schemaOrDto: ZodSchema | ZodDto) { const schema = isZodDto(schemaOrDto) ? schemaOrDto.schema : schemaOrDto; const result = schema.safeParse(value); // ← Zod validation if (!result.success) { throw createValidationException(result.error); } return result.data; // ← Return validated data } ``` ### The Magic Chain ```mermaid graph TD A[HTTP Request] --> B[NestJS Router] B --> C[Body decorator extracts request.body] C --> D[TypeScript reflection gets UserDto] D --> E[ZodValidationPipe.transform] E --> F[isZodDto check] F --> G[UserDto.schema.safeParse] G --> H[Validation success/failure] H --> I[Return validated data] ``` ### Why This Design? 1. **Developer Experience**: Only need type annotations, no manual validation calls 2. **Type Safety**: TypeScript ensures compile-time type correctness 3. **Runtime Validation**: Zod ensures runtime data correctness 4. **Framework Integration**: Fully integrated into NestJS lifecycle ## createZodDto Deep Analysis ### Core Problem: Why Can't We Use Zod Schema Directly? #### Problem: Direct Schema Usage ```typescript const UserSchema = z.object({ name: z.string(), email: z.string().email() }); // ❌ This doesn't work in NestJS @Post() createUser(@Body() userData: typeof UserSchema) { // This is not a type! } // ❌ This doesn't work either @Post() createUser(@Body() userData: z.infer<typeof UserSchema>) { // Pure interface, no validation logic } ``` #### Requirements Analysis NestJS needs: 1. **Class** - Because `reflect-metadata` needs constructor function 2. **Type Information** - Let TypeScript know parameter types 3. **Validation Logic** - Actual Zod validation functionality 4. **Identifier** - Let Pipe know this is a ZodDto ### Clever Design of `createZodDto` #### Generic Parameters Analysis (`dto.ts:15-18`) ```typescript export function createZodDto< TOutput = any, // Zod schema output type TDef extends ZodTypeDef = ZodTypeDef, // Zod internal definition TInput = TOutput // Zod schema input type (usually equals output) >(schema: ZodSchema<TOutput, TDef, TInput>) ``` **Type Inference Example**: ```typescript const UserSchema = z.object({ name: z.string(), email: z.string().email() }); // TypeScript automatically infers: // TOutput = { name: string; email: string } // TInput = { name: string; email: string } // TDef = ZodObjectDef<...> ``` #### Core Implementation: `AugmentedZodDto` Class (`dto.ts:20-27`) ```typescript class AugmentedZodDto { public static isZodDto = true // ← Identifier public static schema = schema // ← Wrap original schema public static create(input: unknown) { return this.schema.parse(input) // ← Provide static validation method } } ``` **Design Highlights**: 1. **Static Properties** - Can be used without instantiation 2. **Schema Encapsulation** - Preserves all original Zod schema functionality 3. **Identifier** - `isZodDto: true` lets system identify this as ZodDto #### Type Assertion Magic (`dto.ts:29`) ```typescript return AugmentedZodDto as unknown as ZodDto<TOutput, TDef, TInput> ``` **Why `as unknown as`?** - `AugmentedZodDto` is the concrete class implementation - `ZodDto` is the abstract interface definition - TypeScript can't directly convert, needs bridging through `unknown` ### Sophisticated `ZodDto` Interface Design (`dto.ts:4-13`) #### Interface Member Analysis ```typescript export interface ZodDto<TOutput, TDef, TInput> { new (): TOutput // Constructor signature isZodDto: true // Type guard identifier schema: ZodSchema<TOutput, TDef, TInput> // Original Zod schema create(input: unknown): TOutput // Static validation method } ``` **Purpose of `new (): TOutput`**: ```typescript // This signature tells TypeScript: class UserDto extends createZodDto(UserSchema) {} // UserDto instance type is TOutput const user: UserDto = new UserDto(); // user's type is { name: string; email: string } ``` ### Type Identification: `isZodDto` Function (`dto.ts:33-35`) ```typescript export function isZodDto(metatype: any): metatype is ZodDto<unknown> { return metatype?.isZodDto } ``` **Type Guard**: - Checks if object has `isZodDto` property - If yes, TypeScript narrows type to `ZodDto<unknown>` - Allows Validation Pipe to safely access `.schema` property ### Complete Usage Flow #### Step 1: Define Schema and DTO ```typescript // 1. Create Zod Schema const UserSchema = z.object({ name: z.string().min(2), email: z.string().email(), age: z.number().min(18) }); // 2. Use createZodDto to create DTO class class UserDto extends createZodDto(UserSchema) {} // Equivalent to creating this class: class UserDto { static isZodDto = true; static schema = UserSchema; static create(input: unknown) { return UserSchema.parse(input); } } ``` #### Step 2: TypeScript Type Inference ```typescript // TypeScript automatically infers: type UserDto = { name: string; email: string; age: number; } // Instance type const userInstance: UserDto; // { name: string; email: string; age: number } // Class type (Constructor) const UserDtoClass: typeof UserDto; // Class containing isZodDto, schema, create ``` #### Step 3: Usage in NestJS ```typescript @Controller('users') class UserController { @Post() createUser(@Body() userData: UserDto) { // userData is validated, type is { name: string; email: string; age: number } console.log(userData.name); // TypeScript knows this is string return userData; } } ``` #### Step 4: Runtime Validation Flow ```typescript // 1. HTTP request comes in const requestBody = { name: "John", email: "john@example.com", age: 25 }; // 2. NestJS gets parameter type (via reflect-metadata) const paramType = UserDto; // Retrieved from @Body() userData: UserDto // 3. ZodValidationPipe checks if it's ZodDto if (isZodDto(paramType)) { // true, because paramType.isZodDto === true // 4. Execute Zod validation const validatedData = paramType.schema.parse(requestBody); // validatedData = { name: "John", email: "john@example.com", age: 25 } // 5. Pass to controller method controller.createUser(validatedData); } ``` ### Why Class Wrapper is Needed #### Technical Limitations 1. **reflect-metadata requirement**: Can only read type info from class constructors 2. **NestJS decorator system**: Expects parameters to be classes, not schema objects 3. **TypeScript type system**: Needs concrete classes for type inference #### Comparison Analysis ```typescript // ❌ Direct Schema usage - doesn't work const UserSchema = z.object({ name: z.string() }); @Post() createUser(@Body() userData: UserSchema) { // Syntax error } // ❌ Using z.infer - loses validation capability @Post() createUser(@Body() userData: z.infer<typeof UserSchema>) { // userData has type but no validation! } // ✅ Using createZodDto - perfect solution class UserDto extends createZodDto(UserSchema) {} @Post() createUser(@Body() userData: UserDto) { // Has type + has validation + NestJS compatible } ``` ### Advanced Type Inference Example ```typescript // Complex Schema const PostSchema = z.object({ title: z.string().min(1), content: z.string().optional(), tags: z.array(z.string()), author: z.object({ id: z.number(), name: z.string() }), publishedAt: z.date().optional() }); class PostDto extends createZodDto(PostSchema) {} // TypeScript automatically infers: type PostDto = { title: string; content?: string; tags: string[]; author: { id: number; name: string; }; publishedAt?: Date; } // Usage with full type support @Post() createPost(@Body() post: PostDto) { console.log(post.title); // string console.log(post.content?.length); // number | undefined console.log(post.tags[0]); // string console.log(post.author.name); // string } ``` ## Complete Integration Flow ### The Big Picture ```mermaid graph TD A[Developer writes Zod Schema] --> B[createZodDto wraps schema in class] B --> C[TypeScript emits parameter metadata] C --> D[Global ZodValidationPipe intercepts] D --> E[Pipe reads metadata via reflection] E --> F[isZodDto check passes] F --> G[Zod validation executes] G --> H[Validated data reaches controller] ``` ### Key Advantages Summary 1. **Seamless Integration**: Zod schema ↔ NestJS DTO ↔ TypeScript types 2. **Developer Experience**: Write once schema, get validation + types + docs 3. **Runtime Safety**: Automatic validation prevents invalid data from entering business logic 4. **Type Safety**: Compile-time type checking + runtime type validation 5. **Framework Compatibility**: Fully integrated into NestJS lifecycle ### Technology Stack - **TypeScript**: Provides compile-time type safety and metadata emission - **reflect-metadata**: Enables runtime type information access - **Zod**: Provides runtime schema validation - **NestJS**: Orchestrates the entire pipeline through pipes and decorators This architecture represents a sophisticated solution to the impedance mismatch between Zod's functional schema approach and NestJS's class-based architecture, providing developers with the best of both worlds.

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully