Skip to content

Commit

Permalink
feat: createUrlSchema
Browse files Browse the repository at this point in the history
  • Loading branch information
zeyu2001 authored Sep 17, 2024
1 parent b934a1b commit 81ad010
Show file tree
Hide file tree
Showing 11 changed files with 83 additions and 10 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Release
on:
push:
tags:
- "v*"
- "*/v*"
workflow_dispatch: {}

permissions:
Expand Down Expand Up @@ -56,4 +56,3 @@ jobs:
run: pnpm -r publish --no-git-checks --tag latest --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

4 changes: 3 additions & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
"license": "ISC",
"devDependencies": {
"vitepress": "^1.3.1",
"vue": "~3.4.31"
"vue": "~3.4.31",
"@opengovsg/starter-kitty-validators": "workspace:*",
"@opengovsg/starter-kitty-fs": "workspace:*"
},
"prettier": "@opengovsg/prettier-config-starter-kitty"
}
4 changes: 4 additions & 0 deletions etc/starter-kitty-validators.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@

import { z } from 'zod';
import { ZodSchema } from 'zod';
import { ZodTypeDef } from 'zod';

// @public
export const createEmailSchema: (options?: EmailValidatorOptions) => ZodSchema<string>;

// @public
export const createPathSchema: (options: PathValidatorOptions) => ZodSchema<string>;

// @public
export const createUrlSchema: (options?: UrlValidatorOptions) => ZodSchema<URL, ZodTypeDef, string>;

// @public
export interface EmailValidatorOptions {
domains?: {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"build:docs": "turbo build:docs",
"dev": "turbo dev",
"lint": "turbo lint",
"lint:fix": "turbo lint:fix",
"test": "turbo test",
"ci:report": "turbo ci:report",
"format": "turbo format",
Expand Down
1 change: 1 addition & 0 deletions packages/safe-fs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"build:report": "api-extractor run --local --verbose",
"build:docs": "api-documenter markdown --input-folder ../../temp/ --output-folder ../../apps/docs/api/",
"lint": "eslint \"**/*.{js,jsx,ts,tsx}\" --cache",
"lint:fix": "eslint \"**/*.{js,jsx,ts,tsx}\" --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test": "vitest",
Expand Down
3 changes: 2 additions & 1 deletion packages/validators/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@opengovsg/starter-kitty-validators",
"version": "1.2.3",
"version": "1.2.4",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
Expand All @@ -11,6 +11,7 @@
"build:report": "api-extractor run --local --verbose",
"build:docs": "api-documenter markdown --input-folder ../../temp/ --output-folder ../../apps/docs/api/",
"lint": "eslint \"**/*.{js,jsx,ts,tsx}\" --cache",
"lint:fix": "eslint \"**/*.{js,jsx,ts,tsx}\" --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test": "vitest",
Expand Down
40 changes: 39 additions & 1 deletion packages/validators/src/__tests__/url.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'

import { OptionsError } from '@/common/errors'
import { UrlValidator } from '@/index'
import { createUrlSchema, UrlValidator } from '@/index'
import { UrlValidationError } from '@/url/errors'

describe('UrlValidator with default options', () => {
Expand Down Expand Up @@ -116,3 +116,41 @@ describe('UrlValidator with invalid options', () => {
expect(() => new UrlValidator({ baseOrigin: 'https://example.com/path' })).toThrow(OptionsError)
})
})

describe('createUrlSchema', () => {
it('should create a schema with default options', () => {
const schema = createUrlSchema()
expect(() => schema.parse('https://example.com')).not.toThrow()
})

it('should create a schema with custom options', () => {
const schema = createUrlSchema({
whitelist: {
protocols: ['http', 'https', 'mailto'],
},
})
expect(() => schema.parse('mailto:[email protected]')).not.toThrow()
})

it('should throw an error when the options are invalid', () => {
expect(() => createUrlSchema({ baseOrigin: 'invalid' })).toThrow(OptionsError)
expect(() => createUrlSchema({ baseOrigin: 'ftp://example.com' })).toThrow(OptionsError)
expect(() => createUrlSchema({ baseOrigin: 'https://example.com/path' })).toThrow(OptionsError)
})

it('should not throw an error when the options are valid', () => {
expect(() =>
createUrlSchema({
whitelist: {
protocols: ['http', 'https'],
hosts: ['example.com'],
},
}),
).not.toThrow()
})

it('should reject relaative URLs when the base URL is not provided', () => {
const schema = createUrlSchema()
expect(() => schema.parse('/path')).toThrow(UrlValidationError)
})
})
24 changes: 21 additions & 3 deletions packages/validators/src/url/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { ZodError } from 'zod'
import { ZodError, ZodSchema, ZodTypeDef } from 'zod'
import { fromError } from 'zod-validation-error'

import { OptionsError } from '@/common/errors'
import { UrlValidationError } from '@/url/errors'
import { defaultOptions, optionsSchema, UrlValidatorOptions } from '@/url/options'
import { createUrlSchema } from '@/url/schema'
import { toSchema } from '@/url/schema'

/**
* Parses URLs according to WHATWG standards and validates against a whitelist of allowed protocols and hostnames,
Expand Down Expand Up @@ -36,7 +36,7 @@ export class UrlValidator {
constructor(options: UrlValidatorOptions = defaultOptions) {
const result = optionsSchema.safeParse({ ...defaultOptions, ...options })
if (result.success) {
this.schema = createUrlSchema(result.data)
this.schema = toSchema(result.data)
return
}
throw new OptionsError(fromError(result.error).toString())
Expand All @@ -63,3 +63,21 @@ export class UrlValidator {
}
}
}

/**
* Create a schema that validates user-supplied URLs. This does the same thing as the `UrlValidator` class,
* but it returns a Zod schema which can be used as part of a larger schema.
*
* @param options - The options to use for validation
* @throws {@link OptionsError} If the options are invalid
* @returns A Zod schema that validates paths.
*
* @public
*/
export const createUrlSchema = (options: UrlValidatorOptions = defaultOptions): ZodSchema<URL, ZodTypeDef, string> => {
const result = optionsSchema.safeParse({ ...defaultOptions, ...options })
if (result.success) {
return toSchema(result.data)
}
throw new OptionsError(fromError(result.error).toString())
}
2 changes: 1 addition & 1 deletion packages/validators/src/url/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { z } from 'zod'
import { ParsedUrlValidatorOptions } from '@/url/options'
import { isSafeUrl, resolveRelativeUrl } from '@/url/utils'

export const createUrlSchema = (options: ParsedUrlValidatorOptions) => {
export const toSchema = (options: ParsedUrlValidatorOptions) => {
return z
.string()
.transform(url => resolveRelativeUrl(url, options.baseOrigin))
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
"dependsOn": ["build"]
},
"build:docs": {
"dependsOn": ["^build:docs", "build:report"]
"dependsOn": ["^build:report", "^build:docs"]
},
"lint": {
"dependsOn": ["^lint"]
},
"lint:fix": {
"dependsOn": ["^lint:fix"]
},
"format": {
"dependsOn": ["^format"]
},
Expand Down

0 comments on commit 81ad010

Please sign in to comment.